Image validation

圖片上傳可以運作了,不過目前並沒有針對上傳的檔案做任何限制,這可能會導致問題,例如使用者嘗試上傳檔案很大的檔案或是不合法的檔案類型。為了彌補這個缺陷,我們要在 server 和 client 端(例如瀏覽器)對圖片大小和格式增加驗證功能。

圖片格式的限制可以在 CarrierWave 的上傳程序中設定。我們要限制能使用的圖片副檔名(PNG、GIF、JPG、JPEG):

app/uploaders/picture_uploader.rb

class PictureUploader < CarrierWave::Uploader::Base
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Add a white list of extensions which are allowed to be uploaded.
  def extension_white_list
    %w(jpg jpeg gif png)
  end
end

圖片大小的限制在 Micropost model 裡面設定。和前面的 model 驗證不同,Rails 沒有為檔案大小提供現成的驗證方法。所以我們要自訂驗證方法,把這個方法命名為 picture_size,注意,調用自定義的驗證時,使用的是 validate,而不是 validates

app/models/micropost.rb

class Micropost < ActiveRecord::Base
  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

    # Validates the size of an uploaded picture.
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end
end

這個自定義的驗證會去調用指定的 symbol(:picture_size)對應的方法。在 picture_size 方法中,如果圖片大於 5MB,就向 errors 集合增加一個自定義的錯誤訊息。

除了以上兩個驗證之外,我們還要在 client 端檢查上傳的圖片。首先,我們在 file_field 方法中使用 accept 參數限制圖片的格式:

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

有效的格式使用 MIME types 指定,這些類型對應到 app/uploaders/picture_uploader.rb 裡限制的類型。

接著,我們要使用一些 JavaScript(更確切地說是 JQuery),如果使用者嘗試上傳太大的圖片,就彈出一個提示視窗(節省了上傳的時間,也減少 server 的負擔):

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

監視頁面中的 #micropost_picture id 元素,當這個元素的內容發生變化時,會執行這段程式碼;如果檔案太大,就會調用 alert 方法。

可以試著搜尋「javascript maximum file size」,會找到一些解法

把上面兩個檢查加進 micropost 表單中:

app/views/shared/_micropost_form.html.erb

<%= form_for(@micropost, html: { multipart: true }) 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, 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('Maximum file size is 5MB. Please choose a smaller file.');
    }
  });
</script>

你可以看到,如果嘗試上傳很大的檔案,根據上述程式碼無法真正的清空 file input 欄位,使用者可以關掉 alert 視窗然後繼續嘗試上傳檔案(打死他)。如果這裡是在教 JQuery 的話,也許我們會嘗試去把欄位清掉。但其實前端的程式碼無法真的阻止使用者做蠢事的決心,使用者也許會透過 web inspector 來編輯 JavaScript,或是直接發送 POST 請求(例如,curl)。為了阻止使用者上傳太大的檔案,還是有必要在 server 端加上驗證。