discourse/spec/components/cooked_post_processor_spec.rb

Failed to ignore revisions in .git-blame-ignore-revs.

1858 lines
67 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require "rails_helper"
2013-11-06 02:04:47 +08:00
require "cooked_post_processor"
require "file_store/s3_store"
2013-02-06 03:16:51 +08:00
def s3_setup
Rails.configuration.action_controller.stubs(:asset_host).returns("https://local.cdn.com")
SiteSetting.s3_upload_bucket = "some-bucket-on-s3"
SiteSetting.s3_access_key_id = "s3-access-key-id"
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
SiteSetting.s3_cdn_url = "https://s3.cdn.com"
SiteSetting.enable_s3_uploads = true
SiteSetting.authorized_extensions = "png|jpg|gif|mov|ogg|"
end
2013-02-06 03:16:51 +08:00
describe CookedPostProcessor do
fab!(:upload) { Fabricate(:upload) }
let(:upload_path) { Discourse.store.upload_path }
context "#post_process" do
fab!(:post) do
Fabricate(:post, raw: <<~RAW)
<img src="#{upload.url}">
RAW
end
2013-02-06 03:16:51 +08:00
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
2013-07-08 07:39:08 +08:00
let(:post_process) { sequence("post_process") }
2013-02-06 03:16:51 +08:00
2013-07-11 04:59:07 +08:00
it "post process in sequence" do
2013-07-08 07:39:08 +08:00
cpp.expects(:post_process_oneboxes).in_sequence(post_process)
cpp.expects(:post_process_images).in_sequence(post_process)
2013-11-06 02:04:47 +08:00
cpp.expects(:optimize_urls).in_sequence(post_process)
cpp.expects(:pull_hotlinked_images).in_sequence(post_process)
2013-07-08 07:39:08 +08:00
cpp.post_process
expect(PostUpload.exists?(post: post, upload: upload)).to eq(true)
2013-02-06 03:16:51 +08:00
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
<html>
<head>
<title>#{title}</title>
<meta property='og:title' content="#{title}">
<meta property='og:description' content="some description">
</head>
</html>
RAW
)
end
end
after do
InlineOneboxer.purge(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: described_class::INLINE_ONEBOX_CSS_CLASS
},
text: title,
count: 2
)
expect(cpp.html).to have_tag('aside.onebox a', text: title, count: 2)
expect(cpp.html).to have_tag('aside.onebox a',
text: url_hostname,
count: 2
)
expect(cpp.html).to have_tag('a',
without: {
class: described_class::INLINE_ONEBOX_LOADING_CSS_CLASS
},
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
let(:loading_css_class) do
described_class::INLINE_ONEBOX_LOADING_CSS_CLASS
end
before do
SiteSetting.enable_inline_onebox_on_all_domains = true
end
describe 'internal links' do
fab!(:topic) { Fabricate(:topic) }
fab!(:post) { Fabricate(:post, 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: loading_css_class
},
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: loading_css_class
},
text: topic.title,
count: 1
)
end
end
describe 'external links' do
let(:url_with_path) do
'https://meta.discourse.org/t/mini-inline-onebox-support-rfc/66400'
end
let(:url_with_query_param) do
'https://meta.discourse.org?a'
end
let(:url_no_path) do
'https://meta.discourse.org/'
end
let(:urls) do
[
url_with_path,
url_with_query_param,
url_no_path
]
end
let(:title) { '<b>some title</b>' }
let(:escaped_title) { CGI.escapeHTML(title) }
let(:post) do
Fabricate(:post, 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
end
let(:staff_post) do
Fabricate(:post, user: Fabricate(:admin), raw: <<~RAW)
This is a #{url_with_path} topic
RAW
end
before do
urls.each do |url|
stub_request(:get, url).to_return(
status: 200,
body: "<html><head><title>#{escaped_title}</title></head></html>"
)
end
end
after do
urls.each { |url| InlineOneboxer.purge(url) }
end
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: loading_css_class
},
text: title
)
expect(html).to have_tag('a',
with: {
href: url_with_path
},
without: {
class: loading_css_class
},
text: title,
count: 2
)
expect(html).to have_tag('a',
with: {
href: url_with_query_param
},
without: {
class: loading_css_class
},
text: title,
count: 1
)
expect(html).to have_tag("a[rel='nofollow noopener']")
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='nofollow noopener']")
end
end
end
context "processing images" do
before do
SiteSetting.responsive_post_image_sizes = ""
end
context "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: 10000)
post = Fabricate(:post, raw: "hello <img src='#{upload.url}'>")
# 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
)
# Fake a loading image
_optimized_image = OptimizedImage.create!(
url: "/#{upload_path}/10x10.png",
width: CookedPostProcessor::LOADING_SIZE,
height: CookedPostProcessor::LOADING_SIZE,
upload_id: upload.id,
sha1: SecureRandom.hex,
extension: '.png',
filesize: 123
)
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-small-upload="//test.localhost/#{upload_path}/10x10.png"|)
# 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-small-upload="//cdn.localhost/#{upload_path}/10x10.png"|)
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: 12345)
post = Fabricate(:post, raw: "hello <img src='#{upload.url}'>")
# 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
)
2013-07-08 07:39:08 +08:00
cpp = CookedPostProcessor.new(post)
cpp.add_to_size_cache(upload.url, 200, 4000)
cpp.post_process
2013-07-08 07:39:08 +08:00
expect(cpp.html).to_not include('srcset="')
end
end
2013-11-26 01:36:13 +08:00
shared_examples "leave dimensions alone" do
it "doesn't use them" do
expect(cpp.html).to match(/src="http:\/\/foo.bar\/image.png" width="" height=""/)
expect(cpp.html).to match(/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) }
let(:cpp) { CookedPostProcessor.new(post, image_sizes: image_sizes) }
before do
cpp.post_process
end
context "valid" do
let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 111, "height" => 222 } } }
2013-02-06 03:16:51 +08:00
it "uses them" do
expect(cpp.html).to match(/src="http:\/\/foo.bar\/image.png" width="111" height="222"/)
expect(cpp.html).to match(/src="http:\/\/domain.com\/picture.jpg" width="50" height="42"/)
expect(cpp).to be_dirty
end
end
2013-02-06 03:16:51 +08:00
context "invalid width" do
let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 0, "height" => 222 } } }
include_examples "leave dimensions alone"
end
context "invalid height" do
let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 111, "height" => 0 } } }
include_examples "leave dimensions alone"
end
2013-07-08 07:39:08 +08:00
context "invalid width & height" do
let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 0, "height" => 0 } } }
include_examples "leave dimensions alone"
end
2013-06-18 04:46:48 +08:00
end
context "with unsized images" do
fab!(:upload) { Fabricate(:image_upload, width: 123, height: 456) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}">
HTML
end
2013-06-18 04:46:48 +08:00
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
2013-02-06 03:16:51 +08:00
end
2014-05-26 19:17:20 +08:00
context "with large images" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}">
HTML
end
2017-11-15 03:37:27 +08:00
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
before do
SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true
end
it "generates overlay information" do
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
describe 'when image is inside onebox' do
let(:url) { 'https://image.com/my-avatar' }
let(:post) { Fabricate(:post, raw: url) }
before do
Oneboxer.stubs(:onebox).with(url, anything).returns("<img class='onebox' src='/#{upload_path}/original/1X/1234567890123456.jpg' />")
end
it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><img class="onebox" src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg" width="690" height="788"></p>
HTML
end
end
describe 'when image is an svg' do
fab!(:post) do
Fabricate(:post, raw: "<img src=\"/#{Discourse.store.upload_path}/original/1X/1234567890123456.svg\">")
end
it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.svg" width="690" height="788"></p>
HTML
end
describe 'when image src is an URL' do
let(:post) do
Fabricate(:post, raw: "<img src=\"http://test.discourse/#{upload_path}/original/1X/1234567890123456.svg?somepamas\">")
end
it 'should not add lightbox' do
FastImage.expects(:size).returns([1750, 2000])
SiteSetting.crawl_images = true
cpp.post_process
expect(cpp.html).to match_html("<p><img src=\"http://test.discourse/#{upload_path}/original/1X/1234567890123456.svg?somepamas\" width=\"690\"\ height=\"788\"></p>")
end
end
end
context "s3_uploads" do
let(:upload) { Fabricate(:secure_upload_s3) }
before do
s3_setup
stored_path = Discourse.store.get_path_for_upload(upload)
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
stub_request(
:put,
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/optimized/1X/#{upload.sha1}_2_#{optimized_size}.#{upload.extension}"
)
stub_request(:get, /#{SiteSetting.s3_upload_bucket}\.s3\.amazonaws\.com/)
SiteSetting.login_required = true
SiteSetting.secure_media = true
end
let(:optimized_size) { "600x500" }
let(:post) do
Fabricate(:post, raw: "![large.png|#{optimized_size}](#{upload.short_url})")
end
let(:cooked_html) do
<<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost/secure-media-uploads/original/1X/#{upload.sha1}.png" data-download-href="//test.localhost/uploads/short-url/#{upload.base62_sha1}.unknown?dl=1" title="large.png"><img src="" alt="large.png" data-base62-sha1="#{upload.base62_sha1}" width="600" height="500"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">large.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
end
context "when the upload is attached to the correct post" do
before do
FastImage.expects(:size).returns([1750, 2000])
OptimizedImage.expects(:resize).returns(true)
Discourse.store.class.any_instance.expects(:has_been_uploaded?).at_least_once.returns(true)
upload.update(secure: true, access_control_post: post)
end
# TODO fix this spec, it is sometimes getting CDN links when it runs concurrently
xit "handles secure images with the correct lightbox link href" do
cpp.post_process
expect(cpp.html).to match_html cooked_html
end
end
context "when the upload is attached to a different post" do
before do
FastImage.size(upload.url)
upload.update(secure: true, access_control_post: Fabricate(:post))
end
it "does not create thumbnails or optimize images" do
CookedPostProcessor.any_instance.expects(:optimize_image!).never
Upload.any_instance.expects(:create_thumbnail!).never
cpp.post_process
expect(cpp.html).not_to match_html cooked_html
end
end
end
end
context "with tall images" do
fab!(:upload) { Fabricate(:image_upload, width: 860, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}">
HTML
end
2017-11-15 03:37:27 +08:00
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
2017-11-15 03:37:27 +08:00
before do
SiteSetting.create_thumbnails = true
end
2017-11-15 03:37:27 +08:00
it "crops the image" do
cpp.post_process
2017-11-15 03:37:27 +08:00
expect(cpp.html).to match(/width="690" height="500">/)
expect(cpp).to be_dirty
end
2017-11-15 03:37:27 +08:00
end
context "with iPhone X screenshots" do
fab!(:upload) { Fabricate(:image_upload, width: 1125, height: 2436) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}">
HTML
end
2017-11-15 03:37:27 +08:00
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
2017-11-15 03:37:27 +08:00
before do
SiteSetting.create_thumbnails = true
end
2017-11-15 03:37:27 +08:00
it "crops the image" do
cpp.post_process
2017-11-15 03:37:27 +08:00
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_230x500.png" width="230" height="500"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1125×2436 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
2013-02-06 03:16:51 +08:00
end
context "with large images when using subfolders" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="/subfolder#{upload.url}">
HTML
end
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
before do
set_subfolder "/subfolder"
stub_request(:get, "http://#{Discourse.current_hostname}/subfolder#{upload.url}").to_return(status: 200, body: File.new(Discourse.store.path_for(upload)))
SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true
end
it "generates overlay information" do
cpp.post_process
expect(cpp.html). to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost/subfolder#{upload.url}" data-download-href="//test.localhost/subfolder/#{upload_path}/#{upload.sha1}" title="logo.png"><img src="//test.localhost/subfolder/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">logo.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
it "should escape the filename" do
upload.update!(original_filename: "><img src=x onerror=alert('haha')>.png")
cpp.post_process
2013-02-06 03:16:51 +08:00
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost/subfolder#{upload.url}" data-download-href="//test.localhost/subfolder/#{upload_path}/#{upload.sha1}" title="&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png"><img src="//test.localhost/subfolder/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
end
2016-08-11 11:27:12 +08:00
end
context "with title and alt" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" title="WAT" alt="RED">
HTML
end
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
before do
SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true
end
it "generates overlay information using image title and ignores alt" do
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="WAT"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" title="WAT" alt="RED" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">WAT</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
end
context "with title only" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" title="WAT">
HTML
end
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
before do
SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true
end
it "generates overlay information using image title" do
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="WAT"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" title="WAT" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">WAT</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
end
context "with alt only" do
fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) }
fab!(:post) do
Fabricate(:post, raw: <<~HTML)
<img src="#{upload.url}" alt="RED">
HTML
end
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
before do
SiteSetting.max_image_height = 2000
SiteSetting.create_thumbnails = true
end
it "generates overlay information using image alt" do
cpp.post_process
expect(cpp.html).to match_html <<~HTML
<p><div class="lightbox-wrapper"><a class="lightbox" href="//test.localhost#{upload.url}" data-download-href="//test.localhost/#{upload_path}/#{upload.sha1}" title="RED"><img src="//test.localhost/#{upload_path}/optimized/1X/#{upload.sha1}_#{OptimizedImage::VERSION}_690x788.png" alt="RED" width="690" height="788"><div class="meta">
<svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use xlink:href="#far-image"></use></svg><span class="filename">RED</span><span class="informations">1750×2000 1.21 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use xlink:href="#discourse-expand"></use></svg>
</div></a></div></p>
HTML
expect(cpp).to be_dirty
end
end
context "topic image" do
let(:post) { Fabricate(:post_with_uploaded_image) }
let(:cpp) { CookedPostProcessor.new(post) }
it "adds a topic image if there's one in the first post" do
FastImage.stubs(:size)
expect(post.topic.image_url).to eq(nil)
cpp.post_process
post.topic.reload
expect(post.topic.image_url).to be_present
end
it "removes image if post is edited and no longer has an image" do
FastImage.stubs(:size)
cpp.post_process
post.topic.reload
expect(post.topic.image_url).to be_present
expect(post.image_url).to be_present
post.update!(raw: "This post no longer has an image.")
CookedPostProcessor.new(post).post_process
post.topic.reload
expect(post.topic.image_url).not_to be_present
expect(post.image_url).not_to be_present
end
it "won't remove the original image if another post doesn't have an image" do
FastImage.stubs(:size)
topic = post.topic
cpp.post_process
topic.reload
expect(topic.image_url).to be_present
expect(post.image_url).to be_present
post = Fabricate(:post, topic: topic, raw: "this post doesn't have an image")
CookedPostProcessor.new(post).post_process
topic.reload
expect(post.topic.image_url).to be_present
expect(post.image_url).to be_blank
end
end
context "post image" do
let(:reply) { Fabricate(:post_with_uploaded_image, post_number: 2) }
let(:cpp) { CookedPostProcessor.new(reply) }
2013-07-08 07:39:08 +08:00
it "adds a post image if there's one in the post" do
FastImage.stubs(:size)
expect(reply.image_url).to eq(nil)
cpp.post_process
reply.reload
expect(reply.image_url).to be_present
end
end
end
2013-07-08 07:39:08 +08:00
end
2013-06-24 08:10:21 +08:00
context "#extract_images" do
2013-06-24 08:10:21 +08:00
2015-08-05 18:57:31 +08:00
let(:post) { build(:post_with_plenty_of_images) }
2013-11-06 02:04:47 +08:00
let(:cpp) { CookedPostProcessor.new(post) }
2013-07-08 07:39:08 +08:00
2015-08-05 18:57:31 +08:00
it "does not extract emojis or images inside oneboxes or quotes" do
2015-01-10 00:34:37 +08:00
expect(cpp.extract_images.length).to eq(0)
2013-06-24 08:10:21 +08:00
end
2013-11-06 02:04:47 +08:00
end
context "#get_size_from_attributes" do
let(:post) { build(:post) }
let(:cpp) { CookedPostProcessor.new(post) }
it "returns the size when width and height are specified" do
2017-07-28 09:20:09 +08:00
img = { 'src' => 'http://foo.bar/image3.png', 'width' => 50, 'height' => 70 }
expect(cpp.get_size_from_attributes(img)).to eq([50, 70])
end
it "returns the size when width and height are floats" do
2017-07-28 09:20:09 +08:00
img = { 'src' => 'http://foo.bar/image3.png', 'width' => 50.2, 'height' => 70.1 }
expect(cpp.get_size_from_attributes(img)).to eq([50, 70])
end
it "resizes when only width is specified" do
2017-07-28 09:20:09 +08:00
img = { 'src' => 'http://foo.bar/image3.png', 'width' => 100 }
SiteSetting.crawl_images = true
FastImage.expects(:size).returns([200, 400])
expect(cpp.get_size_from_attributes(img)).to eq([100, 200])
end
it "resizes when only height is specified" do
2017-07-28 09:20:09 +08:00
img = { 'src' => 'http://foo.bar/image3.png', 'height' => 100 }
SiteSetting.crawl_images = true
FastImage.expects(:size).returns([100, 300])
expect(cpp.get_size_from_attributes(img)).to eq([33, 100])
end
it "doesn't raise an error with a weird url" do
2017-07-28 09:20:09 +08:00
img = { 'src' => nil, 'height' => 100 }
SiteSetting.crawl_images = true
expect(cpp.get_size_from_attributes(img)).to be_nil
end
end
context "#get_size_from_image_sizes" do
2013-11-06 02:04:47 +08:00
let(:post) { build(:post) }
let(:cpp) { CookedPostProcessor.new(post) }
it "returns the size" do
image_sizes = { "http://my.discourse.org/image.png" => { "width" => 111, "height" => 222 } }
2015-01-10 00:34:37 +08:00
expect(cpp.get_size_from_image_sizes("/image.png", image_sizes)).to eq([111, 222])
2013-07-08 07:39:08 +08:00
end
2013-06-26 08:44:20 +08:00
2013-11-06 02:04:47 +08:00
end
context "#get_size" do
2013-11-06 02:04:47 +08:00
let(:post) { build(:post) }
let(:cpp) { CookedPostProcessor.new(post) }
it "ensures urls are absolute" do
cpp.expects(:is_valid_image_url?).with("http://test.localhost/relative/url/image.png")
cpp.get_size("/relative/url/image.png")
end
it "ensures urls have a default scheme" do
cpp.expects(:is_valid_image_url?).with("http://schemaless.url/image.jpg")
cpp.get_size("//schemaless.url/image.jpg")
end
it "caches the results" do
SiteSetting.crawl_images = true
2013-11-06 02:04:47 +08:00
FastImage.expects(:size).returns([200, 400])
cpp.get_size("http://foo.bar/image3.png")
2015-01-10 00:34:37 +08:00
expect(cpp.get_size("http://foo.bar/image3.png")).to eq([200, 400])
2013-11-06 02:04:47 +08:00
end
context "when crawl_images is disabled" do
before do
SiteSetting.crawl_images = false
end
2013-11-06 02:04:47 +08:00
it "doesn't call FastImage" do
FastImage.expects(:size).never
2015-01-10 00:34:37 +08:00
expect(cpp.get_size("http://foo.bar/image1.png")).to eq(nil)
2013-11-06 02:04:47 +08:00
end
it "is always allowed to crawl our own images" do
store = stub
Discourse.expects(:store).returns(store).at_least_once
2013-11-06 02:04:47 +08:00
store.expects(:has_been_uploaded?).returns(true)
FastImage.expects(:size).returns([100, 200])
2015-01-10 00:34:37 +08:00
expect(cpp.get_size("http://foo.bar/image2.png")).to eq([100, 200])
2013-11-06 02:04:47 +08:00
end
it "returns nil if FastImage can't get the original size" do
Discourse.store.class.any_instance.expects(:has_been_uploaded?).returns(true)
FastImage.expects(:size).returns(nil)
expect(cpp.get_size("http://foo.bar/image3.png")).to eq(nil)
end
2013-07-08 07:39:08 +08:00
end
2013-06-26 08:44:20 +08:00
2013-07-08 07:39:08 +08:00
end
2013-06-26 08:44:20 +08:00
context "#is_valid_image_url?" do
2013-11-06 02:04:47 +08:00
let(:post) { build(:post) }
let(:cpp) { CookedPostProcessor.new(post) }
it "validates HTTP(s) urls" do
2015-01-10 00:34:37 +08:00
expect(cpp.is_valid_image_url?("http://domain.com")).to eq(true)
expect(cpp.is_valid_image_url?("https://domain.com")).to eq(true)
2013-11-06 02:04:47 +08:00
end
it "doesn't validate other urls" do
2015-01-10 00:34:37 +08:00
expect(cpp.is_valid_image_url?("ftp://domain.com")).to eq(false)
expect(cpp.is_valid_image_url?("ftps://domain.com")).to eq(false)
expect(cpp.is_valid_image_url?("/tmp/image.png")).to eq(false)
expect(cpp.is_valid_image_url?("//domain.com")).to eq(false)
2013-11-06 02:04:47 +08:00
end
it "doesn't throw an exception with a bad URI" do
2015-01-10 00:34:37 +08:00
expect(cpp.is_valid_image_url?("http://do<main.com")).to eq(nil)
2013-11-06 02:04:47 +08:00
end
end
context "#get_filename" do
2013-06-26 08:44:20 +08:00
2013-07-08 07:39:08 +08:00
let(:post) { build(:post) }
let(:cpp) { CookedPostProcessor.new(post) }
it "returns the filename of the src when there is no upload" do
2015-01-10 00:34:37 +08:00
expect(cpp.get_filename(nil, "http://domain.com/image.png")).to eq("image.png")
2013-06-26 08:44:20 +08:00
end
2013-07-08 07:39:08 +08:00
it "returns the original filename of the upload when there is an upload" do
2017-07-28 09:20:09 +08:00
upload = build(:upload, original_filename: "upload.jpg")
2015-01-10 00:34:37 +08:00
expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq("upload.jpg")
2013-07-08 07:39:08 +08:00
end
2013-02-06 03:16:51 +08:00
2013-07-08 07:39:08 +08:00
it "returns a generic name for pasted images" do
2017-07-28 09:20:09 +08:00
upload = build(:upload, original_filename: "blob.png")
2015-01-10 00:34:37 +08:00
expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq(I18n.t('upload.pasted_image_filename'))
2013-02-19 14:57:14 +08:00
end
2013-02-26 00:42:20 +08:00
2013-07-08 07:39:08 +08:00
end
context "#convert_to_link" do
fab!(:thumbnail) { Fabricate(:optimized_image, upload: upload, width: 512, height: 384) }
before do
CookedPostProcessor.any_instance.stubs(:get_size).with(upload.url).returns([1024, 768])
end
it "adds lightbox and optimizes images" do
post = Fabricate(:post, raw: "![image|1024x768, 50%](#{upload.short_url})")
cpp = CookedPostProcessor.new(post, disable_loading_image: true)
cpp.post_process
doc = Nokogiri::HTML::fragment(cpp.html)
expect(doc.css('.lightbox-wrapper').size).to eq(1)
expect(doc.css('img').first['srcset']).to_not eq(nil)
end
it "optimizes images in quotes" do
post = Fabricate(:post, raw: <<~MD)
[quote]
![image|1024x768, 50%](#{upload.short_url})
[/quote]
MD
cpp = CookedPostProcessor.new(post, disable_loading_image: true)
cpp.post_process
doc = Nokogiri::HTML::fragment(cpp.html)
expect(doc.css('.lightbox-wrapper').size).to eq(0)
expect(doc.css('img').first['srcset']).to_not eq(nil)
end
it "optimizes images in Onebox" do
Oneboxer.expects(:onebox)
.with("https://discourse.org", anything)
.returns("<aside class='onebox'><img src='#{upload.url}' width='512' height='384'></aside>")
post = Fabricate(:post, raw: "https://discourse.org")
cpp = CookedPostProcessor.new(post, disable_loading_image: true)
cpp.post_process
doc = Nokogiri::HTML::fragment(cpp.html)
expect(doc.css('.lightbox-wrapper').size).to eq(0)
expect(doc.css('img').first['srcset']).to_not eq(nil)
end
end
context "#post_process_oneboxes" do
2013-11-06 02:04:47 +08:00
let(:post) { build(:post_with_youtube, id: 123) }
let(:cpp) { CookedPostProcessor.new(post, invalidate_oneboxes: true) }
2013-11-06 02:04:47 +08:00
before do
Oneboxer
.expects(:onebox)
.with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id)
2017-07-28 09:20:09 +08:00
.returns("<div>GANGNAM STYLE</div>")
2013-11-06 02:04:47 +08:00
cpp.post_process_oneboxes
2013-02-06 03:16:51 +08:00
end
it "inserts the onebox without wrapping p" do
2015-01-10 00:34:37 +08:00
expect(cpp).to be_dirty
expect(cpp.html).to match_html "<div>GANGNAM STYLE</div>"
2013-11-06 02:04:47 +08:00
end
describe "replacing downloaded onebox image" do
let(:url) { 'https://image.com/my-avatar' }
let(:image_url) { 'https://image.com/avatar.png' }
it "successfully replaces the image" do
Oneboxer.stubs(:onebox).with(url, anything).returns("<img class='onebox' src='#{image_url}' />")
post = Fabricate(:post, raw: url)
upload.update!(url: "https://test.s3.amazonaws.com/something.png")
post.custom_fields[Post::DOWNLOADED_IMAGES] = { "//image.com/avatar.png": upload.id }
post.save_custom_fields
cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true)
cpp.post_process_oneboxes
expect(cpp.doc.to_s).to eq("<p><img class=\"onebox\" src=\"#{upload.url}\" width=\"\" height=\"\"></p>")
upload.destroy!
cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true)
cpp.post_process_oneboxes
expect(cpp.doc.to_s).to eq("<p><img class=\"onebox\" src=\"#{image_url}\" width=\"\" height=\"\"></p>")
Oneboxer.unstub(:onebox)
end
context "when the post is with_secure_media and the upload is secure and secure media is enabled" do
before do
upload.update(secure: true)
SiteSetting.login_required = true
s3_setup
SiteSetting.secure_media = true
stub_request(:head, "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/")
end
it "does not use the direct URL, uses the cooked URL instead (because of the private ACL preventing w/h fetch)" do
Oneboxer.stubs(:onebox).with(url, anything).returns("<img class='onebox' src='#{image_url}' />")
post = Fabricate(:post, raw: url)
upload.update!(url: "https://test.s3.amazonaws.com/something.png")
post.custom_fields[Post::DOWNLOADED_IMAGES] = { "//image.com/avatar.png": upload.id }
post.save_custom_fields
cooked_url = "https://localhost/secure-media-uploads/test.png"
UrlHelper.expects(:cook_url).with(upload.url, secure: true).returns(cooked_url)
cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true)
cpp.post_process_oneboxes
expect(cpp.doc.to_s).to eq("<p><img class=\"onebox\" src=\"#{cooked_url}\" width=\"\" height=\"\"></p>")
end
end
end
it "replaces large image placeholder" do
url = 'https://image.com/my-avatar'
image_url = 'https://image.com/avatar.png'
Oneboxer.stubs(:onebox).with(url, anything).returns("<img class='onebox' src='#{image_url}' />")
post = Fabricate(:post, raw: url)
post.custom_fields[Post::LARGE_IMAGES] = "[\"//image.com/avatar.png\"]"
post.save_custom_fields
cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true)
cpp.post_process
expect(cpp.doc.to_s).to match(/<div class="large-image-placeholder">/)
end
end
2013-02-06 03:16:51 +08:00
context "#post_process_oneboxes removes nofollow if add_rel_nofollow_to_user_content is disabled" do
let(:post) { build(:post_with_youtube, id: 123) }
let(:cpp) { CookedPostProcessor.new(post, invalidate_oneboxes: true) }
before do
SiteSetting.add_rel_nofollow_to_user_content = false
Oneboxer.expects(:onebox)
.with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id)
.returns('<aside class="onebox"><a href="https://www.youtube.com/watch?v=9bZkp7q19f0" rel="nofollow noopener">GANGNAM STYLE</a></aside>')
cpp.post_process_oneboxes
end
it "removes nofollow noopener from links" do
expect(cpp).to be_dirty
expect(cpp.html).to match_html '<aside class="onebox"><a href="https://www.youtube.com/watch?v=9bZkp7q19f0">GANGNAM STYLE</a></aside>'
end
end
context "#post_process_oneboxes removes nofollow if user is tl3" do
let(:post) { build(:post_with_youtube, id: 123) }
let(:cpp) { CookedPostProcessor.new(post, invalidate_oneboxes: true) }
before do
post.user.trust_level = TrustLevel[3]
post.user.save!
SiteSetting.add_rel_nofollow_to_user_content = true
SiteSetting.tl3_links_no_follow = false
Oneboxer.expects(:onebox)
.with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id)
.returns('<aside class="onebox"><a href="https://www.youtube.com/watch?v=9bZkp7q19f0" rel="nofollow noopener">GANGNAM STYLE</a></aside>')
cpp.post_process_oneboxes
end
it "removes nofollow noopener from links" do
expect(cpp).to be_dirty
expect(cpp.html).to match_html '<aside class="onebox"><a href="https://www.youtube.com/watch?v=9bZkp7q19f0">GANGNAM STYLE</a></aside>'
end
end
context "#post_process_oneboxes with oneboxed image" do
let(:post) { build(:post_with_youtube, id: 123) }
let(:cpp) { CookedPostProcessor.new(post, invalidate_oneboxes: true) }
it "applies aspect ratio to container" do
Oneboxer.expects(:onebox)
.with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id)
.returns("<aside class='onebox'><div class='scale-images'><img src='/img.jpg' width='400' height='500'/></div></div>")
cpp.post_process_oneboxes
expect(cpp.html).to match_html('<aside class="onebox"><div class="aspect-image-full-size" style="--aspect-ratio:400/500;"><img src="/img.jpg"></div></aside>')
end
it "applies aspect ratio when wrapped in link" do
Oneboxer.expects(:onebox)
.with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id)
.returns("<aside class='onebox'><div class='scale-images'><a href='https://example.com'><img src='/img.jpg' width='400' height='500'/></a></div></div>")
cpp.post_process_oneboxes
expect(cpp.html).to match_html('<aside class="onebox"><div class="aspect-image-full-size" style="--aspect-ratio:400/500;"><a href="https://example.com"><img src="/img.jpg"></a></div></aside>')
end
end
context "#post_process_oneboxes with square image" do
it "generates a onebox-avatar class" do
SiteSetting.crawl_images = true
url = 'https://square-image.com/onebox'
body = <<~HTML
<html>
<head>
<meta property='og:title' content="Page awesome">
<meta property='og:image' content="https://image.com/avatar.png">
<meta property='og:description' content="Page awesome desc">
</head>
</html>
HTML
stub_request(:head, url)
stub_request(:get , url).to_return(body: body)
FinalDestination.stubs(:lookup_ip).returns('1.2.3.4')
# not an ideal stub but shipping the whole image to fast image can add
# a lot of cost to this test
FastImage.stubs(:size).returns([200, 200])
post = Fabricate.build(:post, raw: url)
cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true)
cpp.post_process_oneboxes
expect(cpp.doc.to_s).not_to include('aspect-image')
expect(cpp.doc.to_s).to include('onebox-avatar')
2013-11-06 02:04:47 +08:00
end
2013-11-06 02:04:47 +08:00
end
2013-02-06 03:16:51 +08:00
context "#optimize_urls" do
2013-07-08 07:39:08 +08:00
2013-11-06 02:04:47 +08:00
let(:post) { build(:post_with_uploads_and_links) }
let(:cpp) { CookedPostProcessor.new(post) }
it "uses schemaless url for uploads" do
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p><a href="//test.localhost/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
<img src="//test.localhost/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="nofollow noopener">Google</a><br>
<img src="http://foo.bar/image.png"><br>
<a class="attachment" href="//test.localhost/#{upload_path}/original/1X/af2c2618032c679333bebf745e75f9088748d737.txt">text.txt</a> (20 Bytes)<br>
<img src="//test.localhost/images/emoji/twitter/smile.png?v=#{Emoji::EMOJI_VERSION}" title=":smile:" class="emoji only-emoji" alt=":smile:"></p>
HTML
2013-07-08 07:39:08 +08:00
end
2013-11-06 02:04:47 +08:00
context "when CDN is enabled" do
it "uses schemaless CDN url for http uploads" do
2013-11-06 02:04:47 +08:00
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="nofollow noopener">Google</a><br>
<img src="http://foo.bar/image.png"><br>
<a class="attachment" href="//my.cdn.com/#{upload_path}/original/1X/af2c2618032c679333bebf745e75f9088748d737.txt">text.txt</a> (20 Bytes)<br>
<img src="//my.cdn.com/images/emoji/twitter/smile.png?v=#{Emoji::EMOJI_VERSION}" title=":smile:" class="emoji only-emoji" alt=":smile:"></p>
HTML
2013-11-06 02:04:47 +08:00
end
it "doesn't use schemaless CDN url for https uploads" do
Rails.configuration.action_controller.stubs(:asset_host).returns("https://my.cdn.com")
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p><a href="https://my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
<img src="https://my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="nofollow noopener">Google</a><br>
<img src="http://foo.bar/image.png"><br>
<a class="attachment" href="https://my.cdn.com/#{upload_path}/original/1X/af2c2618032c679333bebf745e75f9088748d737.txt">text.txt</a> (20 Bytes)<br>
<img src="https://my.cdn.com/images/emoji/twitter/smile.png?v=#{Emoji::EMOJI_VERSION}" title=":smile:" class="emoji only-emoji" alt=":smile:"></p>
HTML
end
it "doesn't use CDN when login is required" do
SiteSetting.login_required = true
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="nofollow noopener">Google</a><br>
<img src="http://foo.bar/image.png"><br>
<a class="attachment" href="//test.localhost/#{upload_path}/original/1X/af2c2618032c679333bebf745e75f9088748d737.txt">text.txt</a> (20 Bytes)<br>
<img src="//my.cdn.com/images/emoji/twitter/smile.png?v=#{Emoji::EMOJI_VERSION}" title=":smile:" class="emoji only-emoji" alt=":smile:"></p>
HTML
end
it "doesn't use CDN when preventing anons from downloading files" do
SiteSetting.prevent_anons_from_downloading_files = true
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p><a href="//my.cdn.com/#{upload_path}/original/2X/2345678901234567.jpg">Link</a><br>
<img src="//my.cdn.com/#{upload_path}/original/1X/1234567890123456.jpg"><br>
<a href="http://www.google.com" rel="nofollow noopener">Google</a><br>
<img src="http://foo.bar/image.png"><br>
<a class="attachment" href="//test.localhost/#{upload_path}/original/1X/af2c2618032c679333bebf745e75f9088748d737.txt">text.txt</a> (20 Bytes)<br>
<img src="//my.cdn.com/images/emoji/twitter/smile.png?v=#{Emoji::EMOJI_VERSION}" title=":smile:" class="emoji only-emoji" alt=":smile:"></p>
HTML
end
context "s3_uploads" do
before do
s3_setup
uploaded_file = file_from_fixtures("smallest.png")
upload_sha1 = Digest::SHA1.hexdigest(File.read(uploaded_file))
upload.update!(
original_filename: "smallest.png",
width: 10,
height: 20,
sha1: upload_sha1,
extension: "png",
)
end
it "uses the right CDN when uploads are on S3" do
stored_path = Discourse.store.get_path_for_upload(upload)
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
the_post = Fabricate(:post, raw: %Q{This post has a local emoji :+1: and an external upload\n\n![smallest.png|10x20](#{upload.short_url})})
cpp = CookedPostProcessor.new(the_post)
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
<p><img src="https://s3.cdn.com/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
HTML
end
it "doesn't use CDN for secure media" do
SiteSetting.secure_media = true
stored_path = Discourse.store.get_path_for_upload(upload)
upload.update_column(:url, "#{SiteSetting.Upload.absolute_base_url}/#{stored_path}")
upload.update_column(:secure, true)
the_post = Fabricate(:post, raw: %Q{This post has a local emoji :+1: and an external upload\n\n![smallest.png|10x20](#{upload.short_url})})
cpp = CookedPostProcessor.new(the_post)
cpp.optimize_urls
expect(cpp.html).to match_html <<~HTML
<p>This post has a local emoji <img src="https://local.cdn.com/images/emoji/twitter/+1.png?v=#{Emoji::EMOJI_VERSION}" title=":+1:" class="emoji" alt=":+1:"> and an external upload</p>
<p><img src="/secure-media-uploads/#{stored_path}" alt="smallest.png" data-base62-sha1="#{upload.base62_sha1}" width="10" height="20"></p>
HTML
end
context "media uploads" do
fab!(:image_upload) { Fabricate(:upload) }
fab!(:audio_upload) { Fabricate(:upload, extension: "ogg") }
fab!(:video_upload) { Fabricate(:upload, extension: "mov") }
before do
video_upload.update!(url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(video_upload)}")
stub_request(:head, video_upload.url)
end
it "ignores prevent_anons_from_downloading_files and oneboxes video uploads" do
SiteSetting.prevent_anons_from_downloading_files = true
the_post = Fabricate(:post, raw: "This post has an S3 video onebox:\n#{video_upload.url}")
cpp = CookedPostProcessor.new(the_post)
cpp.post_process_oneboxes
expect(cpp.html).to match_html <<~HTML
<p>This post has an S3 video onebox:<br></p>
<div class="onebox video-onebox">
<video width="100%" height="100%" controls="">
<source src="#{video_upload.url}">
<a href="#{video_upload.url}" rel="nofollow ugc noopener">#{video_upload.url}</a>
</source>
</video>
</div>
HTML
end
it "oneboxes video using secure url when secure_media is enabled" do
SiteSetting.login_required = true
SiteSetting.secure_media = true
video_upload.update_column(:secure, true)
the_post = Fabricate(:post, raw: "This post has an S3 video onebox:\n#{video_upload.url}")
cpp = CookedPostProcessor.new(the_post)
cpp.post_process_oneboxes
secure_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
expect(cpp.html).to match_html <<~HTML
<p>This post has an S3 video onebox:<br>
<div class="onebox video-onebox">
<video width="100%" height="100%" controls="">
<source src="#{secure_url}">
<a href="#{secure_url}">#{secure_url}</a>
</source>
</video>
</div>
</p>
HTML
end
it "oneboxes only audio/video and not images when secure_media is enabled" do
SiteSetting.login_required = true
SiteSetting.secure_media = true
video_upload.update_column(:secure, true)
audio_upload.update!(
url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(audio_upload)}",
secure: true
)
image_upload.update!(
url: "#{SiteSetting.s3_cdn_url}/#{Discourse.store.get_path_for_upload(image_upload)}",
secure: true
)
stub_request(:head, audio_upload.url)
stub_request(:get, image_upload.url)
raw = <<~RAW
This post has a video upload.
#{video_upload.url}
This post has an audio upload.
#{audio_upload.url}
And an image upload.
![logo.png](upload://#{image_upload.base62_sha1}.#{image_upload.extension})
RAW
the_post = Fabricate(:post, raw: raw)
cpp = CookedPostProcessor.new(the_post)
cpp.post_process_oneboxes
secure_video_url = video_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
secure_audio_url = audio_upload.url.sub(SiteSetting.s3_cdn_url, "#{Discourse.base_url}/secure-media-uploads")
expect(cpp.html).to match_html <<~HTML
<p>This post has a video upload.<br></p>
<div class="onebox video-onebox">
<video width="100%" height="100%" controls="">
<source src="#{secure_video_url}">
<a href="#{secure_video_url}">#{secure_video_url}</a>
</source>
</video>
</div>
<p>This post has an audio upload.<br>
<audio controls><source src="#{secure_audio_url}"><a href="#{secure_audio_url}">#{secure_audio_url}</a></source></audio>
</p>
<p>And an image upload.<br>
<img src="#{image_upload.url}" alt="#{image_upload.original_filename}" data-base62-sha1="#{image_upload.base62_sha1}"></p>
HTML
end
end
end
2013-02-06 03:16:51 +08:00
end
2013-02-06 03:16:51 +08:00
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) }
2013-07-08 07:39:08 +08:00
let(:cpp) { CookedPostProcessor.new(post) }
2013-11-15 23:46:41 +08:00
before { cpp.stubs(:available_disk_space).returns(90) }
it "runs even when download_remote_images_to_local is disabled" do
# We want to run it to pull hotlinked optimized images
SiteSetting.download_remote_images_to_local = false
expect { cpp.pull_hotlinked_images }.
to change { Jobs::PullHotlinkedImages.jobs.count }.by 1
end
context "when download_remote_images_to_local? is enabled" do
before do
SiteSetting.download_remote_images_to_local = true
end
2013-11-06 02:04:47 +08:00
it "disables download_remote_images if there is not enough disk space" do
cpp.expects(:available_disk_space).returns(5)
2013-11-06 02:04:47 +08:00
cpp.pull_hotlinked_images
expect(SiteSetting.download_remote_images_to_local).to eq(false)
2013-11-06 02:04:47 +08:00
end
context "and there is enough disk space" do
2013-11-15 23:46:41 +08:00
before { cpp.expects(:disable_if_low_on_disk_space) }
2013-11-06 02:04:47 +08:00
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
2013-11-06 02:04:47 +08:00
context "and the post has been updated by an actual user" do
2013-11-06 02:04:47 +08:00
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).once
cpp.pull_hotlinked_images
end
2013-11-06 02:04:47 +08:00
end
end
2013-06-25 04:56:03 +08:00
end
end
context "#disable_if_low_on_disk_space" do
2013-11-15 23:46:41 +08:00
let(:post) { build(:post, created_at: 20.days.ago) }
2013-11-15 23:46:41 +08:00
let(:cpp) { CookedPostProcessor.new(post) }
before do
SiteSetting.download_remote_images_to_local = true
SiteSetting.download_remote_images_threshold = 20
cpp.stubs(:available_disk_space).returns(50)
end
2013-11-15 23:46:41 +08:00
it "does nothing when there's enough disk space" do
SiteSetting.expects(:download_remote_images_to_local=).never
cpp.disable_if_low_on_disk_space
2013-11-15 23:46:41 +08:00
end
context "when there's not enough disk space" do
before { SiteSetting.download_remote_images_threshold = 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
cpp.disable_if_low_on_disk_space
2015-01-10 00:34:37 +08:00
expect(SiteSetting.download_remote_images_to_local).to eq(false)
end
it "doesn't disable download_remote_images_to_local if site uses S3" do
SiteSetting.s3_access_key_id = "s3-access-key-id"
SiteSetting.s3_secret_access_key = "s3-secret-access-key"
SiteSetting.enable_s3_uploads = true
cpp.disable_if_low_on_disk_space
expect(SiteSetting.download_remote_images_to_local).to eq(true)
end
2013-11-15 23:46:41 +08:00
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).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('<body><div><a><img id="linked_image"></a><p><img id="standard_image"></p></div></body>') }
it "is true when the image is inside a link" do
img = doc.css("img#linked_image").first
2015-01-10 00:34:37 +08:00
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
2015-01-10 00:34:37 +08:00
expect(cpp.is_a_hyperlink?(img)).to eq(false)
end
end
context "grant badges" do
let(:cpp) { CookedPostProcessor.new(post) }
2016-04-13 02:09:59 +08:00
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
2016-04-13 02:09:59 +08:00
context "onebox" do
before do
Oneboxer.stubs(:onebox).with(anything, anything).returns(nil)
Oneboxer.stubs(:onebox).with('https://discourse.org', anything).returns("<aside class=\"onebox whitelistedgeneric\">the rest of the onebox</aside>")
end
2016-04-13 02:09:59 +08:00
it "awards the badge for using an onebox" do
post = Fabricate(:post, raw: "onebox me:\n\nhttps://discourse.org\n")
cpp = CookedPostProcessor.new(post)
2016-04-13 02:09:59 +08:00
cpp.post_process_oneboxes
cpp.grant_badges
expect(post.user.user_badges.where(badge_id: Badge::FirstOnebox).exists?).to eq(true)
end
it "does not award the badge when link is not oneboxed" do
post = Fabricate(:post, raw: "onebox me:\n\nhttp://example.com\n")
cpp = CookedPostProcessor.new(post)
cpp.post_process_oneboxes
cpp.grant_badges
expect(post.user.user_badges.where(badge_id: Badge::FirstOnebox).exists?).to eq(false)
end
it "does not award the badge when the badge is disabled" do
Badge.where(id: Badge::FirstOnebox).update_all(enabled: false)
post = Fabricate(:post, raw: "onebox me:\n\nhttps://discourse.org\n")
cpp = CookedPostProcessor.new(post)
cpp.post_process_oneboxes
cpp.grant_badges
expect(post.user.user_badges.where(badge_id: Badge::FirstOnebox).exists?).to eq(false)
end
2016-04-13 02:09:59 +08:00
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 "full quote on direct reply" do
fab!(: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
let(:raw3) do
<<~RAW.strip
[quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"]
this is the first post
[/quote]
[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
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 do
topic.bumped_at = 1.day.ago
CookedPostProcessor.new(reply).remove_full_quote_on_direct_reply
2019-04-08 21:30:04 +08:00
expect(topic.ordered_posts.pluck(:id))
.to eq([post.id, hidden.id, small_action.id, reply.id])
expect(topic.bumped_at).to eq_time(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 nothing if there are multiple quotes' do
reply = Fabricate(:post, topic: topic, raw: raw3)
CookedPostProcessor.new(reply).remove_full_quote_on_direct_reply
expect(topic.ordered_posts.pluck(:id)).to eq([post.id, reply.id])
expect(reply.raw).to eq(raw3)
end
it 'does not delete quote if not first paragraph' do
reply = Fabricate(:post, topic: topic, raw: raw2)
CookedPostProcessor.new(reply).remove_full_quote_on_direct_reply
2019-04-08 21:30:04 +08:00
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).remove_full_quote_on_direct_reply
expect(reply.raw).to eq(raw)
end
it "does not generate a blank HTML document" do
post = Fabricate(:post, topic: topic, raw: "<sunday><monday>")
cp = CookedPostProcessor.new(post)
cp.post_process
expect(cp.html).to eq("<p></p>")
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
it "works with nested quotes" do
reply1 = Fabricate(:post, topic: topic, raw: raw)
reply2 = Fabricate(:post, topic: topic, raw: <<~RAW.strip)
[quote="#{reply1.user.username}, post:#{reply1.post_number}, topic:#{topic.id}"]
#{raw}
[/quote]
quoting a post with a quote
RAW
CookedPostProcessor.new(reply2).remove_full_quote_on_direct_reply
expect(reply2.raw).to eq('quoting a post with a quote')
end
end
2013-02-06 03:16:51 +08:00
end