アップローダーにおけるバリデーションの実装
画像フォーマットのバリデーション
現状では、画像として無効なファイルをアップロードできてしまう
Railsチュートリアル 第13章 ユーザーのマイクロポスト - 基本的な画像アップロードで実装した画像のアップローダーでは、画像として無効なファイルをアップロードすることができてしまいます。
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」というのは、明らかに画像ファイルではありません。しかし、それでも上記のようにマイクロポストの投稿は正常に完了してしまうのです。
このときのフィードの内容は以下のようになります。

「Untitled diagram」という文字になっている部分が、画像として無効なファイルを描画しようとした成れの果てです。
CarrierWaveの機能により、特定の拡張子のファイルしかアップロードできないようにする
実は、CarrierWaveによって作られたPictureアップローダーには、「特定の拡張子のファイルしかアップロードできないようにする」という機能があります。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
当該コードを以下のように変更すれば、指定した拡張子のファイルしかアップロードできないようになります。
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標準のバリデーション1としては用意されておらず、独自に定義する必要がある」とあります。まずはファイルサイズに対するバリデーションの実体そのものを定義する必要があります。
今回のサンプルアプリケーションでは、画像はマイクロポストに関連付けされます。そのため、画像のファイルサイズに対するバリデーションもMicropostモデルに実装されるのが自然です。
ファイルサイズに対するバリデーションの実装
メソッド名はpicture_size
とします。定義する箇所は、app/models/micropost.rb
のprivate
メソッド以降の部分となります。
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ガイドを参照ください。
ファイルサイズに対してバリデーションを行うようにする
上述picutre_size
バリデーションをapp/models/micropost.rb
内でvalidate
メソッドによって呼び出すようにすれば、ファイルサイズに対してバリデーションが行われるようになります。
(validates
ではありません。validate
です。私は一度ここを間違えました。)
validate :picture_size
独自に定義したバリデーションに対してvalidate
メソッドで呼び出しを行う場合、validate
メソッドの第1引数には、「当該バリデーション名のシンボル」を与えます。
ファイルサイズのバリデーションの実装に必要となる、Micropostモデルに対する変更の全体像
上記を踏まえ、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
オプションを与えて使います。
<%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
受理するファイル形式はMIMEタイプで記述します。カンマ区切りで複数記述することができます。
大きすぎるファイルサイズに対して警告を出すようにする
Javascript(正確にはjQuery)を用いて実装してみます。
$('#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
の変更の全体像は以下のようになります。
<%= 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
リクエストを実行する
このように、ビューに対するバリデーションには限界があります。ビューに対するバリデーションの限界を超えるためには、モデルやアップローダー段階でのバリデーションが必要となるのです。
-
presence
やlength
等が該当します。 ↩