diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js index 43a630d4a9e..ccb3937ca8e 100644 --- a/app/assets/javascripts/discourse/components/utilities.js +++ b/app/assets/javascripts/discourse/components/utilities.js @@ -174,9 +174,13 @@ Discourse.Utilities = { return false; } var upload = files[0]; - // ensures that new users can upload image - if (Discourse.User.current('trust_level') === 0 && Discourse.SiteSettings.newuser_max_images === 0) { - bootbox.alert(I18n.t('post.errors.upload_not_allowed_for_new_user')); + // ensures that new users can upload image/attachment + if (Discourse.Utilities.isUploadForbidden(upload.name)) { + if (Discourse.Utilities.isAnImage(upload.name)) { + bootbox.alert(I18n.t('post.errors.image_upload_not_allowed_for_new_user')); + } else { + bootbox.alert(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user')); + } return false; } // if the image was pasted, sets its name to a default one @@ -242,6 +246,17 @@ Discourse.Utilities = { **/ maxUploadSizeInKB: function(path) { return Discourse.Utilities.isAnImage(path) ? Discourse.SiteSettings.max_image_size_kb : Discourse.SiteSettings.max_attachment_size_kb; + }, + + /** + Test whether an upload is forbidden or not + + @method isUploadForbidden + @param {String} path The path + **/ + isUploadForbidden: function(path) { + if (Discourse.User.current('trust_level') > 0) { return false; } + return Discourse.Utilities.isAnImage(path) ? Discourse.SiteSettings.newuser_max_images === 0 : Discourse.SiteSettings.newuser_max_attachments === 0; } }; diff --git a/app/models/post.rb b/app/models/post.rb index ad33a08609c..c1f6b8e34eb 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1,5 +1,7 @@ require_dependency 'jobs' require_dependency 'pretty_text' +require_dependency 'local_store' +require_dependency 's3_store' require_dependency 'rate_limiter' require_dependency 'post_revisor' require_dependency 'enum' @@ -89,7 +91,7 @@ class Post < ActiveRecord::Base @post_analyzer = PostAnalyzer.new(raw, topic_id) end - %w{raw_mentions linked_hosts image_count link_count raw_links}.each do |attr| + %w{raw_mentions linked_hosts image_count attachment_count link_count raw_links}.each do |attr| define_method(attr) do PostAnalyzer.new(raw, topic_id).send(attr) end diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 8ee8acc9e03..26cf7ac5d3a 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -39,6 +39,18 @@ class PostAnalyzer end.count end + # How many attachments are present in the post + def attachment_count + return 0 unless @raw.present? + + if SiteSetting.enable_s3_uploads? + cooked_document.css("a.attachment[href^=\"#{S3Store.base_url}\"]") + else + cooked_document.css("a.attachment[href^=\"#{LocalStore.directory}\"]") + + cooked_document.css("a.attachment[href^=\"#{LocalStore.base_url}\"]") + end.count + end + def raw_mentions return [] if @raw.blank? diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index adb0add7977..a8c2e16bd57 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -212,6 +212,7 @@ class SiteSetting < ActiveRecord::Base setting(:newuser_max_links, 2) client_setting(:newuser_max_images, 0) + client_setting(:newuser_max_attachments, 0) setting(:newuser_spam_host_threshold, 3) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e83302dc6df..ca5f109aa7c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -757,7 +757,8 @@ en: upload_too_large: "Sorry, the file you are trying to upload is too big (maximum size is {{max_size_kb}}kb), please resize it and try again." too_many_uploads: "Sorry, you can only upload one file at a time." upload_not_authorized: "Sorry, the file you are trying to upload is not authorized (authorized extension: {{authorized_extensions}})." - upload_not_allowed_for_new_user: "Sorry, new users can not upload images." + image_upload_not_allowed_for_new_user: "Sorry, new users can not upload images." + attachment_upload_not_allowed_for_new_user: "Sorry, new users can not upload attachments." abandon: "Are you sure you want to abandon your post?" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 3e8d40dae5b..5403068f96a 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -737,7 +737,8 @@ fr: upload_too_large: "Désolé, le fichier que vous êtes en train d'envoyer est trop grand (maximum {{max_size_kb}}Kb). Merci de le redimensionner et de réessayer." too_many_uploads: "Désolé, vous ne pouvez envoyer qu'un seul fichier à la fois." upload_not_authorized: "Désole, le fichier que vous êtes en train d'uploader n'est pas autorisé (extensions autorisées : {{authorized_extensions}})." - upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader d'images." + image_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader d'image." + attachment_upload_not_allowed_for_new_user: "Désolé, les nouveaux utilisateurs ne peuvent pas uploader de fichier." abandon: "Voulez-vous vraiment abandonner ce message ?" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 85b0fd274bf..7e079b072d7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -39,6 +39,10 @@ en: zero: "Sorry, new users can't put images in posts." one: "Sorry, new users can only put one image in a post." other: "Sorry, new users can only put %{count} images in a post." + too_many_attachments: + zero: "Sorry, new users can't put attachments in posts." + one: "Sorry, new users can only put one attachment in a post." + other: "Sorry, new users can only put %{count} attachments in a post." too_many_links: zero: "Sorry, new users can't put links in posts." one: "Sorry, new users can only put one link in a post." @@ -606,6 +610,7 @@ en: newuser_max_links: "How many links a new user can add to a post" newuser_max_images: "How many images a new user can add to a post" + newuser_max_attachments: "How many attachments a new user can add to a post" newuser_max_mentions_per_post: "Maximum number of @name notifications a new user can use in a post" max_mentions_per_post: "Maximum number of @name notifications you can use in a post" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 00be8d3212e..1fcf8f1a208 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -41,6 +41,10 @@ fr: zero: "Désolé, les visiteurs ne peuvent pas ajouter d'image." one: "Désolé, les visiteurs ne peuvent ajouter qu'une seule image." other: "Désolé, les visiteurs ne peuvent ajouter que %{count} images." + too_many_attachments: + zero: "Désolé, les visiteurs ne peuvent pas ajouter de fichier." + one: "Désolé, les visiteurs ne peuvent ajouter qu'un seul fichier." + other: "Désolé, les visiteurs ne peuvent ajouter que %{count} fichiers." too_many_links: zero: "Désolé, les visiteurs ne peuvent pas insérer de liens." one: "Désolé, les visiteurs ne peuvent insérer qu'un seul lien." @@ -534,6 +538,7 @@ fr: newuser_max_links: "Nombre maximum de liens qu'un visiteur peut ajouter à un message" newuser_max_images: "Nombre maximum d'images qu'un visiteur peut ajouter à un message" + newuser_max_attachments: "Nombre maximum de fichiers qu'un visiteur peut ajouter à un message" newuser_max_mentions_per_post: "Nombre maximum de référence à un @utilisateur qu'un visiteur peut ajouter à un message" max_mentions_per_post: "Le nombre maximal de @mentions que vous pouvez ajouter à un message" diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index ae10848ad40..47921943018 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -223,10 +223,12 @@ class CookedPostProcessor def attachments if SiteSetting.enable_s3_uploads? - @doc.css("a[href^=\"#{S3Store.base_url}\"]") + @doc.css("a.attachment[href^=\"#{S3Store.base_url}\"]") else # local uploads are identified using a relative uri - @doc.css("a[href^=\"#{LocalStore.directory}\"]") + @doc.css("a.attachment[href^=\"#{LocalStore.directory}\"]") + + # when cdn is enabled, we have the whole url + @doc.css("a.attachment[href^=\"#{LocalStore.base_url}\"]") end end diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb index 941585c5130..f8e14a6df07 100644 --- a/lib/validators/post_validator.rb +++ b/lib/validators/post_validator.rb @@ -7,6 +7,7 @@ class Validators::PostValidator < ActiveModel::Validator raw_quality(record) max_mention_validator(record) max_images_validator(record) + max_attachments_validator(record) max_links_validator(record) unique_post_validator(record) end @@ -41,6 +42,11 @@ class Validators::PostValidator < ActiveModel::Validator add_error_if_count_exceeded(post, :too_many_images, post.image_count, SiteSetting.newuser_max_images) unless acting_user_is_trusted?(post) end + # Ensure new users can not put too many attachments in a post + def max_attachments_validator(post) + add_error_if_count_exceeded(post, :too_many_attachments, post.attachment_count, SiteSetting.newuser_max_attachments) unless acting_user_is_trusted?(post) + end + # Ensure new users can not put too many links in a post def max_links_validator(post) add_error_if_count_exceeded(post, :too_many_links, post.link_count, SiteSetting.newuser_max_links) unless acting_user_is_trusted?(post) diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 525e14133df..2f0d54d8a2b 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -184,6 +184,54 @@ describe Post do end + describe "maximum attachments" do + let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) } + let(:post_no_attachments) { Fabricate.build(:post, post_args.merge(user: newuser)) } + let(:post_one_attachment) { post_with_body('file.txt', newuser) } + let(:post_two_attachments) { post_with_body('errors.log model.3ds', newuser) } + + it "returns 0 attachments for an empty post" do + Fabricate.build(:post).attachment_count.should == 0 + end + + it "finds attachments from HTML" do + post_two_attachments.attachment_count.should == 2 + end + + context "validation" do + + before do + SiteSetting.stubs(:newuser_max_attachments).returns(1) + end + + context 'newuser' do + it "allows a new user to post below the limit" do + post_one_attachment.should be_valid + end + + it "doesn't allow more than the maximum" do + post_two_attachments.should_not be_valid + end + + it "doesn't allow a new user to edit their post to insert an attachment" do + post_no_attachments.user.trust_level = TrustLevel.levels[:new] + post_no_attachments.save + -> { + post_no_attachments.revise(post_no_attachments.user, post_two_attachments.raw) + post_no_attachments.reload + }.should_not change(post_no_attachments, :raw) + end + end + + it "allows more attachments from a not-new account" do + post_two_attachments.user.trust_level = TrustLevel.levels[:basic] + post_two_attachments.should be_valid + end + + end + + end + context "links" do let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) } let(:no_links) { post_with_body("hello world my name is evil trout", newuser) } diff --git a/test/javascripts/components/utilities_test.js b/test/javascripts/components/utilities_test.js index 23e3af6902c..40329feb622 100644 --- a/test/javascripts/components/utilities_test.js +++ b/test/javascripts/components/utilities_test.js @@ -22,13 +22,22 @@ test("uploading one file", function() { ok(bootbox.alert.calledWith(I18n.t('post.errors.too_many_uploads'))); }); -test("new user", function() { +test("new user cannot upload images", function() { Discourse.SiteSettings.newuser_max_images = 0; this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0); this.stub(bootbox, "alert"); - ok(!validUpload([1])); - ok(bootbox.alert.calledWith(I18n.t('post.errors.upload_not_allowed_for_new_user'))); + ok(!validUpload([{name: "image.png"}])); + ok(bootbox.alert.calledWith(I18n.t('post.errors.image_upload_not_allowed_for_new_user'))); +}); + +test("new user cannot upload attachments", function() { + Discourse.SiteSettings.newuser_max_attachments = 0; + this.stub(Discourse.User, 'current').withArgs("trust_level").returns(0); + this.stub(bootbox, "alert"); + + ok(!validUpload([{name: "roman.txt"}])); + ok(bootbox.alert.calledWith(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user'))); }); test("ensures an authorized upload", function() { @@ -141,4 +150,4 @@ test("avatarImg", function() { blank(Discourse.Utilities.avatarImg({username: 'weird*username', size: 'tiny'}), "it doesn't render avatars for invalid usernames"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/fixtures/site_settings_fixtures.js b/test/javascripts/fixtures/site_settings_fixtures.js index 8bf7e041307..2c854088454 100644 --- a/test/javascripts/fixtures/site_settings_fixtures.js +++ b/test/javascripts/fixtures/site_settings_fixtures.js @@ -1,3 +1,3 @@ /*jshint maxlen:10000000 */ Discourse.SiteSettingsOriginal = {"title":"Discourse Meta","logo_url":"/assets/logo.png","logo_small_url":"/assets/logo-single.png","traditional_markdown_linebreaks":false,"top_menu":"latest|new|unread|read|favorited|categories","post_menu":"like|edit|flag|delete|share|bookmark|reply","share_links":"twitter|facebook|google+|email","track_external_right_clicks":false,"must_approve_users":false,"ga_tracking_code":"UA-33736483-2","ga_domain_name":"","enable_long_polling":true,"polling_interval":3000,"anon_polling_interval":30000,"min_post_length":20,"max_post_length":16000,"min_topic_title_length":15,"max_topic_title_length":255,"min_private_message_title_length":2,"allow_uncategorized_topics":true,"min_search_term_length":3,"flush_timings_secs":5,"suppress_reply_directly_below":true,"email_domains_blacklist":"mailinator.com","email_domains_whitelist":null,"version_checks":true,"min_title_similar_length":10,"min_body_similar_length":15,"category_colors":"BF1E2E|F1592A|F7941D|9EB83B|3AB54A|12A89D|25AAE2|0E76BD|652D90|92278F|ED207B|8C6238|231F20|808281|B3B5B4|283890","max_upload_size_kb":1024,"category_featured_topics":6,"favicon_url":"/assets/favicon.ico","dynamic_favicon":false,"uncategorized_name":"uncategorized","uncategorized_color":"AB9364","uncategorized_text_color":"FFFFFF","invite_only":false,"login_required":false,"enable_local_logins":true,"enable_local_account_create":true,"enable_google_logins":true,"enable_yahoo_logins":true,"enable_twitter_logins":true,"enable_facebook_logins":true,"enable_cas_logins":false,"enable_github_logins":true,"enable_persona_logins":true,"educate_until_posts":2,"topic_views_heat_low":1000,"topic_views_heat_medium":2000,"topic_views_heat_high":5000,"min_private_message_post_length":5,"faq_url":"","tos_url":"","privacy_policy_url":"","authorized_extensions":".jpg|.jpeg|.png|.gif","relative_date_duration":14}; -Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal); \ No newline at end of file +Discourse.SiteSettings = jQuery.extend(true, {}, Discourse.SiteSettingsOriginal);