mirror of
https://github.com/discourse/discourse.git
synced 2024-11-28 03:53:51 +08:00
ce0bac7a3d
* use image alt as a fallback when there's no title * update spec we used to check that the overlay information is added when the image has a titie. This adds 2 more scenarios. One where an image has both a title and an alt, in which case the title should be used and alt ignored. The other is when there's only an alt, it should then be used to generate the overlay
1495 lines
54 KiB
Ruby
1495 lines
54 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
require "rails_helper"
|
||
require "cooked_post_processor"
|
||
require "file_store/s3_store"
|
||
|
||
describe CookedPostProcessor do
|
||
fab!(:upload) { Fabricate(:upload) }
|
||
|
||
context "#post_process" do
|
||
fab!(:post) do
|
||
Fabricate(:post, raw: <<~RAW)
|
||
<img src="#{upload.url}">
|
||
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
|
||
<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: '/uploads/default/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: '/uploads/default/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: '/uploads/default/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/uploads/default/10x10.png"|)
|
||
# 1.5x is skipped cause we have a missing thumb
|
||
expect(html).to include('srcset="//test.localhost/uploads/default/666x500.jpg, //test.localhost/uploads/default/1998x1500.jpg 3x"')
|
||
expect(html).to include('src="//test.localhost/uploads/default/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/uploads/default/10x10.png"|)
|
||
expect(html).to include('srcset="//cdn.localhost/uploads/default/666x500.jpg, //cdn.localhost/uploads/default/1998x1500.jpg 3x"')
|
||
expect(html).to include('src="//cdn.localhost/uploads/default/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
|
||
)
|
||
|
||
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(/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 } } }
|
||
|
||
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
|
||
|
||
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
|
||
|
||
context "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!(:post) { Fabricate(:post_with_unsized_images) }
|
||
let(:cpp) { CookedPostProcessor.new(post) }
|
||
|
||
it "adds the width and height to images that don't have them" do
|
||
FastImage.expects(:size).returns([123, 456])
|
||
cpp.post_process
|
||
expect(cpp.html).to match(/width="123" height="456"/)
|
||
expect(cpp).to be_dirty
|
||
end
|
||
|
||
end
|
||
|
||
context "with large images" do
|
||
fab!(:post) do
|
||
Fabricate(:post, raw: <<~HTML)
|
||
<img src="#{upload.url}">
|
||
HTML
|
||
end
|
||
|
||
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
|
||
|
||
before do
|
||
SiteSetting.max_image_height = 2000
|
||
SiteSetting.create_thumbnails = true
|
||
FastImage.expects(:size).returns([1750, 2000])
|
||
end
|
||
|
||
it "generates overlay information" do
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
|
||
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/uploads/default/#{upload.sha1}" title="logo.png"><img src="//test.localhost/uploads/default/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='/uploads/default/original/1X/1234567890123456.jpg' />")
|
||
end
|
||
|
||
it 'should not add lightbox' do
|
||
cpp.post_process
|
||
|
||
expect(cpp.html).to match_html <<~HTML
|
||
<p><img class="onebox" src="//test.localhost/uploads/default/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="/uploads/default/original/1X/1234567890123456.svg">')
|
||
end
|
||
|
||
it 'should not add lightbox' do
|
||
cpp.post_process
|
||
|
||
expect(cpp.html).to match_html <<~HTML
|
||
<p><img src="//test.localhost/uploads/default/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/uploads/default/original/1X/1234567890123456.svg?somepamas">')
|
||
end
|
||
|
||
it 'should not add lightbox' do
|
||
SiteSetting.crawl_images = true
|
||
cpp.post_process
|
||
|
||
expect(cpp.html).to match_html("<p><img src=\"http://test.discourse/uploads/default/original/1X/1234567890123456.svg?somepamas\" width=\"690\"\ height=\"788\"></p>")
|
||
end
|
||
end
|
||
end
|
||
|
||
end
|
||
|
||
context "with tall images" do
|
||
fab!(:post) do
|
||
Fabricate(:post, raw: <<~HTML)
|
||
<img src="#{upload.url}">
|
||
HTML
|
||
end
|
||
|
||
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
|
||
|
||
before do
|
||
SiteSetting.create_thumbnails = true
|
||
FastImage.expects(:size).returns([860, 2000])
|
||
OptimizedImage.expects(:resize).never
|
||
OptimizedImage.expects(:crop).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
end
|
||
|
||
it "crops the image" do
|
||
cpp.post_process
|
||
|
||
expect(cpp.html).to match(/width="690" height="500">/)
|
||
expect(cpp).to be_dirty
|
||
end
|
||
|
||
end
|
||
|
||
context "with iPhone X screenshots" do
|
||
fab!(:post) do
|
||
Fabricate(:post, raw: <<~HTML)
|
||
<img src="#{upload.url}">
|
||
HTML
|
||
end
|
||
|
||
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
|
||
|
||
before do
|
||
SiteSetting.create_thumbnails = true
|
||
FastImage.expects(:size).returns([1125, 2436])
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
OptimizedImage.expects(:crop).never
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
end
|
||
|
||
it "crops the image" 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/uploads/default/#{upload.sha1}" title="logo.png"><img src="//test.localhost/uploads/default/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
|
||
|
||
end
|
||
|
||
context "with large images when using subfolders" do
|
||
fab!(:post) do
|
||
Fabricate(:post, raw: <<~HTML)
|
||
<img src="/subfolder#{upload.url}">
|
||
HTML
|
||
end
|
||
|
||
let(:cpp) { CookedPostProcessor.new(post, disable_loading_image: true) }
|
||
let(:base_url) { "http://test.localhost/subfolder" }
|
||
let(:base_uri) { "/subfolder" }
|
||
|
||
before do
|
||
SiteSetting.max_image_height = 2000
|
||
SiteSetting.create_thumbnails = true
|
||
Discourse.stubs(:base_url).returns(base_url)
|
||
Discourse.stubs(:base_uri).returns(base_uri)
|
||
FastImage.expects(:size).returns([1750, 2000])
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
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/uploads/default/#{upload.sha1}" title="logo.png"><img src="//test.localhost/subfolder/uploads/default/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
|
||
|
||
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/uploads/default/#{upload.sha1}" title="&gt;&lt;img src=x onerror=alert(&#39;haha&#39;)&gt;.png"><img src="//test.localhost/subfolder/uploads/default/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">&gt;&lt;img src=x onerror=alert(&#39;haha&#39;)&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
|
||
|
||
end
|
||
|
||
context "with title and alt" do
|
||
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
|
||
FastImage.expects(:size).returns([1750, 2000])
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
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/uploads/default/#{upload.sha1}" title="WAT"><img src="//test.localhost/uploads/default/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!(: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
|
||
FastImage.expects(:size).returns([1750, 2000])
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
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/uploads/default/#{upload.sha1}" title="WAT"><img src="//test.localhost/uploads/default/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!(: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
|
||
FastImage.expects(:size).returns([1750, 2000])
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
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/uploads/default/#{upload.sha1}" title="RED"><img src="//test.localhost/uploads/default/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
|
||
end
|
||
|
||
context "post image" do
|
||
let(:reply) { Fabricate(:post_with_uploaded_image, post_number: 2) }
|
||
let(:cpp) { CookedPostProcessor.new(reply) }
|
||
|
||
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
|
||
end
|
||
|
||
context "#extract_images" do
|
||
|
||
let(:post) { build(:post_with_plenty_of_images) }
|
||
let(:cpp) { CookedPostProcessor.new(post) }
|
||
|
||
it "does not extract emojis or images inside oneboxes or quotes" do
|
||
expect(cpp.extract_images.length).to eq(0)
|
||
end
|
||
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
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
|
||
|
||
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 } }
|
||
expect(cpp.get_size_from_image_sizes("/image.png", image_sizes)).to eq([111, 222])
|
||
end
|
||
|
||
end
|
||
|
||
context "#get_size" do
|
||
|
||
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
|
||
FastImage.expects(:size).returns([200, 400])
|
||
cpp.get_size("http://foo.bar/image3.png")
|
||
expect(cpp.get_size("http://foo.bar/image3.png")).to eq([200, 400])
|
||
end
|
||
|
||
context "when crawl_images is disabled" do
|
||
|
||
before do
|
||
SiteSetting.crawl_images = false
|
||
end
|
||
|
||
it "doesn't call FastImage" do
|
||
FastImage.expects(:size).never
|
||
expect(cpp.get_size("http://foo.bar/image1.png")).to eq(nil)
|
||
end
|
||
|
||
it "is always allowed to crawl our own images" do
|
||
store = stub
|
||
store.expects(:has_been_uploaded?).returns(true)
|
||
Discourse.expects(:store).returns(store)
|
||
FastImage.expects(:size).returns([100, 200])
|
||
expect(cpp.get_size("http://foo.bar/image2.png")).to eq([100, 200])
|
||
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
|
||
|
||
end
|
||
|
||
end
|
||
|
||
context "#is_valid_image_url?" do
|
||
|
||
let(:post) { build(:post) }
|
||
let(:cpp) { CookedPostProcessor.new(post) }
|
||
|
||
it "validates HTTP(s) urls" do
|
||
expect(cpp.is_valid_image_url?("http://domain.com")).to eq(true)
|
||
expect(cpp.is_valid_image_url?("https://domain.com")).to eq(true)
|
||
end
|
||
|
||
it "doesn't validate other urls" do
|
||
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)
|
||
end
|
||
|
||
it "doesn't throw an exception with a bad URI" do
|
||
expect(cpp.is_valid_image_url?("http://do<main.com")).to eq(nil)
|
||
end
|
||
|
||
end
|
||
|
||
context "#get_filename" do
|
||
|
||
let(:post) { build(:post) }
|
||
let(:cpp) { CookedPostProcessor.new(post) }
|
||
|
||
it "returns the filename of the src when there is no upload" do
|
||
expect(cpp.get_filename(nil, "http://domain.com/image.png")).to eq("image.png")
|
||
end
|
||
|
||
it "returns the original filename of the upload when there is an upload" do
|
||
upload = build(:upload, original_filename: "upload.jpg")
|
||
expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq("upload.jpg")
|
||
end
|
||
|
||
it "returns a generic name for pasted images" do
|
||
upload = build(:upload, original_filename: "blob.png")
|
||
expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq(I18n.t('upload.pasted_image_filename'))
|
||
end
|
||
|
||
end
|
||
|
||
context "#post_process_oneboxes" do
|
||
let(:post) { build(:post_with_youtube, id: 123) }
|
||
let(:cpp) { CookedPostProcessor.new(post, invalidate_oneboxes: true) }
|
||
|
||
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)
|
||
.returns("<div>GANGNAM STYLE</div>")
|
||
|
||
cpp.post_process_oneboxes
|
||
end
|
||
|
||
it "inserts the onebox without wrapping p" do
|
||
expect(cpp).to be_dirty
|
||
expect(cpp.html).to match_html "<div>GANGNAM STYLE</div>"
|
||
end
|
||
|
||
it "replaces downloaded onebox image" 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)
|
||
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>")
|
||
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
|
||
|
||
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')
|
||
end
|
||
|
||
end
|
||
|
||
context "#optimize_urls" do
|
||
|
||
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/uploads/default/original/2X/2345678901234567.jpg">Link</a><br>
|
||
<img src="//test.localhost/uploads/default/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/uploads/default/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" alt=":smile:"></p>
|
||
HTML
|
||
end
|
||
|
||
context "when CDN is enabled" do
|
||
|
||
it "uses schemaless CDN url for http uploads" do
|
||
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/uploads/default/original/2X/2345678901234567.jpg">Link</a><br>
|
||
<img src="//my.cdn.com/uploads/default/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/uploads/default/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" alt=":smile:"></p>
|
||
HTML
|
||
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/uploads/default/original/2X/2345678901234567.jpg">Link</a><br>
|
||
<img src="https://my.cdn.com/uploads/default/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/uploads/default/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" 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/uploads/default/original/2X/2345678901234567.jpg">Link</a><br>
|
||
<img src="//my.cdn.com/uploads/default/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/uploads/default/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" 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/uploads/default/original/2X/2345678901234567.jpg">Link</a><br>
|
||
<img src="//my.cdn.com/uploads/default/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/uploads/default/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" alt=":smile:"></p>
|
||
HTML
|
||
end
|
||
|
||
it "uses the right CDN when uploads are on S3" do
|
||
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
|
||
|
||
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",
|
||
)
|
||
|
||
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
|
||
|
||
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 "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
|
||
|
||
it "disables download_remote_images if there is not enough disk space" do
|
||
cpp.expects(:available_disk_space).returns(5)
|
||
cpp.pull_hotlinked_images
|
||
expect(SiteSetting.download_remote_images_to_local).to eq(false)
|
||
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 do
|
||
SiteSetting.download_remote_images_to_local = true
|
||
cpp.expects(:available_disk_space).returns(50)
|
||
end
|
||
|
||
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
|
||
|
||
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
|
||
expect(cpp.disable_if_low_on_disk_space).to eq(false)
|
||
expect(SiteSetting.download_remote_images_to_local).to eq(true)
|
||
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('<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
|
||
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
|
||
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
|
||
|
||
it "awards the badge for using an onebox" do
|
||
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(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
|
||
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
|
||
|
||
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).remove_full_quote_on_direct_reply
|
||
|
||
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).remove_full_quote_on_direct_reply
|
||
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 "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
|