diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 70e9a1bc347..73325758304 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -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 {
color: dark-light-choose($primary-low-mid, $secondary-high);
border: 1px solid $primary-low;
diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb
index 39a630635f4..b84c573156e 100644
--- a/app/jobs/regular/pull_hotlinked_images.rb
+++ b/app/jobs/regular/pull_hotlinked_images.rb
@@ -8,6 +8,8 @@ module Jobs
sidekiq_options queue: 'low'
+ LARGE_IMAGES = "large_images".freeze
+
def initialize
@max_size = SiteSetting.max_image_size_kb.kilobytes
end
@@ -46,7 +48,8 @@ module Jobs
raw = post.raw.dup
start_raw = raw.dup
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|
src = original_src = image['src']
@@ -57,7 +60,7 @@ module Jobs
if is_valid_image_url(src)
begin
# 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 File.size(hotlinked.path) <= @max_size
filename = File.basename(URI.parse(src).path)
@@ -70,6 +73,7 @@ module Jobs
end
else
large_images << original_src
+ new_large_images << original_src
end
else
broken_images << original_src
@@ -104,15 +108,18 @@ module Jobs
end
+ post.custom_fields[LARGE_IMAGES] = large_images
+ post.save!
post.reload
+
if start_raw == post.raw && raw != post.raw
changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") }
# we never want that job to bump the topic
options = { bypass_bump: true }
post.revise(Discourse.system_user, changes, options)
- elsif downloaded_urls.present?
+ elsif downloaded_urls.present? || new_large_images.present?
post.trigger_post_process(true)
- elsif broken_images.present? || large_images.present?
+ elsif broken_images.present?
start_html = post.cooked
doc = Nokogiri::HTML::fragment(start_html)
images = doc.css("img[src]") - doc.css("img.avatar")
@@ -125,21 +132,6 @@ module Jobs
tag.remove_attribute('src')
tag.remove_attribute('width')
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 = ''
- parent = tag.parent
- if parent.name == 'a'
- parent.add_next_sibling(tag)
- parent.add_next_sibling('
')
- parent.content = parent["href"]
- end
end
end
if start_html == post.cooked && doc.to_html != post.cooked
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 140726dca0a..4c897f38174 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -524,7 +524,6 @@ en:
post:
image_placeholder:
broken: "This image is broken"
- large: "This image is too large to display here; click to view"
rate_limiter:
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."
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?"
+ placeholders:
+ too_large: "(image larger than %{max_size_kb}KB)"
avatar:
missing: "Sorry, we can't find any avatar associated with that email address. Can you try uploading it again?"
diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb
index a1e38c702af..c1aec48a37a 100644
--- a/lib/cooked_post_processor.rb
+++ b/lib/cooked_post_processor.rb
@@ -81,11 +81,53 @@ class CookedPostProcessor
return if images.blank?
images.each do |img|
+ next if large_images.include?(img["src"]) && add_large_image_placeholder!(img)
+
limit_size!(img)
convert_to_link!(img)
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
# all image with a src attribute
@doc.css("img[src]") -
@@ -244,21 +286,18 @@ class CookedPostProcessor
def add_lightbox!(img, original_width, original_height, upload = nil)
# first, create a div to hold our lightbox
- lightbox = Nokogiri::XML::Node.new("div", @doc)
- lightbox["class"] = "lightbox-wrapper"
+ lightbox = create_node("div", "lightbox-wrapper")
img.add_next_sibling(lightbox)
lightbox.add_child(img)
# 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)
if upload && Discourse.store.internal?
a["data-download-href"] = Discourse.store.download_url(upload)
end
- a["href"] = img["src"]
- a["class"] = "lightbox"
a.add_child(img)
# 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)
# then, some overlay informations
- meta = Nokogiri::XML::Node.new("div", @doc)
- meta["class"] = "meta"
+ meta = create_node("div", "meta")
img.add_next_sibling(meta)
filename = get_filename(upload, img["src"])
@@ -287,13 +325,32 @@ class CookedPostProcessor
return I18n.t("upload.pasted_image_filename")
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)
- span = Nokogiri::XML::Node.new("span", @doc)
+ span = create_node("span", klass)
span.content = content if content
- span["class"] = klass
span
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
img = extract_images_for_post.first
return if img.blank?
@@ -318,14 +375,17 @@ class CookedPostProcessor
uploads = oneboxed_image_uploads.select(:url, :origin)
oneboxed_images.each do |img|
+ if large_images.include?(img["src"])
+ img.remove
+ next
+ end
+
url = img["src"].sub(/^https?:/i, "")
upload = uploads.find { |u| u.origin.sub(/^https?:/i, "") == url }
img["src"] = upload.url if upload.present?
- end
- # make sure we grab dimensions for oneboxed images
- # and wrap in a div
- oneboxed_images.each do |img|
+ # make sure we grab dimensions for oneboxed images
+ # and wrap in a div
limit_size!(img)
next if img["class"]&.include?('onebox-avatar')
diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb
index 60c27008c2d..5b9f8bc7614 100644
--- a/spec/components/cooked_post_processor_spec.rb
+++ b/spec/components/cooked_post_processor_spec.rb
@@ -162,7 +162,7 @@ describe CookedPostProcessor do
it "generates overlay information" do
cpp.post_process_images
- expect(cpp.html).to match_html "