基礎Ruby on Rails Chapter11 ストロング・パラメータ／エラーページのカスタマイズ

前 [基礎Ruby on Rails Chapter11 モデル間の関連付け／ブログ機能の追加（コントローラ／ビュー編）](https://qiita.com/tseno/items/4c8fca375a8e20fa34a1)
次 [基礎Ruby on Rails Chapter12 アセット・パイプライン](https://qiita.com/tseno/items/a917be7a6d3f4dd9a80d)

## ストロング・パラメータとは

### マスアサインメント脆弱性

- 例えば、編集ページを開いて、HTMLソースの`account[member]`を`account[administrator]`に書き換えて、更新ボタンを押せば管理者に変更できてしまう。これは重大なセキュリティホールで、**マスアサインメント脆弱性**と呼ばれる。

### ストロング・パラメータの有効化

- **ストロング・パラメータ**は、マスアサインメント脆弱性への対策として、Rails4.0から導入された仕組み。
- 現在は学習のためにストロング・パラメータを無効化しているので、有効化する。

```rb:config/application.rb（一部）
    # false=ストロング・パラメータの有効化
    config.action_controller.permit_all_parameters = false
```

ストロング・パラメータの有効にした状態で更新するとエラーが出る。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/ad8c2c3a-2aa7-6423-cc13-bff5cbee5225.png)

### ストロング・パラメータの使い方

- assign_attributesの所に、`.permit(:member, :name,・・・`を追加することにより、**余計な項目を保存しないようにする**。

```rb
  def update
    @member = current_member
    @member.assign_attributes(params[:account].permit(:member, :name, :full_name, :sex, :birthday, :email))
    if @member.save
      redirect_to :account, notice: "アカウント情報を更新しました。"
    else
      render "edit"
    end
  end
```

### プライベートメソッドを活用する

- さらに上記のassign_attributesの引数部分をプライベートメソッドにすると、ごちゃごちゃしない。

```rb
  #プライベートメソッド
  private def account_params
    params[:account].permit(:member, :name, :full_name, :sex, :birthday, :email)
  end

  #以下のように呼び出しができる。
  @member.assign_attributes(account_params)
```

### requireメソッド

- 上記で、`:account`キーが含まれていない場合、NoMethodErrorが発生してしまう。
- params.require(:account).permitとすれば、`:account`がない時は、ParameterMissingとなる。
- エラーの種類としては、NoMethodErrorはバグなので、ParameterMissingのほうが望ましい。よって、requireを使うべきである。

```rb
  private def account_params
    params.require(:account).permit(:member, :name, :full_name, :sex, :birthday, :email)
  end
```

## コントローラの修正

### マイアカウントページのストロング・パラメータ対応

- プライベートメソッド`account_params`を作成し、updateのパラメータに入れる。

```rb:app/controllers/accounts_controller.rb（一部）
  def update
    @member = current_member
    @member.assign_attributes(account_params)
    if @member.save
      redirect_to :account, notice: "アカウント情報を更新しました。"
    else
      render "edit"
    end
  end

  private def account_params
    params.require(:account).permit(
        :number,
        :name,
        :full_name,
        :sex,
        :birthday,
        :email
    )
  end
```

### 会員情報のストロング・パラメータ対応

- プライベートメソッド`member_params`を作成し、createとupdateのパラメータに入れる。

```rb:app/controllers/members_controller.rb（一部）
  def create
    @member = Member.new(member_params)
    if @member.save
      # 保存が成功したらshowにリダイレクトする。フラッシュ値を設定する。
      redirect_to @member, notice: "会員を登録しました。"
    else
      # エラー発生時はnewに戻る
      render "new"
    end
  end

  def update
    @member = Member.find(params[:id])
    @member.assign_attributes(member_params)
    if @member.save
      # 保存が成功したらshowにリダイレクトする。フラッシュ値を設定する。
      redirect_to @member, notice: "会員を更新しました。"
    else
      # エラー発生時はeditに戻る
      render "edit"
    end
  end

  private def member_params
    attrs = [
        :number,
        :name,
        full_name,
        :sex,
        :bitthday,
        :email,
        :administrator
    ]
    # createの時は、:passwordを追加する
    attrs << :password if params[:action] == "create"

    params.require(:member).permit(attrs)
  end
```

### パスワード変更機能のストロング・パラメータ対応

- プライベートメソッド`account_params`を作成し、updateのパラメータに入れる。
- また、`current_password = params[:account][:current_password]`も、`current_password = account_params[:current_password]`にする。

```rb:app/controllers/passwords_controller.rb（一部）
  def update
    @member = current_member
    current_password = account_params[:current_password]

    if current_password.present?
      if @member.authenticate(current_password)
        @member.assign_attributes(account_params)
        if @member.save
          redirect_to :account, notice: "パスワードを変更しました。"
        else
          render "edit"
        end
      else
        @member.errors.add(:current_password, :wrong)
        render "edit"
      end
    else
      @member.errors.add(:current_password, :empty)
      render "edit"
    end
  end

  private def account_params
    params.require(:account).permit(
        :current_password,
        :password,
        :password_confirmation
    )
  end
```

### ニュース記事管理機能のストロング・パラメータ対応

- プライベートメソッド`article_params`を作成し、createとupdateのパラメータに入れる。

```rb:app/controllers/articles_controller.rb（一部）
  def create
    @article = Article.new(article_params)
    if @article.save
      redirect_to @article, notice: "ニュース記事を登録しました。"
    else
      render "new"
    end
  end

  def update
    @article = Article.find(article_params)
    @article.assign_attributes(params[:article])
    if @article.save
      redirect_to @article, notice: "ニュース記事を更新しました。"
    else
      render "edit"
    end
  end

  private def article_params
    params.require(:article).permit(
        :title,
        :body,
        :released_at,
        :no_expiration,
        :expired_at,
        :member_only
    )
  end
```

### ブログ記事管理機能のストロング・パラメータ対応

- プライベートメソッド`entry_params`を作成し、createとupdateのパラメータに入れる。

```rb:app/controllers/entries_controller.rb（一部）
  def create
    @entry = Entry.new(entry_params)
    @entry.author = current_member
    if @entry.save
      redirect_to @entry, notice: "記事を作成しました。"
    else
      render "new"
    end
  end

  def update
    @entry = current_member.entries.find(params[:id])
    @entry.assign_attributes(entry_params)
    if @entry.save
      redirect_to @entry, notice: "記事を更新しました。"
    else
      render "edit"
    end
  end

  private def entry_params
    params.require(:entry).permit(
        :member_id,
        :title,
        :body,
        :posted_at,
        :status
    )
  end
```

- いくつかの画面を更新したところ、エラーが出ることなく保存できることを確認した。

# エラーページのカスタマイズ

## Railsの例外を処理する

- Railsアプリケーションの起動中に例外が発生したときは、Railsが自動的に例外を捕捉してエラー表示を行う。
- エラー表示をカスタマイズしたい場合は、コントローラの中でクラスメソッド`rescue_from`メソッドを使用して、自分で例外を処理する。
- rescue_fromの第1引数は、例外のクラス名。withオプションには例外が発生した時に実行するメソッドの名前を指定する。
- 上の3つは、本番モードの時だけ処理している。
- 親子関係にある例外クラスは、親のほうを先に指定する。StandardErrorは親（の親）。

```rb:app/controllers/application_controller.rb（一部）
  if Rails.env.production? || ENV["RESCUE_EXCEPTIONS"]
    rescue_from StandardError, with: :rescue_internal_server_error
    rescue_from ActiveRecord::RecordNotFound, with: :rescue_not_found
    rescue_from ActionController::ParameterMissing, with: :rescue_bad_request
  end

  rescue_from LoginRequired, with: :rescue_login_required
  rescue_from Forbidden, with: :rescue_forbidden

#(省略)

  private def rescue_bad_request(exception)
    render "error/bad_request", status: 400, layout: "error", formats: [:html]
  end

  private def rescue_login_required(exception)
    render "error/login_required", status: 403, layout: "error", formats: [:html]
  end

  private def rescue_forbidden(exception)
    render "error/forbidden", status: 403, layout: "error", formats: [:html]
  end

  private def rescue_not_found(exception)
    render "error/not_found", status: 404, layout: "error", formats: [:html]
  end

  private def rescue_internal_server_error(exception)
    render "error/internal_server_error", status: 500, layout: "error", formats: [:html]
  end
```

## エラー用テンプレート

### テンプレートの作成

```erb:app/views/layouts/error.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= page_title %></title>
    <%= stylesheet_link_tag "application", media: "all" %>
  </head>
  <body>
    <div id="container">
      <header>
        <%= image_tag("logo.gif", size: "272x48", alt: "Morning Glory") %>
        <nav class="menubar">
          <a href="/">TOP</a>
        </nav>
      </header>
      <main>
        <%= yield %>
      </main>
      <footer>
        <%= render "shared/footer" %>
      </footer>
    </div>
  </body>
</html>
```

```erb:app/views/errors/bad_request.html.erb
<% @page_title = "Bad Request" %>
<h1>400 Bad Request</h1>
<p>リクエストの形式が正しくありません。</p>
```

```erb:app/views/errors/login_required.html.erb
<% @page_title = "ログインが必要です" %>
<h1><%= @page_title %></h1>

<%= render "shared/login_form" %>
```

```erb:app/views/errors/forbidden.html.erb
<% @page_title = "Forbidden" %>
<h1>403 Forbidden</h1>
<p>このページにはアクセスできません。</p>
```

```erb:app/views/errors/not_found.html.erb
<% @page_title = "Not Found" %>
<h1>404 Not Found</h1>
<p>ページが見つかりません。</p>
```

```erb:app/views/errors/internal_server_error.html.erb
<% @page_title = "Internal Server Error" %>
<h1>500 Internal Server Error</h1>
<p>システムエラーのためページが表示できません。</p>
```

### エラーページの確認

- config/environmentsディレクトリのdevelop.rbに、`consider_all_requests_local`という設定項目がある
    - true・・・エラーをブラウザに表示をする。
    - false・・・ブラウザには詳細情報が表示されない。
- falseに設定する。

```rb:config/environments/development.rb（一部）
  # Show full error reports.
  config.consider_all_requests_local = false
```

- 上のほうで、以下のような部分があり、環境変数`ENV["RESCUE_EXCEPTIONS"]`がtrueであれば、if内のエラー表示がされるようになる。

```rb:app/controllers/application_controller.rb（一部）
  if Rails.env.production? || ENV["RESCUE_EXCEPTIONS"]
    rescue_from StandardError, with: :rescue_internal_server_error
    rescue_from ActiveRecord::RecordNotFound, with: :rescue_not_found
    rescue_from ActionController::ParameterMissing, with: :rescue_bad_request
  end
```

- 以下のようにしてサーバーを起動すると、環境変数`ENV["RESCUE_EXCEPTIONS"]=1`がセットされて、本番モードと同様にすべての例外が捕捉されるようになる。

```sh
$ RESCUE_EXCEPTIONS=1 bin/rails s
```

- `http://localhost:3000/members`を開くと、ログインフォームが表示される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/79ef3718-4d54-139d-e216-6a7d28131685.png)

