Redmine
Plugin
Rails5

Redmine 4.0(Rails 5.2)に用語集(Glossary)プラグインを対応させる

はじめに

Redmineは本日(2018-07-04)現在、バージョン3.4.6が最新で、これはRuby on Rails 4.2上で動作します。

一方、次のメジャーバージョンアップとなるRedmine 4.0に向けた開発が進められており、これはRuby on Rails 5.2上で動作することを目指しています。

Redmine 3.xで動作しているプラグインについて、Redmine 4.0(未だリリース前なので開発ブランチの最新を使用)で動かそうとすると、RedmineがフレームワークとしているRuby on Railsのメジャーバージョンアップによる非互換性のある変更に起因してエラーとなってしまうものが多数でる見込みです。

本記事は、Redmine 3.4で(表面的には)動作している用語集(Glossary)プラグインを、Redmine 4.0で動くように修正した作業の中で得た知見をまとめたものです。

非互換性とその対応

alias_method_chainの削除

alias_method_chainは、Rails(ActiveRecord)で提供される、既存のメソッドを差し替える、あるいは呼び出し前後に処理を挟むことができる機能です。プラグインではRedmine本体のコードを修正することなく処理を挟むことができるので広く使われています。これが、Ruby on Rails 5.1で削除されたため、alias_method_chainを使用しているプラグインは別な手段で処理を実現することが必要になりました。代替手段としてはModuleのprependを使用することが提案されています。

次は、用語集プラグインでalias_method_chainを使用している箇所をprependに置き換えたときの差分です。

alias_method_chainをrependに置き換え(Github)

prependに置き換えた方法を様式的に示すと次のとおりです。

  • 独自のモジュールPを定義
  • モジュールPの中に差し替え対象メソッドと同じシグニチャを定義し、alias_method_chain :xxx :yyy と指定していた場合、xxx_with_yyyの実装を丸ごと持ってくる
  • 実装の中にxxx_without_yyyの呼び出しがあれば、superに置換する
  • 差し替え対象メソッドを持つクラスまたはモジュールCとすると、C.prepend P を呼ぶ

マイグレーションクラスの継承元に素のMigrationは使えない

これまで、データベースのマイグレーション処理では、ActiveRecord::Migrationを継承していました。

001_create_terms.rb
class CreateTerms < ActiveRecord::Migration
  def self.up
  :

このコードがRails 5では、エラーとなります。

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateTerms < ActiveRecord::Migration[4.2]

マイグレーションコードを記述したときのRailsバージョンを指定する必要があります。Migration Versioningと呼ばれる仕組みで、Railsのバージョンアップにより生成されるスキーマが変わることがあるため(NOT NULL制約の有無、INTかBIGINTか、など)、意図しないスキーマが生成されないようバージョンを指定するもののようです。

次は、用語集プラグインでマイグレーションのバージョン指定を追加したときの差分です。

Migrationにバージョン付け(Github)

バージョンは、Redmine 3.xが動作するRuby on Rails 4.2を指定しました。

非推奨だったbefore_filterの削除

before_filter で指定したメソッドは、コントローラーのアクションが実行される際、そのアクション実行の直前に実行されます。例えば、各アクションは認証されたユーザーに限り実行可能であるとする場合、各アクションのメソッドの中で認証チェックするのではなく、before_filterで認証チェックするメソッドを指定しておくといった使い方です。

アクションの前に実行するので、よりふさわしい命名であるbefore_actionに変更され、Rails 5.1でbefore_filterは削除されています。

置き換えは単純にbefore_filterをbefore_actionに修正するだけです。

-  before_filter :find_project, :authorize                                                      
-  before_filter :find_term, :only => [:show, :edit, :destroy]                                  
-  before_filter :retrieve_glossary_style, :only => [:index, :show, :show_all, :import_csv_exec]
+  before_action :find_project, :authorize                                                      
+  before_action :find_term, :only => [:show, :edit, :destroy]                                  
+  before_action :retrieve_glossary_style, :only => [:index, :show, :show_all, :import_csv_exec]

次は、用語集プラグインでbefore_filterをbefore_actionに置き換えたときの差分です。

before_filterをbefore_actionに置き換え(Github)

コントローラーのunloadable

コントローラークラスにおまじないのように記載されているunloadableですが、Redmine 3.xでは不要なようです。Redmine本体では次のチケットで削除されています。
http://www.redmine.org/issues/27963

そこで、今回バッサリ削除します。

開発モードで実行する際、unloadableが記載されていると、コントローラーのソースコードを変更してクライアントから再度アクセスすると「Unable to autoload constant XxxController ...」エラーとなってしまいます。

