権限のない一般ユーザーがRedmineのマイページのカレンダーを乗っ取った件


背景

Redmine Advent Calendar 2019の第3日目の記事として「JavaScript」を利用して「権限のない一般ユーザーがRedmineのカレンダーを乗っ取った件」を投稿します。
(昨日はhanachinさんの「agileware-jp/redmine-plugin orb実装詳解」でした。)

元々Advent Calendarがクリスマスまでのカウントダウン用のカレンダーだそうです。1 2019年、令和元年も今日を含めて後残り29日、クリスマスまでは残り23日になりました。
「Advent Calendar」イベント自体への参加が今回初めてで、Redmine Advent Calendarでどんな記事を書こうかと悩みましたが、「Redmine Advent Calendar」にちなんで、RedmineのマイページのCalendarに関するカスタマイズ方法を書くことにしました。

やりたいこと

RedmineのマイページのCalendarは1週間分が表示されています。

alt

これでは12/3現在、クリスマスがいつなのかわかりません。それでRedmineのマイページのCalendarを1か月分表示をするようにカスタマイズをしてみます。ついでに来年のカレンダーも気になりますので、来月のカレンダーも表示をさせます。

完成イメージです。
alt

しかし、私にはRedmineの管理者権限がありません。だからかの有名な「redmine-view-customize」プラグインも使えません。そして当然、rubyの改修、変更もできません。「なんとかJavaScript(jQuery)だけでRedmineのマイページのカレンダーを乗っ取りたい」、これが今日のお題になります。

rubyソースの修正を通してマイページのカレンダーを月表示させる方法は検索をすると多数でてきます。ただし私が調べた範囲では2か月分のカレンダーを表示させる方法は見受けられなかったかと思います。ざっくりとしか調べていないので、rubyによるマイページのカレンダー2か月分表示の方法がありましたら、コメントにて教えてください。

JavaScript(jQuery)は妄想世界を創りだせるので、現実世界(元々のhtmlで表現された世界)では一週間分のカレンダーしかありませんが、JavaScript(jQuery)で他のページの一か月分のカレンダーを召喚して、元々のページに接ぎ木をするイメージで妄想をしてみます。2

方針

1.Redmineのマイページにカレンダーが存在すれば、以下のごにょごにょの処理をする。存在しなければ何もしない。
2.Redmineのマイページの1週間のカレンダーをバシルーラさせる3
3.Redmineのどこからか一か月分のカレンダーを召喚してくる
4.来月のカレンダーのURLもついでにgetして、そのカレンダーも表示させる
5.永続して上記の処理が自動実行できるようにChrome拡張させてカレンダを乗っ取る

ソースの説明

まずは下記のようなJavaScriptを作成して、ブラウザ(Chrome)の「検証」->「Console」で意図した通り動作するか検証を行います。

qiita.rb
$(function () {
  //マイページにカレンダーがある場合のみ、処理を行う
  if($("#block-calendar form").length){
    console.log("処理の開始")
    //もともとのカレンダーの中身を削除する
    $("#block-calendar form").remove()
    //今月のカレンダーを取得
    $.ajax({
        url: $(".home").attr("href") + "issues/calendar",
        type: 'get',
        async: false,
        contenttype:'application/json; charset=utf-8',
    })
    .done(function( data ) {
        $(data).find("table.cal").appendTo("#block-calendar")
        nextMonthURL=$(data).find("#query_form_with_buttons > p.contextual > a:nth-child(2)").attr('href')
        nextMonthText=$(data).find("#query_form_with_buttons > p.contextual > a:nth-child(2)").text().replace(" »", "")
    })

    //来月のカレンダーを取得
    $.ajax({
        url: nextMonthURL,
        type: 'get',
        contenttype:'application/json; charset=utf-8',
    })
    .done(function( data ) {
        $("<h3>"+nextMonthText+"</h3>").appendTo("#block-calendar")
        $(data).find("table.cal").appendTo("#block-calendar")
    })
  }else{
    console.log("処理の中断")
  }
});

