# frozen_string_literal: true require "rails_helper" require "cooked_post_processor" require "file_store/s3_store" describe CookedPostProcessor do context "#post_process" do let(:upload) do Fabricate(:upload, url: '/uploads/default/original/1X/1/1234567890123456.jpg' ) end let(:post) do Fabricate(:post, raw: <<~RAW) RAW end let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } let(:post_process) { sequence("post_process") } it "post process in sequence" do cpp.expects(:post_process_oneboxes).in_sequence(post_process) cpp.expects(:post_process_images).in_sequence(post_process) cpp.expects(:optimize_urls).in_sequence(post_process) cpp.expects(:pull_hotlinked_images).in_sequence(post_process) cpp.post_process expect(PostUpload.exists?(post: post, upload: upload)).to eq(true) end describe 'when post contains oneboxes and inline oneboxes' do let(:url_hostname) { 'meta.discourse.org' } let(:url) do "https://#{url_hostname}/t/mini-inline-onebox-support-rfc/66400" end let(:not_oneboxed_url) do "https://#{url_hostname}/t/random-url" end let(:title) { 'some title' } let(:post) do Fabricate(:post, raw: <<~RAW) #{url} This is a #{url} with path #{not_oneboxed_url} This is a https://#{url_hostname}/t/another-random-url test This is a #{url} with path #{url} RAW end before do SiteSetting.enable_inline_onebox_on_all_domains = true %i{head get}.each do |method| stub_request(method, url).to_return( status: 200, body: <<~RAW
Link
Google
text.txt (20 Bytes)
Link
Google
text.txt (20 Bytes)
Link
Google
text.txt (20 Bytes)
Link
Google
text.txt (20 Bytes)
Link
Google
text.txt (20 Bytes)
This post has a local emoji and an external upload
HTML end end end context "#remove_user_ids" do let(:topic) { Fabricate(:topic) } let(:post) do Fabricate(:post, raw: <<~RAW) link to a topic: #{topic.url}?u=foo a tricky link to a topic: #{topic.url}?bob=bob;u=sam&jane=jane link to an external topic: https://google.com/?u=bar a malformed url: https://www.example.com/#123#4 RAW end let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) } it "does remove user ids" do cpp.remove_user_ids expect(cpp.html).to have_tag('a', with: { href: topic.url }) expect(cpp.html).to have_tag('a', with: { href: "#{topic.url}?bob=bob&jane=jane" }) expect(cpp.html).to have_tag('a', with: { href: "https://google.com/?u=bar" }) expect(cpp.html).to have_tag('a', with: { href: "https://www.example.com/#123#4" }) end end context "#pull_hotlinked_images" do let(:post) { build(:post, created_at: 20.days.ago) } let(:cpp) { CookedPostProcessor.new(post) } before { cpp.stubs(:available_disk_space).returns(90) } it "does not run when download_remote_images_to_local is disabled" do SiteSetting.download_remote_images_to_local = false Jobs.expects(:cancel_scheduled_job).never cpp.pull_hotlinked_images end context "when download_remote_images_to_local? is enabled" do before do SiteSetting.download_remote_images_to_local = true end it "does not run when there is not enough disk space" do cpp.expects(:disable_if_low_on_disk_space).returns(true) Jobs.expects(:cancel_scheduled_job).never cpp.pull_hotlinked_images end context "and there is enough disk space" do before { cpp.expects(:disable_if_low_on_disk_space).returns(false) } it "does not run when the system user updated the post" do post.last_editor_id = Discourse.system_user.id Jobs.expects(:cancel_scheduled_job).never cpp.pull_hotlinked_images end context "and the post has been updated by an actual user" do before { post.id = 42 } it "ensures only one job is scheduled right after the editing_grace_period" do Jobs.expects(:cancel_scheduled_job).with(:pull_hotlinked_images, post_id: post.id).once delay = SiteSetting.editing_grace_period + 1 Jobs.expects(:enqueue_in).with(delay.seconds, :pull_hotlinked_images, post_id: post.id, bypass_bump: false).once cpp.pull_hotlinked_images end end end end end context "#disable_if_low_on_disk_space" do let(:post) { build(:post, created_at: 20.days.ago) } let(:cpp) { CookedPostProcessor.new(post) } before { cpp.expects(:available_disk_space).returns(50) } it "does nothing when there's enough disk space" do SiteSetting.expects(:download_remote_images_threshold).returns(20) SiteSetting.expects(:download_remote_images_to_local).never expect(cpp.disable_if_low_on_disk_space).to eq(false) end context "when there's not enough disk space" do before { SiteSetting.expects(:download_remote_images_threshold).returns(75) } it "disables download_remote_images_threshold and send a notification to the admin" do StaffActionLogger.any_instance.expects(:log_site_setting_change).once SystemMessage.expects(:create_from_system_user).with(Discourse.site_contact_user, :download_remote_images_disabled).once expect(cpp.disable_if_low_on_disk_space).to eq(true) expect(SiteSetting.download_remote_images_to_local).to eq(false) end end end context "#download_remote_images_max_days_old" do let(:post) { build(:post, created_at: 20.days.ago) } let(:cpp) { CookedPostProcessor.new(post) } before do SiteSetting.download_remote_images_to_local = true cpp.expects(:disable_if_low_on_disk_space).returns(false) end it "does not run when download_remote_images_max_days_old is not satisfied" do SiteSetting.download_remote_images_max_days_old = 15 Jobs.expects(:cancel_scheduled_job).never cpp.pull_hotlinked_images end it "runs when download_remote_images_max_days_old is satisfied" do SiteSetting.download_remote_images_max_days_old = 30 Jobs.expects(:cancel_scheduled_job).with(:pull_hotlinked_images, post_id: post.id).once delay = SiteSetting.editing_grace_period + 1 Jobs.expects(:enqueue_in).with(delay.seconds, :pull_hotlinked_images, post_id: post.id, bypass_bump: false).once cpp.pull_hotlinked_images end end context "#is_a_hyperlink?" do let(:post) { build(:post) } let(:cpp) { CookedPostProcessor.new(post) } let(:doc) { Nokogiri::HTML::fragment('') } it "is true when the image is inside a link" do img = doc.css("img#linked_image").first expect(cpp.is_a_hyperlink?(img)).to eq(true) end it "is false when the image is not inside a link" do img = doc.css("img#standard_image").first expect(cpp.is_a_hyperlink?(img)).to eq(false) end end context "grant badges" do let(:cpp) { CookedPostProcessor.new(post) } context "emoji inside a quote" do let(:post) { Fabricate(:post, raw: "time to eat some sweet \n[quote]\n:candy:\n[/quote]\n mmmm") } it "doesn't award a badge when the emoji is in a quote" do cpp.grant_badges expect(post.user.user_badges.where(badge_id: Badge::FirstEmoji).exists?).to eq(false) end end context "emoji in the text" do let(:post) { Fabricate(:post, raw: "time to eat some sweet :candy: mmmm") } it "awards a badge for using an emoji" do cpp.grant_badges expect(post.user.user_badges.where(badge_id: Badge::FirstEmoji).exists?).to eq(true) end end context "onebox" do let(:post) { Fabricate(:post, raw: "onebox me:\n\nhttps://www.youtube.com/watch?v=Wji-BZ0oCwg\n") } before { Oneboxer.stubs(:onebox) } it "awards a badge for using an onebox" do cpp.post_process_oneboxes cpp.grant_badges expect(post.user.user_badges.where(badge_id: Badge::FirstOnebox).exists?).to eq(true) end it "doesn't award the badge when the badge is disabled" do Badge.where(id: Badge::FirstOnebox).update_all(enabled: false) cpp.post_process_oneboxes cpp.grant_badges expect(post.user.user_badges.where(badge_id: Badge::FirstOnebox).exists?).to eq(false) end end context "reply_by_email" do let(:post) { Fabricate(:post, raw: "This is a **reply** via email ;)", via_email: true, post_number: 2) } it "awards a badge for replying via email" do cpp.grant_badges expect(post.user.user_badges.where(badge_id: Badge::FirstReplyByEmail).exists?).to eq(true) end end end context "quote processing" do let(:cpp) { CookedPostProcessor.new(cp) } let(:pp) { Fabricate(:post, raw: "This post is ripe for quoting!") } context "with an unmodified quote" do let(:cp) do Fabricate( :post, raw: "[quote=\"#{pp.user.username}, post: #{pp.post_number}, topic:#{pp.topic_id}]\nripe for quoting\n[/quote]\ntest" ) end it "should not be marked as modified" do cpp.post_process_quotes expect(cpp.doc.css('aside.quote.quote-modified')).to be_blank end end context "with a modified quote" do let(:cp) do Fabricate( :post, raw: "[quote=\"#{pp.user.username}, post: #{pp.post_number}, topic:#{pp.topic_id}]\nmodified\n[/quote]\ntest" ) end it "should be marked as modified" do cpp.post_process_quotes expect(cpp.doc.css('aside.quote.quote-modified')).to be_present end end end context "remove direct reply full quote" do let(:topic) { Fabricate(:topic) } let!(:post) { Fabricate(:post, topic: topic, raw: "this is the first post") } let(:raw) do <<~RAW.strip [quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"] this is the first post [/quote] and this is the third reply RAW end let(:raw2) do <<~RAW.strip and this is the third reply [quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"] this is the first post [/quote] RAW end before do SiteSetting.remove_full_quote = true end it 'works' do hidden = Fabricate(:post, topic: topic, hidden: true, raw: "this is the second post after") small_action = Fabricate(:post, topic: topic, post_type: Post.types[:small_action]) reply = Fabricate(:post, topic: topic, raw: raw) freeze_time Time.zone.now do topic.bumped_at = 1.day.ago CookedPostProcessor.new(reply).removed_direct_reply_full_quotes expect(topic.ordered_posts.pluck(:id)) .to eq([post.id, hidden.id, small_action.id, reply.id]) expect(topic.bumped_at).to eq(1.day.ago) expect(reply.raw).to eq("and this is the third reply") expect(reply.revisions.count).to eq(1) expect(reply.revisions.first.modifications["raw"]).to eq([raw, reply.raw]) expect(reply.revisions.first.modifications["edit_reason"][1]).to eq(I18n.t(:removed_direct_reply_full_quotes)) end end it 'does not delete quote if not first paragraph' do reply = Fabricate(:post, topic: topic, raw: raw2) CookedPostProcessor.new(reply).removed_direct_reply_full_quotes expect(topic.ordered_posts.pluck(:id)).to eq([post.id, reply.id]) expect(reply.raw).to eq(raw2) end it "does nothing when 'remove_full_quote' is disabled" do SiteSetting.remove_full_quote = false reply = Fabricate(:post, topic: topic, raw: raw) CookedPostProcessor.new(reply).removed_direct_reply_full_quotes expect(reply.raw).to eq(raw) end it "works only on new posts" do Fabricate(:post, topic: topic, hidden: true, raw: "this is the second post after") Fabricate(:post, topic: topic, post_type: Post.types[:small_action]) reply = PostCreator.create!(topic.user, topic_id: topic.id, raw: raw) CookedPostProcessor.new(reply).post_process expect(reply.raw).to eq(raw) PostRevisor.new(reply).revise!(Discourse.system_user, raw: raw, edit_reason: "put back full quote") CookedPostProcessor.new(reply).post_process(new_post: true) expect(reply.raw).to eq("and this is the third reply") end end end