次は、用語集プラグインのコントローラからunloadableを削除したときの差分です。

unloadableをコントローラーから削除(Github)

モデルのunloadable

モデルにもunloadableの記載があったので削除します。

unloadableをモデルから削除(Github)

attr_accessibleの削除

モデルクラスにattr_accessibleで変更可能な属性を指定することで、書き換え可能な属性とするのに使います。

このattr_accessibleは、Rails 4の時点で非推奨となっており、Rails 5では削除されています。

置き換えは、「ストロングパラメーター」という仕組みを使います。Railsでは、HTTPリクエストとしてクライアントから送られてくるパラメーターをそのままモデルに渡してモデルの作成・更新を行うマスアサインメントという機構があります。これはコーディングが楽ですが、不用意に属性が更新され脆弱性をもたらすことになりかねません。ストロングパラメーターは、変更可能な属性を明示的に指定し、その属性以外がパラメーターに含まれていてもモデルには反映しないというものです。

GlossaryStylesモデルのattr_accessible削除とストロングパラメーター導入

まず、GrossaryStyleクラスからattr_accessibleを削除します。

-  attr_accessible :groupby

次に、GlossaryStylesControllerクラスにストロングパラメーターを実装します。

-        @glossary_style = GlossaryStyle.new(params[:glossary_style])
+        @glossary_style = GlossaryStyle.new(glossary_style_params)
  :
+  private
+
+  def glossary_style_params
+    params.require(:glossary_style).permit(:groupby)
+  end

同様に、Termクラスからattr_accessibleを削除し、TermsControllerにストロングパラメーターを実装します。

-  attr_accessible :project_id, :category_id, :author, :name, :name_en, :datatype, :codename, :description,
-                  :rubi, :abbr_whole
-    @term = Term.new(params[:term])
+    @term = Term.new(term_params)
     :
+  def term_params                                                                             
+    params.require(:term).permit(                                                            
+      :project_id, :category_id, :author, :name, :name_en, :datatype, :codename, :description,
+      :ruby, :abbr_whole                                                                      
+    )                                                                                         
+  end                                                                                         

さらに、TermCategoryクラスからattr_accessibleを削除し、TermCategoriesControllerクラスにストロングパラメーターを実装します。

次は、用語集プラグインでattr_accessibleを使っていた箇所を、ストロングパラメーターに置き換えたときの差分です。

削除されたattr_accessibleに替わるストロングパラメーターの実装(Github)

acts_as_listの削除

Redmine 3.xでは、/lib/plugins/acts_as_list が存在していました。しかし、Redmine 4.xではこのacts_as_listが削除されています。

このacts_as_listはgemで存在しているので、別途インストールすることとします。用語集プラグインのディレクトリにGemfileを作成し、acts_as_listをgemでインストールします。

gem 'acts_as_list'

Redmineディレクトリでbundleを実行します。

パラメーターはハッシュを継承しなくなった

HTTPリクエストのパラメーターはRails 5以降はハッシュを継承しなくなったので、ハッシュを引数に取るメソッドにパラメーターを渡していたコードがエラーとなってしまいます。

unable to convert unpermitted parameters to hash

使用箇所の例は次となります。

  • app/views/glossary/index.html.erb
    <%= f.link_to 'CSV', :url => params %>

明示的にハッシュに変換し、かつpermitを有効にします。

-    <%= f.link_to 'CSV', :url => params %>             
+    <%= f.link_to 'CSV', :url => params.permit!.to_h %>

次は、用語集プラグインでリクエストパラメーターをハッシュに変換、permitを与えたときの差分です。

リクエストパラメーターをpermitして明示的にハッシュに変換(Github)

画像ファイルの削除

Redmine 3.xまでは、Redmineインストールディレクトリ/public/imagesディレクトリに次の画像ファイルが提供されていました。

  • 1downarrow.png
  • 1uparrow.png
  • 2downarrow.png
  • 2uparrow.png

しかし、Redmine最新ブランチ(Redmine 4.0向け開発)のpublic/imagesディレクトリにはこれらのファイルが含まれていません。

そこで、プラグインディレクトリのassets/imagesディレクトリにこの4つのファイルを置くことにします。

また、プラグインディレクトリにある画像ファイルを使用する際はプラグイン名の指定が必要となるので、リンクの画像ファイル指定にプラグイン名の指定を追加します。