簡単にソースを説明すると
1.$(function () {でhtmlなどが読み込まれるまで待機します。
2.$("#block-calendar form")これがもともとのカレンダーの中身なので.remove()で1週間のカレンダーを削除します。

元々のRedmineのマイページのカレンダーのHTML構成は下図のようになっています。
「block-calendar」というidのdivが大枠で、カレンダーの実態はformタグで記述されています。

alt

3.$.ajax({でajaxで一か月分のカレンダーを取得します。
4.url: $(".home").attr("href") + "issues/calendar",取得するurlを定義しています。

使用したカレンダーはRedmine上段の「プロジェクト」->「カレンダー」をクリックしたカレンダーです。該当のURLを調べると私の環境では
http://localhost:82/redmine/issues/calendar
となっているので、redmine/の部分の情報を「ホーム」のhref情報から取得しました。

alt

5.async: false,ajaxを2度行うので、あえて非同期ではなく、同期処理をするように指定して、1度目のajaxが終わる前に2度目のajaxが開始しないようにしています。
6.$(data).find("table.cal").appendTo("#block-calendar")でajaxで取得したカレンダーを#block-calendarに挿入しています。

下図のようにカレンダーの部分はtableタグのcalクラスで定義をされているので、ajaxで取得したdataからfindで該当部分を特定してappendToで#block-calendarに挿入をしています。

alt

7. nextMonthURL=$(data).find("#query_form_with_buttons > p.contextual > a:nth-child(2)").attr('href')
nextMonthText=$(data).find("#query_form_with_buttons > p.contextual > a:nth-child(2)").text().replace(" »", "")この2行で来月のカレンダーのurlとそのタイトルを取得しています。
8.$.ajax({ 再度ajaxで来月のカレンダーを取得して項目6と同様に#block-calendarに挿入しています。

ブラウザ(Chrome)の「検証」->「Console」で上記のソースを実行すると意図した通りにカレンダーの乗っ取りに成功しました。

alt

Redmine versionは「4.0.5.stable」で試しました。他のバージョンのRedmineではカレンダーへのパスや要素の定義が異なっている可能性があり、正しく動作しない可能性があります。
その際には、項目4、7などをご自身の環境のRedmineに合わせてください。

Chrome拡張化

作ったJavaScriptを自動的にRedmineに適用させたいのですが、Redmineの管理者でかつ「redmine-view-customize」プラグインを使っているのであれば、「redmine-view-customize」プラグインに上記で作ったJavaScriptを適用させればカレンダーを乗っ取れます。しかし、残念なことに「redmine-view-customize」プラグインがRedmineにインストールされていない、もしくはRedmineの管理者ではないので、簡単に「redmine-view-customize」プラグインにJavaScriptを登録できない、もしくは、カレンダーを乗っ取ることをメンバーに知られたくない、などなど諸般の事情で権限のない一般ユーザーがRedmineのカスタマイズを行いたい場合、Chrome拡張を利用することにより、自作のJavaScriptをwebページに適用することが可能です。

これは完全にクライアントサイドで動作をするため、サーバー管理者に依頼をする必要がありません。しかし現実には社内に色々なセキュリティポリシーがあると思いますので、あくまで自己責任で試してください。

下記の情報を参考に作成したJavaScriptをChrome拡張でRedmineに適用しました。

Chrome拡張を簡単に作れるテンプレとライブラリ造ったので紹介
Chrome拡張の作り方 (超概要)

まずはChrome拡張のひな型が必要なので「Chrome拡張を簡単に作れるテンプレとライブラリ造ったので紹介」で紹介されている「chrome-ex-template」をダウンロードします。
ダウンロードしてファイルを解凍すると「content_script.js」ファイルがあるので、ここに上記のソースを記載します。設定ファイル「manifest.json」を開いて
"content_scripts": [{
"matches": ["*://localhost/*"],
の部分で自分が使っているRedmineのurlを登録します。私の場合はlocalhostにRedmineがあるため、matchesでlocalhostを指定しました。

後はChrome拡張を簡単に作れるテンプレとライブラリ造ったので紹介3. Chrome で動かしてみるで書かれているとおり、パッケージ化されていない拡張機能を読み込みます。

結果

下図のように1週間のカレンダーの代わりに、2か月分のカレンダーが表示されて無事にクリスマスとお正月の予定を確認することができます。
よく見ると、一瞬1週間のカレンダーが表示され、それをjsが強制的に2か月分のカレンダーに変更していることが分かります。
1週間のカレンダーが現実世界で、2か月分のカレンダーがjsによる妄想世界です。

alt

下図のようにレイアウトの位置の変更やカレンダーを閉じると2か月分のカレンダーが同時に移動したり、閉じることができます。

alt

Chrome拡張化により権限のない一般ユーザーでもRedmineのマイページのカレンダーを無事に乗っ取れました。

今回の記事で
1.RedmineでJavaScriptを実行する方法として、サーバーに頼らずに、クライアントサイドのスタンドアロンの自分のPCのブラウザでのみJavaScriptを実行するChrome拡張化のご紹介
2.ajaxを使ったマッシュアップ方法の例示
3.本来はrubyでの開発が必要な複雑なデーターの取得もajaxを複数回実行することで実現できる可能性の提示
ができたかと思います。
あなたの妄想とアイデア次第でruby(ruby on Rails)の開発を迂回して、JavaScript(jQuery)でプチUI改善が可能であるとのインサイトを提供できたのであれば幸いです。

終わりに

オリジナルのクリスマスは、東方の博士たちが不思議な星に導かれて救い主に出会った話です。また野の羊飼いたちは天に突然現れた天使たちの歌声を聞いて救い主に会いに行きました。4
私もRedmineに導かれて不思議な旅を続けていますが、またどこかで皆様とお会いできることを楽しみにしております。5
Redmineの新しいカレンダーが準備できましたので、少し早いですが、皆様に「メリークリスマス!」そして「2020年、東京オリンピックの年である来年も良いお年をお迎えください」

明日は「vzvu3k6k」さんによる「RedmineのtrunkのDockerイメージを毎日自動でビルドする話を書きます。」という記事が投稿されるとのことです。どんな記事なのか私も楽しみにしております。


  1. https://ja.wikipedia.org/wiki/アドベントカレンダー 

  2. 詳しくは https://redmine.tokyo/projects/shinared/wiki/第17回勉強会 で発表をした「The world of Redmine as seen from JavaScript (ankosoft yamasaki)」を参照ください。 

  3. ドラクエに出てくる呪文の一種で、敵をどこかに飛ばしてしまう効果があります。  

  4. クリスマスはキリストの誕生を記念する日です。 原典:マタイ2章ルカ2章 

  5. Redmineの開発、カスタマイズを手掛けるAnkosoftという会社にいますので、Redmineを使いやすく改善したいというニーズがありましたら、是非、ご連絡ください。