discourse/app/jobs/regular/pull_hotlinked_images.rb

146 lines
5.4 KiB
Ruby
Raw Normal View History

require_dependency 'url_helper'
2014-04-14 16:55:57 -04:00
require_dependency 'file_helper'
require_dependency 'upload_creator'
2013-11-05 13:04:47 -05:00
module Jobs
class PullHotlinkedImages < Jobs::Base
sidekiq_options queue: 'low'
2013-11-05 13:04:47 -05:00
def initialize
# maximum size of the file in bytes
@max_size = SiteSetting.max_image_size_kb.kilobytes
2013-11-05 13:04:47 -05:00
end
def execute(args)
return unless SiteSetting.download_remote_images_to_local?
2013-11-05 13:04:47 -05:00
post_id = args[:post_id]
raise Discourse::InvalidParameters.new(:post_id) unless post_id.present?
post = Post.find_by(id: post_id)
2013-11-05 13:04:47 -05:00
return unless post.present?
raw = post.raw.dup
start_raw = raw.dup
2013-11-05 13:04:47 -05:00
downloaded_urls = {}
extract_images_from(post.cooked).each do |image|
src = original_src = image['src']
src = "http:#{src}" if src.start_with?("//")
2013-11-05 13:04:47 -05:00
if is_valid_image_url(src)
hotlinked = nil
2013-11-05 13:04:47 -05:00
begin
# have we already downloaded that file?
unless downloaded_urls.include?(src)
begin
hotlinked = FileHelper.download(
src,
max_file_size: @max_size,
tmp_file_name: "discourse-hotlinked",
follow_redirect: true
)
rescue Discourse::InvalidParameters
end
if hotlinked
2015-08-17 12:57:28 -04:00
if File.size(hotlinked.path) <= @max_size
filename = File.basename(URI.parse(src).path)
filename << File.extname(hotlinked.path) unless filename["."]
upload = UploadCreator.new(hotlinked, filename, origin: src).create_for(post.user_id)
if upload.persisted?
downloaded_urls[src] = upload.url
else
log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - #{upload.errors.join("\n")}")
end
else
log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}")
end
2013-11-05 13:04:47 -05:00
else
log(:info, "There was an error while downloading '#{src}' locally for post: #{post_id}")
2013-11-05 13:04:47 -05:00
end
end
2013-12-21 02:19:22 -05:00
# have we successfully downloaded that file?
2013-11-05 13:04:47 -05:00
if downloaded_urls[src].present?
url = downloaded_urls[src]
escaped_src = Regexp.escape(original_src)
# there are 6 ways to insert an image in a post
2013-11-05 13:04:47 -05:00
# HTML tag - <img src="http://...">
raw.gsub!(/src=["']#{escaped_src}["']/i, "src='#{url}'")
# BBCode tag - [img]http://...[/img]
raw.gsub!(/\[img\]#{escaped_src}\[\/img\]/i, "[img]#{url}[/img]")
# Markdown linked image - [![alt](http://...)](http://...)
raw.gsub!(/\[!\[([^\]]*)\]\(#{escaped_src}\)\]/) { "[<img src='#{url}' alt='#{$1}'>]" }
2013-11-05 13:04:47 -05:00
# Markdown inline - ![alt](http://...)
raw.gsub!(/!\[([^\]]*)\]\(#{escaped_src}\)/) { "![#{$1}](#{url})" }
# Markdown inline - ![](http://... "image title")
raw.gsub!(/!\[\]\(#{escaped_src} "([^\]]*)"\)/) { "![](#{url})" }
# Markdown inline - ![alt](http://... "image title")
raw.gsub!(/!\[([^\]]*)\]\(#{escaped_src} "([^\]]*)"\)/) { "![](#{url})" }
2013-11-05 13:04:47 -05:00
# Markdown reference - [x]: http://
raw.gsub!(/\[([^\]]+)\]:\s?#{escaped_src}/) { "[#{$1}]: #{url}" }
2013-11-05 13:04:47 -05:00
# Direct link
raw.gsub!(/^#{escaped_src}(\s?)$/) { "<img src='#{url}'>#{$1}" }
2013-11-05 13:04:47 -05:00
end
rescue => e
log(:info, "Failed to pull hotlinked image: #{src} post:#{post_id}\n" + e.message + "\n" + e.backtrace.join("\n"))
2013-11-05 13:04:47 -05:00
end
end
end
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)
2017-06-02 05:39:06 -04:00
elsif downloaded_urls.present?
post.trigger_post_process(true)
2013-11-05 13:04:47 -05:00
end
end
def extract_images_from(html)
doc = Nokogiri::HTML::fragment(html)
2017-06-02 05:39:06 -04:00
doc.css("img[src]") - doc.css("img.avatar")
2013-11-05 13:04:47 -05:00
end
def is_valid_image_url(src)
# make sure we actually have a url
return false unless src.present?
# we don't want to pull uploaded images
return false if Discourse.store.has_been_uploaded?(src)
# we don't want to pull relative images
return false if src =~ /\A\/[^\/]/i
# parse the src
begin
uri = URI.parse(src)
rescue URI::InvalidURIError
return false
end
hostname = uri.hostname
return false unless hostname
# we don't want to pull images hosted on the CDN (if we use one)
return false if Discourse.asset_host.present? && URI.parse(Discourse.asset_host).hostname == hostname
return false if SiteSetting.s3_cdn_url.present? && URI.parse(SiteSetting.s3_cdn_url).hostname == hostname
# we don't want to pull images hosted on the main domain
return false if URI.parse(Discourse.base_url_no_prefix).hostname == hostname
# check the domains blacklist
SiteSetting.should_download_images?(src)
2013-11-05 13:04:47 -05:00
end
def log(log_level, message)
Rails.logger.public_send(
log_level,
"#{RailsMultisite::ConnectionManagement.current_db}: #{message}"
)
end
2013-11-05 13:04:47 -05:00
end
end