- `http://localhost:3000/articles/100`を開くと、404エラーが表示される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/fcdf7701-c7fa-40c1-cbee-472f75a9935f.png)

- 他のエラーは出すのが大変なので、top_controllerに、`bad_request`、`forbidden`、`internal_server_error`メソッドを加えて、呼び出す。

```rb:app/controllers/top_controller.rb（一部）
  def bad_request
    raise ActionController::ParameterMissing, ""
  end

  def forbidden
    raise Forbidden, ""
  end

  def internal_server_error
    raise
  end
```

- ルーティングにも、`bad_request`、`forbidden`、`internal_server_error`を追加する。

```rb:config/routes.rb（一部）
Rails.application.routes.draw do
  root "top#index"
  get "about" => "top#about", as: "about"
  get "bad_request" => "top#bad_request"
  get "forbidden" => "top#forbidden"
  get "internal_server_error" => "top#internal_server_error"
```

- それぞれ、`http://localhost:3000/bad_request`、`http://localhost:3000/forbidden`、`http://localhost:3000/internal_server_error`を呼び出す。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/528b5d1d-205c-c23d-b0d0-511cc0470c4d.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/7f57b4bd-d411-1e86-1172-90b7b23d138a.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/11e95b0f-ddff-fecf-f1d0-9b6ed02dc1b9.png)

