NEW: large image placeholder added in cooked html (#5291)

This commit is contained in:
Vinoth Kannan 2017-11-15 16:00:47 +05:30 committed by Régis Hanol
parent 9ed16343fc
commit 7b494a65c9
6 changed files with 127 additions and 39 deletions

View File

@ -414,6 +414,40 @@ a.mention, a.mention-group {
} }
} }
.large-image-placeholder {
> a {
&.link {
margin-right: 10px;
}
> * { overflow: hidden; }
> i.fa {
color: dark-light-choose($primary-medium, $secondary-medium);
margin-right: 6px;
font-size: $base-font-size;
line-height: $base-line-height;
}
> span.url {
display: inline-block;
max-width: 300px;
margin-right: 6px;
text-overflow: ellipsis;
white-space: nowrap;
}
> span.help {
display: inline-block;
color: dark-light-choose($primary-medium, $secondary-medium);
font-size: 0.929em;
font-style: italic;
line-height: $base-line-height;
}
}
}
.broken-image, .large-image { .broken-image, .large-image {
color: dark-light-choose($primary-low-mid, $secondary-high); color: dark-light-choose($primary-low-mid, $secondary-high);
border: 1px solid $primary-low; border: 1px solid $primary-low;

View File

@ -8,6 +8,8 @@ module Jobs
sidekiq_options queue: 'low' sidekiq_options queue: 'low'
LARGE_IMAGES = "large_images".freeze
def initialize def initialize
@max_size = SiteSetting.max_image_size_kb.kilobytes @max_size = SiteSetting.max_image_size_kb.kilobytes
end end
@ -46,7 +48,8 @@ module Jobs
raw = post.raw.dup raw = post.raw.dup
start_raw = raw.dup start_raw = raw.dup
downloaded_urls = {} downloaded_urls = {}
broken_images, large_images = [], [] large_images = post.custom_fields[LARGE_IMAGES].presence || []
broken_images, new_large_images = [], []
extract_images_from(post.cooked).each do |image| extract_images_from(post.cooked).each do |image|
src = original_src = image['src'] src = original_src = image['src']
@ -57,7 +60,7 @@ module Jobs
if is_valid_image_url(src) if is_valid_image_url(src)
begin begin
# have we already downloaded that file? # have we already downloaded that file?
unless downloaded_urls.include?(src) unless downloaded_urls.include?(src) || large_images.include?(src) || broken_images.include?(src)
if hotlinked = download(src) if hotlinked = download(src)
if File.size(hotlinked.path) <= @max_size if File.size(hotlinked.path) <= @max_size
filename = File.basename(URI.parse(src).path) filename = File.basename(URI.parse(src).path)
@ -70,6 +73,7 @@ module Jobs
end end
else else
large_images << original_src large_images << original_src
new_large_images << original_src
end end
else else
broken_images << original_src broken_images << original_src
@ -104,15 +108,18 @@ module Jobs
end end
post.custom_fields[LARGE_IMAGES] = large_images
post.save!
post.reload post.reload
if start_raw == post.raw && raw != post.raw if start_raw == post.raw && raw != post.raw
changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") } changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") }
# we never want that job to bump the topic # we never want that job to bump the topic
options = { bypass_bump: true } options = { bypass_bump: true }
post.revise(Discourse.system_user, changes, options) post.revise(Discourse.system_user, changes, options)
elsif downloaded_urls.present? elsif downloaded_urls.present? || new_large_images.present?
post.trigger_post_process(true) post.trigger_post_process(true)
elsif broken_images.present? || large_images.present? elsif broken_images.present?
start_html = post.cooked start_html = post.cooked
doc = Nokogiri::HTML::fragment(start_html) doc = Nokogiri::HTML::fragment(start_html)
images = doc.css("img[src]") - doc.css("img.avatar") images = doc.css("img[src]") - doc.css("img.avatar")
@ -125,21 +132,6 @@ module Jobs
tag.remove_attribute('src') tag.remove_attribute('src')
tag.remove_attribute('width') tag.remove_attribute('width')
tag.remove_attribute('height') tag.remove_attribute('height')
elsif large_images.include?(src)
tag.name = 'a'
tag.set_attribute('href', src)
tag.set_attribute('target', '_blank')
tag.set_attribute('title', I18n.t('post.image_placeholder.large'))
tag.remove_attribute('src')
tag.remove_attribute('width')
tag.remove_attribute('height')
tag.inner_html = '<span class="large-image fa fa-picture-o"></span>'
parent = tag.parent
if parent.name == 'a'
parent.add_next_sibling(tag)
parent.add_next_sibling('<br>')
parent.content = parent["href"]
end
end end
end end
if start_html == post.cooked && doc.to_html != post.cooked if start_html == post.cooked && doc.to_html != post.cooked

