2013-11-20 20:10:08 +08:00
|
|
|
require_dependency 'url_helper'
|
2014-04-15 04:55:57 +08:00
|
|
|
require_dependency 'file_helper'
|
2013-11-20 20:10:08 +08:00
|
|
|
|
2013-11-06 02:04:47 +08:00
|
|
|
module Jobs
|
|
|
|
|
|
|
|
class PullHotlinkedImages < Jobs::Base
|
|
|
|
def initialize
|
|
|
|
# maximum size of the file in bytes
|
2013-11-14 00:30:48 +08:00
|
|
|
@max_size = SiteSetting.max_image_size_kb.kilobytes
|
2013-11-06 02:04:47 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def execute(args)
|
2013-11-15 22:22:18 +08:00
|
|
|
return unless SiteSetting.download_remote_images_to_local?
|
2013-11-06 02:04:47 +08:00
|
|
|
|
|
|
|
post_id = args[:post_id]
|
|
|
|
raise Discourse::InvalidParameters.new(:post_id) unless post_id.present?
|
|
|
|
|
2014-05-06 21:41:59 +08:00
|
|
|
post = Post.find_by(id: post_id)
|
2013-11-06 02:04:47 +08:00
|
|
|
return unless post.present?
|
|
|
|
|
|
|
|
raw = post.raw.dup
|
2014-04-22 05:08:17 +08:00
|
|
|
start_raw = raw.dup
|
2013-11-06 02:04:47 +08:00
|
|
|
downloaded_urls = {}
|
|
|
|
|
|
|
|
extract_images_from(post.cooked).each do |image|
|
|
|
|
src = image['src']
|
2013-11-26 02:47:53 +08:00
|
|
|
src = "http:" + src if src.start_with?("//")
|
2013-11-06 02:04:47 +08:00
|
|
|
|
|
|
|
if is_valid_image_url(src)
|
2014-04-22 05:08:17 +08:00
|
|
|
hotlinked = nil
|
2013-11-06 02:04:47 +08:00
|
|
|
begin
|
|
|
|
# have we already downloaded that file?
|
2014-04-22 05:08:17 +08:00
|
|
|
unless downloaded_urls.include?(src)
|
2014-04-22 21:32:48 +08:00
|
|
|
begin
|
|
|
|
hotlinked = FileHelper.download(src, @max_size, "discourse-hotlinked")
|
|
|
|
rescue Discourse::InvalidParameters
|
|
|
|
end
|
2014-09-27 00:27:10 +08:00
|
|
|
if hotlinked
|
|
|
|
if hotlinked.size <= @max_size
|
|
|
|
filename = File.basename(URI.parse(src).path)
|
|
|
|
upload = Upload.create_for(post.user_id, hotlinked, filename, hotlinked.size, { origin: src })
|
|
|
|
downloaded_urls[src] = upload.url
|
|
|
|
else
|
|
|
|
Rails.logger.error("Failed to pull hotlinked image: #{src} - Image is bigger than #{@max_size}")
|
|
|
|
end
|
2013-11-06 02:04:47 +08:00
|
|
|
else
|
2014-09-27 00:27:10 +08:00
|
|
|
Rails.logger.error("There was an error while downloading '#{src}' locally.")
|
2013-11-06 02:04:47 +08:00
|
|
|
end
|
|
|
|
end
|
2013-12-21 15:19:22 +08:00
|
|
|
# have we successfully downloaded that file?
|
2013-11-06 02:04:47 +08:00
|
|
|
if downloaded_urls[src].present?
|
|
|
|
url = downloaded_urls[src]
|
2014-06-11 22:37:26 +08:00
|
|
|
escaped_src = Regexp.escape(src)
|
2013-11-20 20:10:08 +08:00
|
|
|
# there are 6 ways to insert an image in a post
|
2013-11-06 02:04:47 +08: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]")
|
2013-11-20 20:10:08 +08:00
|
|
|
# Markdown linked image - [![alt](http://...)](http://...)
|
|
|
|
raw.gsub!(/\[!\[([^\]]*)\]\(#{escaped_src}\)\]/) { "[<img src='#{url}' alt='#{$1}'>]" }
|
2013-11-06 02:04:47 +08:00
|
|
|
# Markdown inline - ![alt](http://...)
|
|
|
|
raw.gsub!(/!\[([^\]]*)\]\(#{escaped_src}\)/) { "![#{$1}](#{url})" }
|
|
|
|
# Markdown reference - [x]: http://
|
|
|
|
raw.gsub!(/\[(\d+)\]: #{escaped_src}/) { "[#{$1}]: #{url}" }
|
|
|
|
# Direct link
|
|
|
|
raw.gsub!(src, "<img src='#{url}'>")
|
|
|
|
end
|
|
|
|
rescue => e
|
2014-04-15 04:55:57 +08:00
|
|
|
Rails.logger.error("Failed to pull hotlinked image: #{src}\n" + e.message + "\n" + e.backtrace.join("\n"))
|
2013-11-06 02:04:47 +08:00
|
|
|
ensure
|
|
|
|
# close & delete the temp file
|
|
|
|
hotlinked && hotlinked.close!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
2014-04-22 05:08:17 +08:00
|
|
|
post.reload
|
|
|
|
if start_raw != post.raw
|
|
|
|
# post was edited - start over (after 10 minutes)
|
|
|
|
backoff = args.fetch(:backoff, 1) + 1
|
|
|
|
delay = SiteSetting.ninja_edit_window * args[:backoff]
|
|
|
|
Jobs.enqueue_in(delay.seconds.to_i, :pull_hotlinked_images, args.merge!(backoff: backoff))
|
|
|
|
elsif raw != post.raw
|
2014-10-28 05:06:43 +08:00
|
|
|
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)
|
2013-11-06 02:04:47 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def extract_images_from(html)
|
|
|
|
doc = Nokogiri::HTML::fragment(html)
|
2014-07-18 23:54:18 +08:00
|
|
|
doc.css("img[src]") - doc.css(".onebox-result img") - doc.css("img.avatar")
|
2013-11-06 02:04:47 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def is_valid_image_url(src)
|
2014-05-08 01:49:16 +08:00
|
|
|
# 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)
|
2014-09-27 00:27:10 +08:00
|
|
|
# we don't want to pull relative images
|
|
|
|
return false if src =~ /\A\/[^\/]/i
|
2014-05-08 01:49:16 +08:00
|
|
|
# parse the src
|
|
|
|
begin
|
|
|
|
uri = URI.parse(src)
|
|
|
|
rescue URI::InvalidURIError
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
# 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 == uri.hostname
|
2015-05-27 09:17:46 +08:00
|
|
|
return false if SiteSetting.s3_cdn_url.present? && URI.parse(SiteSetting.s3_cdn_url).hostname == uri.hostname
|
2014-05-08 01:49:16 +08:00
|
|
|
# we don't want to pull images hosted on the main domain
|
|
|
|
return false if URI.parse(Discourse.base_url_no_prefix).hostname == uri.hostname
|
|
|
|
# check the domains blacklist
|
2014-04-22 04:59:53 +08:00
|
|
|
SiteSetting.should_download_images?(src)
|
2013-11-06 02:04:47 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|