From ea600ff0959e03b7311eaa347caa2fc2ac441120 Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Tue, 18 Sep 2018 22:36:16 -0400 Subject: [PATCH 1/6] Now validate resources mime_types * Accept only file types we allow * Add ability to change de default configuration. * Run and fix offenses with Rubocop --- resources/app/models/refinery/resource.rb | 26 ++-- resources/config/locales/en.yml | 1 + resources/config/locales/fr.yml | 1 + .../initializers/refinery/resources.rb.erb | 3 + .../lib/refinery/resources/configuration.rb | 10 +- resources/spec/factories/resource.rb | 6 +- .../features/refinery/admin/resources_spec.rb | 140 ++++++++++-------- .../spec/fixtures/cape-town-tide-table.pdf | Bin 0 -> 22024 bytes .../spec/fixtures/cape-town-tide-table2.pdf | Bin 0 -> 22024 bytes .../spec/fixtures/refinery_is_awesome.txt | 1 - .../spec/fixtures/refinery_is_awesome2.txt | 1 - .../spec/models/refinery/resource_spec.rb | 131 +++++++++------- 12 files changed, 187 insertions(+), 133 deletions(-) create mode 100644 resources/spec/fixtures/cape-town-tide-table.pdf create mode 100644 resources/spec/fixtures/cape-town-tide-table2.pdf delete mode 100644 resources/spec/fixtures/refinery_is_awesome.txt delete mode 100644 resources/spec/fixtures/refinery_is_awesome2.txt diff --git a/resources/app/models/refinery/resource.rb b/resources/app/models/refinery/resource.rb index 05defeaf8e..5882d6e122 100644 --- a/resources/app/models/refinery/resource.rb +++ b/resources/app/models/refinery/resource.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'dragonfly' module Refinery @@ -7,14 +9,18 @@ class Resource < Refinery::Core::BaseModel extend Mobility translates :resource_title - dragonfly_accessor :file, :app => :refinery_resources + dragonfly_accessor :file, app: :refinery_resources - validates :file, :presence => true + validates :file, presence: true validates_with FileSizeValidator + validates_property :mime_type, + of: :file, + in: ::Refinery::Resources.whitelisted_mime_types, + message: :incorrect_format - delegate :ext, :size, :mime_type, :url, :to => :file + delegate :ext, :size, :mime_type, :url, to: :file - before_destroy :cached_mime_type, :prepend => true + before_destroy :cached_mime_type, prepend: true def cached_mime_type @cached_mime_type ||= mime_type @@ -22,18 +28,18 @@ def cached_mime_type # used for searching def type_of_content - cached_mime_type.split("/").join(" ") + cached_mime_type.split('/').join(' ') end # Returns a titleized version of the filename # my_file.pdf returns My File def title - resource_title.presence || CGI::unescape(file_name.to_s).gsub(/\.\w+$/, '').titleize + resource_title.presence || CGI.unescape(file_name.to_s).gsub(/\.\w+$/, '').titleize end def update_index - return if self.aai_config.disable_auto_indexing - copy = self.dup.tap{ |r| r.file_uid = r.file_uid_was} + return if aai_config.disable_auto_indexing + copy = dup.tap { |r| r.file_uid = r.file_uid_was } self.class.index_remove(copy) self.class.index_add(self) end @@ -47,9 +53,9 @@ def per_page(dialog = false) def create_resources(params) resources = [] - if params.present? and params[:file].is_a?(Array) + if params.present? && params[:file].is_a?(Array) params[:file].each do |resource| - resources << create({:file => resource}.merge(params.except(:file).to_h)) + resources << create({ file: resource }.merge(params.except(:file).to_h)) end else resources << create(params) diff --git a/resources/config/locales/en.yml b/resources/config/locales/en.yml index b6689991f1..73704f24e2 100644 --- a/resources/config/locales/en.yml +++ b/resources/config/locales/en.yml @@ -36,4 +36,5 @@ en: models: refinery/resource: blank: You must specify file for upload + incorrect_format: Your file must be a PDF too_big: File should be smaller than %{size} bytes in size diff --git a/resources/config/locales/fr.yml b/resources/config/locales/fr.yml index 3524b3ae52..c89accc0e3 100644 --- a/resources/config/locales/fr.yml +++ b/resources/config/locales/fr.yml @@ -36,4 +36,5 @@ fr: models: refinery/resource: blank: Vous devez spécifier un fichier à télécharger + incorrect_format: Votre fichier doit être un PDF too_big: Le poids maximal des fichiers est de %{size} megaoctets diff --git a/resources/lib/generators/refinery/resources/templates/config/initializers/refinery/resources.rb.erb b/resources/lib/generators/refinery/resources/templates/config/initializers/refinery/resources.rb.erb index 13e5ab74c8..ec526170d6 100644 --- a/resources/lib/generators/refinery/resources/templates/config/initializers/refinery/resources.rb.erb +++ b/resources/lib/generators/refinery/resources/templates/config/initializers/refinery/resources.rb.erb @@ -9,6 +9,9 @@ Refinery::Resources.configure do |config| # Configure how many resources per page should be displayed in the list of resources in the admin area # config.pages_per_admin_index = <%= Refinery::Resources.pages_per_admin_index.inspect %> + # Configure white-listed mime types for validation + # config.whitelisted_mime_types = <%= Refinery::Resources.whitelisted_mime_types.inspect %> + # Configure Dragonfly. # Refer to config/initializers/refinery/dragonfly.rb for the full list of dragonfly configurations which can be used. # This includes all dragonfly config for Dragonfly v 1.1.1 diff --git a/resources/lib/refinery/resources/configuration.rb b/resources/lib/refinery/resources/configuration.rb index eb1c15390d..71b9a8fbf7 100644 --- a/resources/lib/refinery/resources/configuration.rb +++ b/resources/lib/refinery/resources/configuration.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Refinery module Resources - extend Refinery::Dragonfly::ExtensionConfiguration include ActiveSupport::Configurable - config_accessor :max_file_size, :pages_per_dialog, :pages_per_admin_index, :content_disposition + config_accessor :max_file_size, :pages_per_dialog, :pages_per_admin_index, + :content_disposition, :whitelisted_mime_types self.content_disposition = :attachment self.max_file_size = 52_428_800 @@ -13,6 +15,6 @@ module Resources self.dragonfly_name = :refinery_resources + self.whitelisted_mime_types = %w[application/pdf] end -end - +end \ No newline at end of file diff --git a/resources/spec/factories/resource.rb b/resources/spec/factories/resource.rb index 24c0421533..5518ed66e4 100644 --- a/resources/spec/factories/resource.rb +++ b/resources/spec/factories/resource.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + FactoryBot.define do - factory :resource, :class => Refinery::Resource do - file Refinery.roots('refinery/resources').join("spec/fixtures/refinery_is_awesome.txt") + factory :resource, class: Refinery::Resource do + file Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') end end diff --git a/resources/spec/features/refinery/admin/resources_spec.rb b/resources/spec/features/refinery/admin/resources_spec.rb index 1e31548163..b13d7fa065 100644 --- a/resources/spec/features/refinery/admin/resources_spec.rb +++ b/resources/spec/features/refinery/admin/resources_spec.rb @@ -1,116 +1,135 @@ -# Encoding: UTF-8 -require "spec_helper" +# frozen_string_literal: true + +require 'spec_helper' module Refinery module Admin - describe "Resources", :type => :feature do + describe 'Resources', type: :feature do refinery_login - context "when no files" do - it "invites to upload file" do + context 'when no files' do + it 'invites to upload file' do visit refinery.admin_resources_path - expect(page).to have_content(%q{There are no files yet. Click "Upload new file" to add your first file.}) + expect(page).to have_content('There are no files yet. Click "Upload new file" to add your first file.') end end - it "shows upload file link" do + it 'shows upload file link' do visit refinery.admin_resources_path - expect(page).to have_content("Upload new file") - expect(page).to have_selector("a[href*='/refinery/resources/new']") + expect(page).to have_content('Upload new file') + expect(page).to have_selector('a[href*="/refinery/resources/new"]') end - context "new/create" do - it "uploads file", :js => true do - visit refinery.admin_resources_path - find('a', text: 'Upload new file').click + context 'new/create' do + let(:uploading_a_file) do + lambda do + visit refinery.admin_resources_path + find('a', text: 'Upload new file').click - expect(page).to have_selector 'iframe#dialog_iframe' + expect(page).to have_selector 'iframe#dialog_iframe' - page.within_frame('dialog_iframe') do - attach_file "resource_file", Refinery.roots('refinery/resources'). - join("spec/fixtures/refinery_is_awesome.txt") - click_button ::I18n.t('save', :scope => 'refinery.admin.form_actions') + page.within_frame('dialog_iframe') do + attach_file 'resource_file', file_path + click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') + end end + end - expect(page).to have_content("Refinery Is Awesome") - expect(Refinery::Resource.count).to eq(1) + context 'when the file mime_type is acceptable' do + let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') } + + it 'the file is uploaded', js: true do + expect(uploading_a_file).to change(Refinery::Resource, :count).by(1) + end end - describe "max file size" do + context 'when the file mime_type is not acceptable' do + let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.txt') } + + it 'the file is rejected', js: true do + expect(uploading_a_file).to_not change(Refinery::Resource, :count) + + page.within_frame('dialog_iframe') do + expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) + end + end + end + + describe 'max file size' do before do allow(Refinery::Resources).to receive(:max_file_size).and_return('1224') end - context "in english" do + context 'in english' do before do allow(Refinery::I18n).to receive(:current_locale).and_return(:en) end - it "is shown" do + it 'is shown' do visit refinery.admin_resources_path - click_link "Upload new file" + click_link 'Upload new file' within('#file') do - expect(page).to have_selector("a[tooltip='The maximum file size is 1.2 KB.']") + expect(page).to have_selector('a[tooltip="The maximum file size is 1.2 KB."]') end end end - context "in danish" do + context 'in danish' do before do allow(Refinery::I18n).to receive(:current_locale).and_return(:da) end - it "is shown" do + it 'is shown' do visit refinery.admin_resources_path - click_link "Tilføj en ny fil" - within "#file" do - expect(page).to have_selector("a[tooltip='Filen må maksimalt fylde 1,2 KB.']") + click_link 'Tilføj en ny fil' + within '#file' do + expect(page).to have_selector('a[tooltip="Filen må maksimalt fylde 1,2 KB."]') end end end end end - context "edit/update" do + context 'edit/update' do let!(:resource) { FactoryBot.create(:resource) } - it "updates file" do + it 'updates file' do visit refinery.admin_resources_path - expect(page).to have_content("Refinery Is Awesome") + expect(page).to have_content('Cape Town Tide Table') expect(page).to have_selector("a[href='/refinery/resources/#{resource.id}/edit']") - click_link "Edit this file" + click_link 'Edit this file' - expect(page).to have_content("Refinery Is Awesome or replace it with this one...") + expect(page).to have_content('Cape Town Tide Table or replace it with this one...') expect(page).to have_selector("a[href*='/refinery/resources']") - attach_file "resource_file", Refinery.roots('refinery/resources').join("spec/fixtures/refinery_is_awesome2.txt") - click_button "Save" + attach_file 'resource_file', Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table2.pdf') + click_button 'Save' - expect(page).to have_content("Refinery Is Awesome2") + expect(page).to have_content('Cape Town Tide Table2') expect(Refinery::Resource.count).to eq(1) end - describe "translate" do + describe 'translate' do before do - allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :fr]) + allow(Refinery::I18n).to receive(:frontend_locales).and_return(%i[en fr]) end - it "can have a second locale added to it" do + it 'can have a second locale added to it' do visit refinery.admin_resources_path - expect(page).to have_content("Refinery Is Awesome") + expect(page).to have_content('Cape Town Tide Table') expect(page).to have_selector("a[href='/refinery/resources/#{resource.id}/edit']") - click_link "Edit this file" + click_link 'Edit this file' - within "#switch_locale_picker" do - click_link "FR" + within '#switch_locale_picker' do + click_link 'FR' end - fill_in "Title", :with => "Premier fichier" - click_button "Save" + fill_in 'Title', with: 'Premier fichier' + click_button 'Save' expect(page).to have_content("'Premier fichier' was successfully updated.") expect(Resource::Translation.count).to eq(1) @@ -118,53 +137,52 @@ module Admin end end - context "destroy" do + context 'destroy' do let!(:resource) { FactoryBot.create(:resource) } - it "removes file" do + it 'removes file' do visit refinery.admin_resources_path expect(page).to have_selector("a[href='/refinery/resources/#{resource.id}']") - click_link "Remove this file forever" + click_link 'Remove this file forever' - expect(page).to have_content("'Refinery Is Awesome' was successfully removed.") + expect(page).to have_content("'Cape Town Tide Table' was successfully removed.") expect(Refinery::Resource.count).to eq(0) end end - context "download" do + context 'download' do let!(:resource) { FactoryBot.create(:resource) } - it "succeeds" do + it 'succeeds' do visit refinery.admin_resources_path - click_link "Download this file" + click_link 'Download this file' - expect(page).to have_content("http://www.refineryhq.com/") + expect(page.body[0, 4]).to eq('%PDF') end context 'when the extension is mounted with a named space' do before do Rails.application.routes.draw do - mount Refinery::Core::Engine, :at => "/about" + mount Refinery::Core::Engine, at: '/about' end Rails.application.routes_reloader.reload! end after do Rails.application.routes.draw do - mount Refinery::Core::Engine, :at => "/" + mount Refinery::Core::Engine, at: '/' end end - it "succeeds" do + it 'succeeds' do visit refinery.admin_resources_path - click_link "Download this file" + click_link 'Download this file' - expect(page).to have_content("http://www.refineryhq.com/") + expect(page.body[0, 4]).to eq('%PDF') end - end end end diff --git a/resources/spec/fixtures/cape-town-tide-table.pdf b/resources/spec/fixtures/cape-town-tide-table.pdf new file mode 100644 index 0000000000000000000000000000000000000000..032a8a86c324399ff8c238b568330f3f755b8207 GIT binary patch literal 22024 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 :model do + describe Resource, type: :model do let(:resource) { FactoryBot.create(:resource) } - let(:titled_resource) { FactoryBot.create(:resource, resource_title: 'Resource Title')} + let(:titled_resource) { FactoryBot.create(:resource, resource_title: 'Resource Title') } - context "with valid attributes" do - it "should create successfully" do + context 'with valid attributes' do + it 'should create successfully' do expect(resource.errors).to be_empty end end - context "resource url" do - it "should respond to .url" do + context 'resource url' do + it 'should respond to .url' do expect(resource).to respond_to(:url) end - it "should not support thumbnailing like images do" do + it 'should not support thumbnailing like images do' do expect(resource).not_to respond_to(:thumbnail) end - it "should contain its filename at the end" do + it 'should contain its filename at the end' do expect(resource.url.split('/').last).to match(/\A#{resource.file_name}/) end - context "when Dragonfly.verify_urls is true" do + context 'when Dragonfly.verify_urls is true' do before do allow(Refinery::Resources).to receive(:dragonfly_verify_urls).and_return(true) ::Refinery::Dragonfly.configure!(Refinery::Resources) end - it "returns a url with an SHA parameter" do + it 'returns a url with an SHA parameter' do expect(resource.url).to match(/\?sha=[\da-fA-F]{16}\z/) end end - context "when Dragonfly.verify_urls is false" do + context 'when Dragonfly.verify_urls is false' do before do allow(Refinery::Resources).to receive(:dragonfly_verify_urls).and_return(false) ::Refinery::Dragonfly.configure!(Refinery::Resources) end - it "returns a url without an SHA parameter" do + + it 'returns a url without an SHA parameter' do expect(resource.url).not_to match(/\?sha=[\da-fA-F]{16}\z/) end end end - describe "#type_of_content" do - it "returns formated mime type" do - expect(resource.type_of_content).to eq("text plain") + describe '#type_of_content' do + it 'returns formated mime type' do + expect(resource.type_of_content).to eq('application pdf') end end - describe "#title" do + describe '#title' do context 'when a specific title has not been given' do - it "returns a titleized version of the filename" do - expect(resource.title).to eq("Refinery Is Awesome") + it 'returns a titleized version of the filename' do + expect(resource.title).to eq('Cape Town Tide Table') end end context 'when a specific title has been given' do @@ -65,93 +68,113 @@ module Refinery end end - describe ".per_page" do - context "dialog is true" do - it "returns resource count specified by Resources.pages_per_dialog option" do + describe '.per_page' do + context 'dialog is true' do + it 'returns resource count specified by Resources.pages_per_dialog option' do expect(Resource.per_page(true)).to eq(Resources.pages_per_dialog) end end - context "dialog is false" do - it "returns resource count specified by Resources.pages_per_admin_index constant" do + context 'dialog is false' do + it 'returns resource count specified by Resources.pages_per_admin_index constant' do expect(Resource.per_page).to eq(Resources.pages_per_admin_index) end end end - describe ".create_resources" do - let(:file) { Refinery.roots('refinery/resources').join("spec/fixtures/refinery_is_awesome.txt") } + describe '.create_resources' do + let(:file) { Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') } - context "only one resource uploaded" do - it "returns an array containing one resource" do - expect(Resource.create_resources(:file => file).size).to eq(1) + context 'only one resource uploaded' do + it 'returns an array containing one resource' do + expect(Resource.create_resources(file: file).size).to eq(1) end end - context "many resources uploaded at once" do - it "returns an array containing all those resources" do - expect(Resource.create_resources(:file => [file, file, file]).size).to eq(3) + context 'many resources uploaded at once' do + it 'returns an array containing all those resources' do + expect(Resource.create_resources(file: [file, file, file]).size).to eq(3) end end - specify "each returned array item should be an instance of resource" do - Resource.create_resources(:file => [file, file, file]).each do |r| - expect(r).to be_an_instance_of(Resource) + specify 'each returned array item should be an instance of resource' do + Resource.create_resources(file: [file, file, file]).each do |resource| + expect(resource).to be_an_instance_of(Resource) end end - specify "each returned array item should be passed form parameters" do - params = {:file => [file, file, file], :fake_param => 'blah'} + specify 'each returned array item should be passed form parameters' do + params = { file: [file, file, file], fake_param: 'blah' } - expect(Resource).to receive(:create).exactly(3).times.with({:file => file, :fake_param => 'blah'}) + expect(Resource).to receive(:create).exactly(3).times.with(file: file, fake_param: 'blah') Resource.create_resources(params) end end - describe "validations" do - describe "valid #file" do + describe 'validations' do + describe 'valid #file' do + before do + @file = Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') + Resources.max_file_size = (File.read(@file).size + 1000) + end + + it 'should be valid when size does not exceed .max_file_size' do + expect(Resource.new(file: @file)).to be_valid + end + end + + describe 'wrong mime_type #file' do before do - @file = Refinery.roots('refinery/resources').join("spec/fixtures/refinery_is_awesome.txt") + @file = Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.txt') Resources.max_file_size = (File.read(@file).size + 10) + @resource = Resource.new(file: @file) + end + + it 'should not be valid when mime_type is not in .whitelisted_mime_types' do + expect(@resource).not_to be_valid end - it "should be valid when size does not exceed .max_file_size" do - expect(Resource.new(:file => @file)).to be_valid + it 'should contain an error message' do + @resource.valid? + expect(@resource.errors).not_to be_empty + expect(@resource.errors[:file]).to eq(Array(::I18n.t( + 'incorrect_format', scope: 'activerecord.errors.models.refinery/resource' + ))) end end - describe "too large #file" do + describe 'too large #file' do before do - @file = Refinery.roots('refinery/resources').join("spec/fixtures/refinery_is_awesome.txt") + @file = Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') Resources.max_file_size = (File.read(@file).size - 10) - @resource = Resource.new(:file => @file) + @resource = Resource.new(file: @file) end - it "should not be valid when size exceeds .max_file_size" do + it 'should not be valid when size exceeds .max_file_size' do expect(@resource).not_to be_valid end - it "should contain an error message" do + it 'should contain an error message' do @resource.valid? expect(@resource.errors).not_to be_empty expect(@resource.errors[:file]).to eq(Array(::I18n.t( - 'too_big', :scope => 'activerecord.errors.models.refinery/resource', - :size => Resources.max_file_size - ))) + 'too_big', scope: 'activerecord.errors.models.refinery/resource', + size: Resources.max_file_size + ))) end end - describe "invalid argument for #file" do + describe 'invalid argument for #file' do before do @resource = Resource.new end - it "has an error message" do + it 'has an error message' do @resource.valid? expect(@resource.errors).not_to be_empty expect(@resource.errors[:file]).to eq(Array(::I18n.t( - 'blank', :scope => 'activerecord.errors.models.refinery/resource' - ))) + 'blank', scope: 'activerecord.errors.models.refinery/resource' + ))) end end end From 31b5e6c6bada44b2da8bea5c74c8d6ef0d32db8f Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Tue, 18 Sep 2018 22:59:14 -0400 Subject: [PATCH 2/6] Remove exclude .txt files in gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6b90aed9bf..d9073f40c3 100644 --- a/.gitignore +++ b/.gitignore @@ -89,7 +89,6 @@ Gemfile.lock # Local Gemfile for developing without sharing dependencies .gemfile -*.txt *.orig From 1096dbf655bed71ef31984cfcc68a7cac519255c Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Tue, 18 Sep 2018 22:59:31 -0400 Subject: [PATCH 3/6] Add missing refinery_is_secure.txt file for specs --- resources/spec/fixtures/refinery_is_secure.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 resources/spec/fixtures/refinery_is_secure.txt diff --git a/resources/spec/fixtures/refinery_is_secure.txt b/resources/spec/fixtures/refinery_is_secure.txt new file mode 100644 index 0000000000..3ba158285d --- /dev/null +++ b/resources/spec/fixtures/refinery_is_secure.txt @@ -0,0 +1 @@ +http://www.refineryhq.com/ From 54cfb90efe63b8e9398a9136623cbcf7b3dd0526 Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Wed, 19 Sep 2018 08:52:04 -0400 Subject: [PATCH 4/6] Resources : Whitelist more mime types --- .../lib/refinery/resources/configuration.rb | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/resources/lib/refinery/resources/configuration.rb b/resources/lib/refinery/resources/configuration.rb index 71b9a8fbf7..5f0f8d0419 100644 --- a/resources/lib/refinery/resources/configuration.rb +++ b/resources/lib/refinery/resources/configuration.rb @@ -15,6 +15,40 @@ module Resources self.dragonfly_name = :refinery_resources - self.whitelisted_mime_types = %w[application/pdf] + self.whitelisted_mime_types = %w[ + audio/mp4 + audio/mpeg + audio/wav + audio/x-wav + + image/gif + image/jpeg + image/png + image/svg+xml + image/tiff + image/x-psd + + video/mp4 + video/mpeg + video/quicktime + video/x-msvideo + video/x-ms-wmv + + text/csv + text/plain + + application/pdf + application/rtf + application/x-rar + application/zip + + application/vnd.ms-excel + application/vnd.ms-powerpoint + application/vnd.msword + + application/vnd.openxmlformats-officedocument.presentationml.presentation + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + application/vnd.openxmlformats-officedocument.wordprocessingml.document + ] end end \ No newline at end of file From ed387b868e1ca9bccb89bb28257c710f3b3bb222 Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Wed, 19 Sep 2018 09:03:37 -0400 Subject: [PATCH 5/6] Fix resources whitelist specs --- resources/spec/features/refinery/admin/resources_spec.rb | 2 +- resources/spec/fixtures/refinery_is_secure.html | 1 + resources/spec/fixtures/refinery_is_secure.txt | 1 - resources/spec/models/refinery/resource_spec.rb | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 resources/spec/fixtures/refinery_is_secure.html delete mode 100644 resources/spec/fixtures/refinery_is_secure.txt diff --git a/resources/spec/features/refinery/admin/resources_spec.rb b/resources/spec/features/refinery/admin/resources_spec.rb index b13d7fa065..5dc96092cd 100644 --- a/resources/spec/features/refinery/admin/resources_spec.rb +++ b/resources/spec/features/refinery/admin/resources_spec.rb @@ -44,7 +44,7 @@ module Admin end context 'when the file mime_type is not acceptable' do - let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.txt') } + let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.html') } it 'the file is rejected', js: true do expect(uploading_a_file).to_not change(Refinery::Resource, :count) diff --git a/resources/spec/fixtures/refinery_is_secure.html b/resources/spec/fixtures/refinery_is_secure.html new file mode 100644 index 0000000000..780b652bf1 --- /dev/null +++ b/resources/spec/fixtures/refinery_is_secure.html @@ -0,0 +1 @@ +https://www.refinerycms.com/ \ No newline at end of file diff --git a/resources/spec/fixtures/refinery_is_secure.txt b/resources/spec/fixtures/refinery_is_secure.txt deleted file mode 100644 index 3ba158285d..0000000000 --- a/resources/spec/fixtures/refinery_is_secure.txt +++ /dev/null @@ -1 +0,0 @@ -http://www.refineryhq.com/ diff --git a/resources/spec/models/refinery/resource_spec.rb b/resources/spec/models/refinery/resource_spec.rb index ad64caf689..2774c81709 100644 --- a/resources/spec/models/refinery/resource_spec.rb +++ b/resources/spec/models/refinery/resource_spec.rb @@ -125,7 +125,7 @@ module Refinery describe 'wrong mime_type #file' do before do - @file = Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.txt') + @file = Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.html') Resources.max_file_size = (File.read(@file).size + 10) @resource = Resource.new(file: @file) end From e5aa92b7969316c1d859e108ad27a8ac75aa8951 Mon Sep 17 00:00:00 2001 From: Brice Sanchez Date: Wed, 19 Sep 2018 09:19:57 -0400 Subject: [PATCH 6/6] List all file types in `incorrect_format` locales :en and :fr --- resources/config/locales/en.yml | 5 +++-- resources/config/locales/fr.yml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/config/locales/en.yml b/resources/config/locales/en.yml index 73704f24e2..904ec0854b 100644 --- a/resources/config/locales/en.yml +++ b/resources/config/locales/en.yml @@ -36,5 +36,6 @@ en: models: refinery/resource: blank: You must specify file for upload - incorrect_format: Your file must be a PDF - too_big: File should be smaller than %{size} bytes in size + incorrect_format: "File type is not allowed. Your file must be a MP4, MPEG, WMV, AVI, WAV, + GIF, JPEG, PNG, SVG, TIFF, PSD, CSV, PDF, TXT, RAR, ZIP, XLS, PPT or a DOC" + too_big: File should be smaller than %{size} bytes in size \ No newline at end of file diff --git a/resources/config/locales/fr.yml b/resources/config/locales/fr.yml index c89accc0e3..064847f643 100644 --- a/resources/config/locales/fr.yml +++ b/resources/config/locales/fr.yml @@ -36,5 +36,6 @@ fr: models: refinery/resource: blank: Vous devez spécifier un fichier à télécharger - incorrect_format: Votre fichier doit être un PDF - too_big: Le poids maximal des fichiers est de %{size} megaoctets + incorrect_format: "Type de fichier non autorisé. Votre fichier doit être un MP4, MPEG, WMV, AVI, WAV, + GIF, JPEG, PNG, SVG, TIFF, PSD, CSV, PDF, TXT, RAR, ZIP, XLS, PPT ou un DOC" + too_big: Le poids maximal des fichiers est de %{size} megaoctets \ No newline at end of file