-                <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
-                <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
-                <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
-                <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
+                <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
+                <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
+                <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
+                <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest), :plugin => 'redmine_glossary'), {:controller => 'term_categories', :action => 'change_order', :project_id => @project, :id => category, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>

次は、用語集プラグインに画像ファイルを追加しリンクで画像の指定にプラグイン名を追加したときの差分です。

Redmine本体から削除された画像ファイルをプラグインのassetsに追加し、画像リンクの指定にプラグイン名を追加(Github)

非推奨警告で今後削除されるもの

ルーティングに:actionは使わない

サーバーを起動する際、次の警告メッセージが表示されました。

DEPRECATION WARNING: Using a dynamic :action segment in a route is deprecated and will be removed in Rails 6.0. (called from instance_eval at /work/redmine/config/routes.rb:354)

ルーティング設定に:actionを使用するのは非推奨でRails 6.0で削除されるとあります。

Allowing :controller and :action values to be specified via the path in config/routes.rb has been an underlying cause of a number of issues in Rails that have resulted in security releases. In light of this it's
better that controllers and actions are explicitly whitelisted rather than trying to blacklist or sanitize 'bad' values.

簡単にまとめると

:actionの使用はセキュリティ上よろしくない問題をもたらすので、ルーティング設定ではアクションを明示的に指定すべき。

Redmine本体のroutes.rbの354行目は次の通りプラグインディレクトリのroutes.rbを評価しているコードなので、プラグイン側のroutes.rbの記述が警告対象となっています。

        instance_eval File.read(file) 

プラグインのroutes.rbから:actionを使用している行を抜粋します。

    match 'glossary_styles/:action', :controller => :glossary_styles, :via => [ :get, :post, :put, :patch ]
    match 'projects/:project_id/glossary/:id/:action', :controller => :glossary, :id => /\d+/, :via => :all
    match 'projects/:project_id/glossary/:action', :controller => :glossary, :via => :all
    match 'projects/:project_id/term_categories/:action', :controller => :term_categories, :via => :all
    match 'projects/:project_id/term_categories/:id/:action', :controller => :term_categories, :id => /\d+/, :via => :all

glossary_styles/:action の警告解消

まず最初のglossary_style/:action を検討します。ルーティング設定を確認すると次の通りです。

$ bundle exec rails routes
Prefix  Verb                URI Pattern                           Controller#Action
        GET|POST|PUT|PATCH  /glossary_styles/:action(.:format)    glossary_styles#:action

先に述べたように何でもアクションが呼ばれるのはセキュリティ上問題となるので、呼び出し可能なアクションを明示的に指定するように修正します。

そこで、まずglossary_stylesコントローラーに定義されるアクションを確認します。

  • search
  • edit

次に、それぞれのアクションがどのHTTPメソッドで呼ばれるかを調べます。searchは、GETで呼ばれますが、editはPOSTとPATCHの2つで呼ばれます。

リソース指向(RESTful)では、リソースの変更(edit)では、まずeditアクションをGETで呼び、updateアクションをPATCHで呼びます。しかし、元の用語集プラグインはeditアクションをPOSTおよびPATCHで呼ぶので、ルーティング設定でリソース指向のresource指定を使うのが難しく、別の記述をしています。

  get    '/glossary_styles/search', to: 'glossary_styles#search'
  post   '/glossary_styles/edit', to: 'glossary_styles#edit'
  patch  '/glossary_styles/edit', to: 'glossary_styles#edit'

このようにルーティング設定を書き換えた時の差分です。

match指定をやめて、HTTPコマンド指定に置き換え(Github)

感想のようなもの

RedmineはRuby on Railsフレームワーク上で動作するアプリケーションとして作られています。Remine 3.xはRuby on Rails 4.2上で動きますが、来たるRedmine 4.0はRuby on Rails 5.2上で動きます。プラグインは、これまで動作してきたRuby on Rails 4.2から、新たにRuby on Rails 5.2で動くように対応が必要となります。

Railsがメジャーバージョンアップすると、かなりドラスティックに変化があります。

最初は、今年の4月に用語集プラグインをRedmine 4.0開発ブランチ(trunk/master)に入れて、出たエラーをつぶしていこうとしましたが、少なくはない変更、手に負えない変更があり挫折しました。

そこで、用語集プラグインをゼロから実装してみることでプラグインの作り方を学びました(5〜6月)。そのときの作業を次のWikiページにステップバイステップで書いています。

Redmine Glossaryプラグイン再構築

それから再度用語集プラグインのRedmine 4.0対応を実施しています。コードは次のGithubのブランチ上にあります。

https://github.com/torutk/redmine_glossary/tree/support-redmine4