mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 14:19:49 +08:00
4646a38ae6
When we were pulling hotlinked images for oneboxes in the CookedPostProcessor, we were using the direct S3 URL, which returned a 403 error and thus did not set widths and heights of the images. We now cook the URL first based on whether the upload is secure before handing off to FastImage.
1791 lines
66 KiB
Ruby
1791 lines
66 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
require "rails_helper"
|
||
require "cooked_post_processor"
|
||
require "file_store/s3_store"
|
||
|
||
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
|
||
|
||
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
|
||
|
||
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: "/#{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
|
||
)
|
||
|
||
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/#{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
|
||
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
|
||
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
|
||
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
|
||
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/)
|
||
|
||
OptimizedImage.expects(:resize).returns(true)
|
||
FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0)
|
||
Discourse.store.class.any_instance.expects(:has_been_uploaded?).at_least_once.returns(true)
|
||
|
||
SiteSetting.login_required = true
|
||
SiteSetting.secure_media = true
|
||
upload.update_column(:secure, true)
|
||
end
|
||
|
||
let(:optimized_size) { "600x500" }
|
||
|
||
let(:post) do
|
||
Fabricate(:post, raw: "![large.png|#{optimized_size}](#{upload.short_url})")
|
||
end
|
||
|
||
it "handles secure images with the correct lightbox link href" do
|
||
cpp.post_process
|
||
|
||
expect(cpp.html).to match_html <<~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
|
||
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/#{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
|
||
|
||
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) }
|
||
|
||
before do
|
||
set_subfolder "/subfolder"
|
||
|
||
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" 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
|
||
|
||
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="&gt;&lt;img src=x onerror=alert(&#39;haha&#39;)&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">&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/#{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!(: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/#{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!(: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/#{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
|
||
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
|
||
Discourse.expects(:store).returns(store).at_least_once
|
||
store.expects(:has_been_uploaded?).returns(true)
|
||
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 "#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
|
||
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
|
||
|
||
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
|
||
|
||
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/#{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
|
||
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/#{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
|
||
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
|
||
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
|
||
SiteSetting.download_remote_images_threshold = 20
|
||
cpp.stubs(:available_disk_space).returns(50)
|
||
end
|
||
|
||
it "does nothing when there's enough disk space" do
|
||
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.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
|
||
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
|
||
|
||
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 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 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
|
||
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
|
||
|
||
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
|
||
|
||
end
|