View File

@ -524,7 +524,6 @@ en:
post: post:
image_placeholder: image_placeholder:
broken: "This image is broken" broken: "This image is broken"
large: "This image is too large to display here; click to view"
rate_limiter: rate_limiter:
slow_down: "You have performed this action too many times, try again later." slow_down: "You have performed this action too many times, try again later."
@ -2870,6 +2869,8 @@ en:
too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}KB), please resize it and try again." too_large: "Sorry, the image you are trying to upload is too big (maximum size is %{max_size_kb}KB), please resize it and try again."
larger_than_x_megapixels: "Sorry, the image you are trying to upload is too large (maximum dimension is %{max_image_megapixels}-megapixels), please resize it and try again." larger_than_x_megapixels: "Sorry, the image you are trying to upload is too large (maximum dimension is %{max_image_megapixels}-megapixels), please resize it and try again."
size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?" size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?"
placeholders:
too_large: "(image larger than %{max_size_kb}KB)"
avatar: avatar:
missing: "Sorry, we can't find any avatar associated with that email address. Can you try uploading it again?" missing: "Sorry, we can't find any avatar associated with that email address. Can you try uploading it again?"

View File

@ -81,11 +81,53 @@ class CookedPostProcessor
return if images.blank? return if images.blank?
images.each do |img| images.each do |img|
next if large_images.include?(img["src"]) && add_large_image_placeholder!(img)
limit_size!(img) limit_size!(img)
convert_to_link!(img) convert_to_link!(img)
end end
end end
def add_large_image_placeholder!(img)
url = img["src"]
is_hyperlinked = is_a_hyperlink?(img)
placeholder = create_node("div", "large-image-placeholder")
img.add_next_sibling(placeholder)
placeholder.add_child(img)
a = create_link_node(nil, url, true)
img.add_next_sibling(a)
span = create_span_node("url", url)
a.add_child(span)
span.add_previous_sibling(create_icon_node("image"))
span.add_next_sibling(create_span_node("help", I18n.t("upload.placeholders.too_large", max_size_kb: SiteSetting.max_image_size_kb)))
# Only if the image is already linked
if is_hyperlinked
parent = placeholder.parent
parent.add_next_sibling(placeholder)
if parent.name == 'a' && parent["href"].present? && url != parent["href"]
parent["class"] = "link"
a.add_previous_sibling(parent)
lspan = create_span_node("url", parent["href"])
parent.add_child(lspan)
lspan.add_previous_sibling(create_icon_node("link"))
end
end
img.remove
true
end
def large_images
@large_images ||= @post.custom_fields[Jobs::PullHotlinkedImages::LARGE_IMAGES].presence || []
end
def extract_images def extract_images
# all image with a src attribute # all image with a src attribute
@doc.css("img[src]") - @doc.css("img[src]") -
@ -244,21 +286,18 @@ class CookedPostProcessor
def add_lightbox!(img, original_width, original_height, upload = nil) def add_lightbox!(img, original_width, original_height, upload = nil)
# first, create a div to hold our lightbox # first, create a div to hold our lightbox
lightbox = Nokogiri::XML::Node.new("div", @doc) lightbox = create_node("div", "lightbox-wrapper")
lightbox["class"] = "lightbox-wrapper"
img.add_next_sibling(lightbox) img.add_next_sibling(lightbox)
lightbox.add_child(img) lightbox.add_child(img)
# then, the link to our larger image # then, the link to our larger image
a = Nokogiri::XML::Node.new("a", @doc) a = create_link_node("lightbox", img["src"])
img.add_next_sibling(a) img.add_next_sibling(a)
if upload && Discourse.store.internal? if upload && Discourse.store.internal?
a["data-download-href"] = Discourse.store.download_url(upload) a["data-download-href"] = Discourse.store.download_url(upload)
end end
a["href"] = img["src"]
a["class"] = "lightbox"
a.add_child(img) a.add_child(img)
# replace the image by its thumbnail # replace the image by its thumbnail
@ -266,8 +305,7 @@ class CookedPostProcessor
img["src"] = upload.thumbnail(w, h).url if upload && upload.has_thumbnail?(w, h) img["src"] = upload.thumbnail(w, h).url if upload && upload.has_thumbnail?(w, h)
# then, some overlay informations # then, some overlay informations
meta = Nokogiri::XML::Node.new("div", @doc) meta = create_node("div", "meta")
meta["class"] = "meta"
img.add_next_sibling(meta) img.add_next_sibling(meta)
filename = get_filename(upload, img["src"]) filename = get_filename(upload, img["src"])
@ -287,13 +325,32 @@ class CookedPostProcessor
return I18n.t("upload.pasted_image_filename") return I18n.t("upload.pasted_image_filename")
end end
def create_node(tag_name, klass)
node = Nokogiri::XML::Node.new(tag_name, @doc)
node["class"] = klass if klass.present?
node
end
def create_span_node(klass, content = nil) def create_span_node(klass, content = nil)
span = Nokogiri::XML::Node.new("span", @doc) span = create_node("span", klass)
span.content = content if content span.content = content if content
span["class"] = klass
span span
end end
def create_icon_node(klass)
create_node("i", "fa fa-fw fa-#{klass}")
end
def create_link_node(klass, url, external = false)
a = create_node("a", klass)
a["href"] = url
if external
a["target"] = "_blank"
a["rel"] = "nofollow noopener"
end
a
end
def update_post_image def update_post_image
img = extract_images_for_post.first img = extract_images_for_post.first
return if img.blank? return if img.blank?
@ -318,14 +375,17 @@ class CookedPostProcessor
uploads = oneboxed_image_uploads.select(:url, :origin) uploads = oneboxed_image_uploads.select(:url, :origin)
oneboxed_images.each do |img| oneboxed_images.each do |img|
if large_images.include?(img["src"])
img.remove
next
end
url = img["src"].sub(/^https?:/i, "") url = img["src"].sub(/^https?:/i, "")
upload = uploads.find { |u| u.origin.sub(/^https?:/i, "") == url } upload = uploads.find { |u| u.origin.sub(/^https?:/i, "") == url }
img["src"] = upload.url if upload.present? img["src"] = upload.url if upload.present?
end
# make sure we grab dimensions for oneboxed images # make sure we grab dimensions for oneboxed images
# and wrap in a div # and wrap in a div
oneboxed_images.each do |img|
limit_size!(img) limit_size!(img)
next if img["class"]&.include?('onebox-avatar') next if img["class"]&.include?('onebox-avatar')

