# frozen_string_literal: true require "cooked_post_processor" require "file_store/s3_store" RSpec.describe CookedPostProcessor do fab!(:upload) fab!(:large_image_upload) fab!(:user_with_auto_groups) { Fabricate(:user, refresh_auto_groups: true) } let(:upload_path) { Discourse.store.upload_path } describe "#post_process" do fab!(:post) { Fabricate(:post, user: user_with_auto_groups, raw: <<~RAW) } RAW let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: 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.post_process expect(UploadReference.exists?(target: post, upload: upload)).to eq(true) end describe "when post contains oneboxes and inline oneboxes" do let(:url_hostname) { "meta.discourse.org" } let(:url) { "https://#{url_hostname}/t/mini-inline-onebox-support-rfc/66400" } let(:not_oneboxed_url) { "https://#{url_hostname}/t/random-url" } let(:title) { "some title" } let(:post) { Fabricate(:post, user: user_with_auto_groups, 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 before do SiteSetting.enable_inline_onebox_on_all_domains = true Oneboxer.stubs(:cached_onebox).with(url).returns <<~HTML HTML Oneboxer.stubs(:cached_onebox).with(not_oneboxed_url).returns(nil) %i[head get].each do |method| stub_request(method, url).to_return(status: 200, body: <<~RAW) #{title} RAW end end after do InlineOneboxer.invalidate(url) Oneboxer.invalidate(url) end it "should respect SiteSetting.max_oneboxes_per_post" do SiteSetting.max_oneboxes_per_post = 2 SiteSetting.add_rel_nofollow_to_user_content = false cpp.post_process expect(cpp.html).to have_tag( "a", with: { href: url, class: "inline-onebox", }, text: title, count: 2, ) expect(cpp.html).to have_tag("aside.onebox a", text: title, count: 1) expect(cpp.html).to have_tag("aside.onebox a", text: url_hostname, count: 1) expect(cpp.html).to have_tag( "a", without: { class: "inline-onebox-loading", }, text: not_oneboxed_url, count: 1, ) expect(cpp.html).to have_tag( "a", without: { class: "onebox", }, text: not_oneboxed_url, count: 1, ) end end describe "when post contains inline oneboxes" do before { SiteSetting.enable_inline_onebox_on_all_domains = true } describe "internal links" do fab!(:topic) fab!(:post) { Fabricate(:post, user: user_with_auto_groups, raw: "Hello #{topic.url}") } let(:url) { topic.url } it "includes the topic title" do cpp.post_process expect(cpp.html).to have_tag( "a", with: { href: UrlHelper.cook_url(url), }, without: { class: "inline-onebox-loading", }, text: topic.title, count: 1, ) topic.update!(title: "Updated to something else") cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) cpp.post_process expect(cpp.html).to have_tag( "a", with: { href: UrlHelper.cook_url(url), }, without: { class: "inline-onebox-loading", }, text: topic.title, count: 1, ) end end describe "external links" do let(:url_with_path) { "https://meta.discourse.org/t/mini-inline-onebox-support-rfc/66400" } let(:url_with_query_param) { "https://meta.discourse.org?a" } let(:url_no_path) { "https://meta.discourse.org/" } let(:urls) { [url_with_path, url_with_query_param, url_no_path] } let(:title) { "some title" } let(:escaped_title) { CGI.escapeHTML(title) } let(:post) { Fabricate(:post, user: user_with_auto_groups, raw: <<~RAW) } This is a #{url_with_path} topic This should not be inline #{url_no_path} oneboxed - #{url_with_path} - #{url_with_query_param} RAW let(:staff_post) { Fabricate(:post, user: Fabricate(:admin), raw: <<~RAW) } This is a #{url_with_path} topic RAW before do urls.each do |url| stub_request(:get, url).to_return( status: 200, body: "#{escaped_title}", ) end end after { urls.each { |url| InlineOneboxer.invalidate(url) } } it "should convert the right links to inline oneboxes" do cpp.post_process html = cpp.html expect(html).to_not have_tag( "a", with: { href: url_no_path, }, without: { class: "inline-onebox-loading", }, text: title, ) expect(html).to have_tag( "a", with: { href: url_with_path, }, without: { class: "inline-onebox-loading", }, text: title, count: 2, ) expect(html).to have_tag( "a", with: { href: url_with_query_param, }, without: { class: "inline-onebox-loading", }, text: title, count: 1, ) expect(html).to have_tag("a[rel='noopener nofollow ugc']") end it "removes nofollow if user is staff/tl3" do cpp = CookedPostProcessor.new(staff_post, invalidate_oneboxes: true) cpp.post_process expect(cpp.html).to_not have_tag("a[rel='noopener nofollow ugc']") end end end context "when processing images" do before { SiteSetting.responsive_post_image_sizes = "" } context "with responsive images" do before { SiteSetting.responsive_post_image_sizes = "1|1.5|3" } it "includes responsive images on demand" do upload.update!(width: 2000, height: 1500, filesize: 10_000, dominant_color: "FFFFFF") post = Fabricate(:post, user: user_with_auto_groups, raw: "hello ") # fake some optimized images OptimizedImage.create!( url: "/#{upload_path}/666x500.jpg", width: 666, height: 500, upload_id: upload.id, sha1: SecureRandom.hex, extension: ".jpg", filesize: 500, version: OptimizedImage::VERSION, ) # fake 3x optimized image, we lose 2 pixels here over original due to rounding on downsize OptimizedImage.create!( url: "/#{upload_path}/1998x1500.jpg", width: 1998, height: 1500, upload_id: upload.id, sha1: SecureRandom.hex, extension: ".jpg", filesize: 800, ) cpp = CookedPostProcessor.new(post) cpp.add_to_size_cache(upload.url, 2000, 1500) cpp.post_process html = cpp.html expect(html).to include(%Q|data-dominant-color="FFFFFF"|) # 1.5x is skipped cause we have a missing thumb expect(html).to include( "srcset=\"//test.localhost/#{upload_path}/666x500.jpg, //test.localhost/#{upload_path}/1998x1500.jpg 3x\"", ) expect(html).to include("src=\"//test.localhost/#{upload_path}/666x500.jpg\"") # works with CDN set_cdn_url("http://cdn.localhost") cpp = CookedPostProcessor.new(post) cpp.add_to_size_cache(upload.url, 2000, 1500) cpp.post_process html = cpp.html expect(html).to include(%Q|data-dominant-color="FFFFFF"|) expect(html).to include( "srcset=\"//cdn.localhost/#{upload_path}/666x500.jpg, //cdn.localhost/#{upload_path}/1998x1500.jpg 3x\"", ) expect(html).to include("src=\"//cdn.localhost/#{upload_path}/666x500.jpg\"") end it "doesn't include response images for cropped images" do upload.update!(width: 200, height: 4000, filesize: 12_345) post = Fabricate(:post, user: user_with_auto_groups, raw: "hello ") # fake some optimized images OptimizedImage.create!( url: "http://a.b.c/200x500.jpg", width: 200, height: 500, upload_id: upload.id, sha1: SecureRandom.hex, extension: ".jpg", filesize: 500, ) cpp = CookedPostProcessor.new(post) cpp.add_to_size_cache(upload.url, 200, 4000) cpp.post_process expect(cpp.html).to_not include('srcset="') end end shared_examples "leave dimensions alone" do it "doesn't use them" do expect(cpp.html).to match(%r{src="http://foo.bar/image.png" width="" height=""}) expect(cpp.html).to match(%r{src="http://domain.com/picture.jpg" width="50" height="42"}) expect(cpp).to be_dirty end end context "with image_sizes" do fab!(:post) { Fabricate(:post_with_image_urls, user: user_with_auto_groups) } let(:cpp) { CookedPostProcessor.new(post, image_sizes: image_sizes) } before do stub_image_size cpp.post_process end context "when valid" do let(:image_sizes) do { "http://foo.bar/image.png" => { "width" => 111, "height" => 222 } } end it "uses them" do expect(cpp.html).to match(%r{src="http://foo.bar/image.png" width="111" height="222"}) expect(cpp.html).to match( %r{src="http://domain.com/picture.jpg" width="50" height="42"}, ) expect(cpp).to be_dirty end end context "with invalid width" do let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 0, "height" => 222 } } } include_examples "leave dimensions alone" end context "with invalid height" do let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 111, "height" => 0 } } } include_examples "leave dimensions alone" end context "with invalid width & height" do let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 0, "height" => 0 } } } include_examples "leave dimensions alone" end end context "with unsized images" do fab!(:upload) { Fabricate(:image_upload, width: 123, height: 456) } fab!(:post) { Fabricate(:post, user: user_with_auto_groups, raw: <<~HTML) } HTML let(:cpp) { CookedPostProcessor.new(post) } it "adds the width and height to images that don't have them" do cpp.post_process expect(cpp.html).to match(/width="123" height="456"/) expect(cpp).to be_dirty end end context "with small images" do fab!(:upload) { Fabricate(:image_upload, width: 150, height: 150) } fab!(:post) { Fabricate(:post, user: user_with_auto_groups, raw: <<~HTML) } HTML let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } before { SiteSetting.create_thumbnails = true } it "shows the lightbox when both dimensions are above the minimum" do cpp.post_process expect(cpp.html).to match(/