Railsチュートリアル 第13章 ユーザーのマイクロポスト - 画像の検証


# アップローダーにおけるバリデーションの実装

## 画像フォーマットのバリデーション

### 現状では、画像として無効なファイルをアップロードできてしまう

[Railsチュートリアル 第13章 ユーザーのマイクロポスト - 基本的な画像アップロード](https://qiita.com/rapidliner00/items/a6733ce24f77b3eef3df)で実装した画像のアップローダーでは、画像として無効なファイルをアップロードすることができてしまいます。

```
Started POST "/microposts" for 172.17.0.1 at 2020-01-08 09:50:59 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by MicropostsController#create as HTML
  Parameters: {...略, "micropost"=>{"content"=>"upload invalid format file", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007fd16c2513a0 @tempfile=#<Tempfile:/tmp/RackMultipart20200108-15302-1ati1vk.drawio>, @original_filename="Untitled Diagram.drawio", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"Untitled Diagram.drawio\"\r\nContent-Type: application/octet-stream\r\n">}, "commit"=>"Post"}
  User Load (6.8ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   (0.1ms)  begin transaction
  SQL (22.5ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES (?, ?, ?, ?, ?)  [["content", "upload invalid format file"], ["user_id", 1], ["created_at", "2020-01-08 09:51:00.104135"], ["updated_at", "2020-01-08 09:51:00.104135"], ["picture", "Untitled_Diagram.drawio"]]
   (17.5ms)  commit transaction
Redirected to http://localhost:8080/
Completed 302 Found in 194ms (ActiveRecord: 46.9ms)
```

「Untitled Diagram.drawio」というのは、明らかに画像ファイルではありません。しかし、それでも上記のようにマイクロポストの投稿は正常に完了してしまうのです。

このときのフィードの内容は以下のようになります。

<img width="1168" alt="スクリーンショット 2020-01-08 18.53.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/17b648ef-1267-ac3b-dca6-7f03468ae1fb.png">

「Untitled diagram」という文字になっている部分が、画像として無効なファイルを描画しようとした成れの果てです。

### CarrierWaveの機能により、特定の拡張子のファイルしかアップロードできないようにする

実は、CarrierWaveによって作られたPictureアップローダーには、「特定の拡張子のファイルしかアップロードできないようにする」という機能があります。`app/uploaders/picture_uploader.rb`のソースコード中にも、当該機能を使うためのコードがコメントアウトされた状態で存在します。

```ruby:app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
  # ...略

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  # def extension_whitelist
  #   %w(jpg jpeg gif png)
  # end

  # ...略
end
```

当該コードを以下のように変更すれば、指定した拡張子のファイルしかアップロードできないようになります。

```diff:app/uploaders/picture_uploader.rb
  class PictureUploader < CarrierWave::Uploader::Base
    ...略

    # Add a white list of extensions which are allowed to be uploaded.
    # For images you might use something like this:
-   # def extension_whitelist
-   #   %w(jpg jpeg gif png)
-   # end
+   def extension_whitelist
+     %w(jpg jpeg gif png)
+   end

    ...略
  end
```

上記の例の場合、.jpg/.jpeg/.gif/.png以上4つの拡張子をもつファイルのみがアップロード可能になります。

# モデルにおけるバリデーションの実装

## 画像のファイルサイズに対するバリデーション

### ファイルサイズに対するバリデーションは、独自に実装する必要がある

Railsチュートリアル本文には、「ファイルサイズに対するバリデーションはRails標準のバリデーション[^Rails標準のバリデーション]としては用意されておらず、独自に定義する必要がある」とあります。まずはファイルサイズに対するバリデーションの実体そのものを定義する必要があります。

[^Rails標準のバリデーション]: `presence`や`length`等が該当します。

今回のサンプルアプリケーションでは、画像はマイクロポストに関連付けされます。そのため、画像のファイルサイズに対するバリデーションもMicropostモデルに実装されるのが自然です。

### ファイルサイズに対するバリデーションの実装

メソッド名は`picture_size`とします。定義する箇所は、`app/models/micropost.rb`の`private`メソッド以降の部分となります。

```ruby:
def picture_size
  if picture.size > 5.megabytes
    errors.add(:picture, "should be less than 5MB")
  end
end
```

`picture_size`メソッドは、「アップロードされた画像のファイルサイズが5MBを超える場合、バリデーションを失敗させ、`errors`コレクションにカスタマイズされたエラーメッセージを追加する」という動作をします。

`errors`コレクションの使い方の詳細については、[バリデーションエラーに対応する - Active Record バリデーション - Railsガイド](https://railsguides.jp/active_record_validations.html#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AB%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B)を参照ください。

### ファイルサイズに対してバリデーションを行うようにする

上述`picutre_size`バリデーションを`app/models/micropost.rb`内で`validate`メソッドによって呼び出すようにすれば、ファイルサイズに対してバリデーションが行われるようになります。

（`validates`ではありません。`validate`です。私は一度ここを間違えました。）

```ruby:
validate :picture_size
```

独自に定義したバリデーションに対して`validate`メソッドで呼び出しを行う場合、`validate`メソッドの第1引数には、「当該バリデーション名のシンボル」を与えます。

## ファイルサイズのバリデーションの実装に必要となる、Micropostモデルに対する変更の全体像

上記を踏まえ、`app/models/micropost.rb`全体の変更内容は以下のようになります。

```diff:app/models/micropost.rb
  class Micropost < ApplicationRecord
    belongs_to :user
    default_scope -> { order(created_at: :desc) }
    mount_uploader :picture, PictureUploader
    validates :user_id, presence: true
    validates :content, presence: true, length: { maximum: 140 }
+   validate  :picture_size
+
+   private
+
+     # アップロードされた画像のサイズをバリデーションする
+     def picture_size
+       if picture.size > 5.megabytes
+         errors.add(:picture, "should be less than 5MB")
+       end
+     end
  end
```

# ビューに対するバリデーションの実装

## ファイルアップロード用のフォーム部品に、受理するファイル形式を追加する

ファイルアップロード用のフォーム部品で特定のファイル形式のみを受理するようにするためには、`file_field`タグに`accept`オプションを与えて使います。

```erb:
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
```

受理するファイル形式はMIMEタイプで記述します。カンマ区切りで複数記述することができます。

## 大きすぎるファイルサイズに対して警告を出すようにする

Javascript（正確にはjQuery）を用いて実装してみます。

```js:
$('#micropost_picture').bind('change', function() {
  var size_in_megabytes = this.files[0].size/1024/1024;
  if (size_in_megabytes > 5) {
    alert('Macimum file size is 5MB. Please choose a smaller file.');
  }
});
```

上記Javascriptコードの処理内容は以下のようになります。

* CSS idの`micropost_picture`を含む要素を見つけ出す
    * マイクロポスト投稿フォームの画像アップロード用パーツを指す
* 上記要素に変化があった場合、このJavascriptが動作する
* ファイルサイズが5MB以上となった場合、警告メッセージを出す

## マイクロポスト投稿フォームの実装の変更

ここまでに書かれたことを総合すると、`app/views/shared/_micropost_form.html.erb`の変更の全体像は以下のようになります。

```diff:app/views/shared/_micropost_form.html.erb
  <%= form_for(@micropost) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <div class="field">
      <%= f.text_area :content, placeholder: "Compose new micropost..." %>
    </div>
    <%= f.submit "Post", class: "btn btn-primary" %>
    <span class="picture">
-     <%= f.file_field :picture %>
+     <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
    </span>
  <% end %>
+
+ <script type="text/javascript">
+   $('#micropost_picture').bind('change', function() {
+     var size_in_megabytes = this.files[0].size/1024/1024;
+     if (size_in_megabytes > 5) {
+       alert('Macimum file size is 5MB. Please choose a smaller file.');
+     }
+   });
+ </script>
```

## ビューに対するバリデーションの限界

もっとも、警告を無視して大きすぎるアップロードを強行することは可能です。そのようなアップロードの強行を防ぐような実装も可能ですが、それでも限界はあります。例えば、以下のような操作をされた場合、ビュー側のバリデーションでは対策のしようがありません。

* Webブラウザのインスペクター機能でJavaScriptをいじる
* フォームを使わずに、`curl`などで直接`POST`リクエストを実行する

このように、ビューに対するバリデーションには限界があります。ビューに対するバリデーションの限界を超えるためには、モデルやアップローダー段階でのバリデーションが必要となるのです。