View File

@ -162,7 +162,7 @@ describe CookedPostProcessor do
it "generates overlay information" do it "generates overlay information" do
cpp.post_process_images cpp.post_process_images
expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a data-download-href=\"/uploads/default/#{upload.sha1}\" href=\"/uploads/default/1/1234567890123456.jpg\" class=\"lightbox\" title=\"logo.png\"><img src=\"/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\"> expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a class=\"lightbox\" href=\"/uploads/default/1/1234567890123456.jpg\" data-download-href=\"/uploads/default/#{upload.sha1}\" title=\"logo.png\"><img src=\"/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\">
<span class=\"filename\">logo.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span> <span class=\"filename\">logo.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span>
</div></a></div></p>" </div></a></div></p>"
expect(cpp).to be_dirty expect(cpp).to be_dirty
@ -245,7 +245,7 @@ describe CookedPostProcessor do
it "generates overlay information" do it "generates overlay information" do
cpp.post_process_images cpp.post_process_images
expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a data-download-href=\"/subfolder/uploads/default/#{upload.sha1}\" href=\"/subfolder/uploads/default/1/1234567890123456.jpg\" class=\"lightbox\" title=\"logo.png\"><img src=\"/subfolder/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\"> expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a class=\"lightbox\" href=\"/subfolder/uploads/default/1/1234567890123456.jpg\" data-download-href=\"/subfolder/uploads/default/#{upload.sha1}\" title=\"logo.png\"><img src=\"/subfolder/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\">
<span class=\"filename\">logo.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span> <span class=\"filename\">logo.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span>
</div></a></div></p>" </div></a></div></p>"
expect(cpp).to be_dirty expect(cpp).to be_dirty
@ -254,7 +254,7 @@ describe CookedPostProcessor do
it "should escape the filename" do it "should escape the filename" do
upload.update_attributes!(original_filename: "><img src=x onerror=alert('haha')>.png") upload.update_attributes!(original_filename: "><img src=x onerror=alert('haha')>.png")
cpp.post_process_images cpp.post_process_images
expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a data-download-href=\"/subfolder/uploads/default/#{upload.sha1}\" href=\"/subfolder/uploads/default/1/1234567890123456.jpg\" class=\"lightbox\" title=\"&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png\"><img src=\"/subfolder/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\"> expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a class=\"lightbox\" href=\"/subfolder/uploads/default/1/1234567890123456.jpg\" data-download-href=\"/subfolder/uploads/default/#{upload.sha1}\" title=\"&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png\"><img src=\"/subfolder/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" width=\"690\" height=\"788\"><div class=\"meta\">
<span class=\"filename\">&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span> <span class=\"filename\">&amp;gt;&amp;lt;img src=x onerror=alert(&amp;#39;haha&amp;#39;)&amp;gt;.png</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span>
</div></a></div></p>" </div></a></div></p>"
end end
@ -280,7 +280,7 @@ describe CookedPostProcessor do
it "generates overlay information" do it "generates overlay information" do
cpp.post_process_images cpp.post_process_images
expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a data-download-href=\"/uploads/default/#{upload.sha1}\" href=\"/uploads/default/1/1234567890123456.jpg\" class=\"lightbox\" title=\"WAT\"><img src=\"/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" title=\"WAT\" width=\"690\" height=\"788\"><div class=\"meta\"> expect(cpp.html).to match_html "<p><div class=\"lightbox-wrapper\"><a class=\"lightbox\" href=\"/uploads/default/1/1234567890123456.jpg\" data-download-href=\"/uploads/default/#{upload.sha1}\" title=\"WAT\"><img src=\"/uploads/default/optimized/1X/#{upload.sha1}_1_690x788.png\" title=\"WAT\" width=\"690\" height=\"788\"><div class=\"meta\">
<span class=\"filename\">WAT</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span> <span class=\"filename\">WAT</span><span class=\"informations\">1750x2000 1.21 KB</span><span class=\"expand\"></span>
</div></a></div></p>" </div></a></div></p>"
expect(cpp).to be_dirty expect(cpp).to be_dirty