## ルーティングエラーの処理

- ルーティングエラーは、ApplicationControllerを書き換えただけでは処理できない。
- Action Dispatchがルーティングエラーを処理する。
- Action Controllerに伝わらない例外を処理する方法として、ErrorControllerのshowアクションを呼ぶように指定する。

```rb:config/application.rb（一部）
    config.exceptions_app = ->(env) do
      ErrorsController.action(:show).call(env)
    end
```

- 新規ファイル、errors_controller.rbを新規作成する。

```rb:app/controllers/errors_controller.rb
class ErrorsController < ActionController::Base
  layout "error"

  def show
    case request.env["action_dispatch.exception"]
    when ActionController::RoutingError
      render "not_found", status: 404, formats: [:html]
    else
      render "internal_server_error", status: 500, formats: [:html]
    end
  end
end

```

- `http://localhost:3000/xxxxxxxxxxxxx`のような変なアドレスにしても適切に処理される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/627421de-cea1-a94a-ced7-a38e0ff482eb.png)

- `config/environments/development.rb`の`config.consider_all_requests_local = true`に元に戻して、終了。


#### まとめ
- ストロング・パラメータを設定することにより、画面に表示されていない項目の更新を防ぐ。
- 様々なエラーを画面に適切に出力することにより、実用的なWebサイトを構築できる。

参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)