View File

@ -111,7 +111,7 @@ describe Jobs::PullHotlinkedImages do
expect(post.cooked).to match(/<p><img src=.*\/uploads/) expect(post.cooked).to match(/<p><img src=.*\/uploads/)
expect(post.cooked).to match(/<img src=.*\/uploads.*\ class="thumbnail"/) expect(post.cooked).to match(/<img src=.*\/uploads.*\ class="thumbnail"/)
expect(post.cooked).to match(/<span class="broken-image fa fa-chain-broken/) expect(post.cooked).to match(/<span class="broken-image fa fa-chain-broken/)
expect(post.cooked).to match(/<\/a><br><a href=.*\ target="_blank" .*\><span class="large-image fa fa-picture-o"><\/span><\/a>/) expect(post.cooked).to match(/<div class="large-image-placeholder">/)
end end
end end
end end
@ -132,9 +132,10 @@ describe Jobs::PullHotlinkedImages do
Jobs::ProcessPost.new.execute(post_id: post.id) Jobs::ProcessPost.new.execute(post_id: post.id)
Jobs::PullHotlinkedImages.new.execute(post_id: post.id) Jobs::PullHotlinkedImages.new.execute(post_id: post.id)
Jobs::ProcessPost.new.execute(post_id: post.id)
post.reload post.reload
expect(post.cooked).to match(/<a href=.*\ target="_blank" .*\><span class="large-image fa fa-picture-o"><\/span><\/a>/) expect(post.cooked).to match(/<div class="large-image-placeholder"><a href=.*\ target="_blank" .*\>/)
end end
end end