PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/app/jobs/regular/pull_hotlinked_images.rb

https://gitlab.com/Ruwan-Ranganath/discourse
Ruby | 119 lines | 84 code | 16 blank | 19 comment | 23 complexity | 90508160c2ff2369d9d139cd9b91efe4 MD5 | raw file
  1. require_dependency 'url_helper'
  2. require_dependency 'file_helper'
  3. module Jobs
  4. class PullHotlinkedImages < Jobs::Base
  5. sidekiq_options queue: 'low'
  6. def initialize
  7. # maximum size of the file in bytes
  8. @max_size = SiteSetting.max_image_size_kb.kilobytes
  9. end
  10. def execute(args)
  11. return unless SiteSetting.download_remote_images_to_local?
  12. post_id = args[:post_id]
  13. raise Discourse::InvalidParameters.new(:post_id) unless post_id.present?
  14. post = Post.find_by(id: post_id)
  15. return unless post.present?
  16. raw = post.raw.dup
  17. start_raw = raw.dup
  18. downloaded_urls = {}
  19. extract_images_from(post.cooked).each do |image|
  20. src = image['src']
  21. src = "http:" + src if src.start_with?("//")
  22. if is_valid_image_url(src)
  23. hotlinked = nil
  24. begin
  25. # have we already downloaded that file?
  26. unless downloaded_urls.include?(src)
  27. begin
  28. hotlinked = FileHelper.download(src, @max_size, "discourse-hotlinked", true)
  29. rescue Discourse::InvalidParameters
  30. end
  31. if hotlinked
  32. if File.size(hotlinked.path) <= @max_size
  33. filename = File.basename(URI.parse(src).path)
  34. upload = Upload.create_for(post.user_id, hotlinked, filename, File.size(hotlinked.path), { origin: src })
  35. downloaded_urls[src] = upload.url
  36. else
  37. Rails.logger.info("Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}")
  38. end
  39. else
  40. Rails.logger.error("There was an error while downloading '#{src}' locally for post: #{post_id}")
  41. end
  42. end
  43. # have we successfully downloaded that file?
  44. if downloaded_urls[src].present?
  45. url = downloaded_urls[src]
  46. escaped_src = Regexp.escape(src)
  47. # there are 6 ways to insert an image in a post
  48. # HTML tag - <img src="http://...">
  49. raw.gsub!(/src=["']#{escaped_src}["']/i, "src='#{url}'")
  50. # BBCode tag - [img]http://...[/img]
  51. raw.gsub!(/\[img\]#{escaped_src}\[\/img\]/i, "[img]#{url}[/img]")
  52. # Markdown linked image - [![alt](http://...)](http://...)
  53. raw.gsub!(/\[!\[([^\]]*)\]\(#{escaped_src}\)\]/) { "[<img src='#{url}' alt='#{$1}'>]" }
  54. # Markdown inline - ![alt](http://...)
  55. raw.gsub!(/!\[([^\]]*)\]\(#{escaped_src}\)/) { "![#{$1}](#{url})" }
  56. # Markdown reference - [x]: http://
  57. raw.gsub!(/\[([^\]]+)\]:\s?#{escaped_src}/) { "[#{$1}]: #{url}" }
  58. # Direct link
  59. raw.gsub!(/^#{escaped_src}(\s?)$/) { "<img src='#{url}'>#{$1}" }
  60. end
  61. rescue => e
  62. Rails.logger.info("Failed to pull hotlinked image: #{src} post:#{post_id}\n" + e.message + "\n" + e.backtrace.join("\n"))
  63. ensure
  64. # close & delete the temp file
  65. hotlinked && hotlinked.close!
  66. end
  67. end
  68. end
  69. post.reload
  70. if start_raw == post.raw && raw != post.raw
  71. changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") }
  72. # we never want that job to bump the topic
  73. options = { bypass_bump: true }
  74. post.revise(Discourse.system_user, changes, options)
  75. end
  76. end
  77. def extract_images_from(html)
  78. doc = Nokogiri::HTML::fragment(html)
  79. doc.css("img[src]") - doc.css(".onebox-result img") - doc.css("img.avatar")
  80. end
  81. def is_valid_image_url(src)
  82. # make sure we actually have a url
  83. return false unless src.present?
  84. # we don't want to pull uploaded images
  85. return false if Discourse.store.has_been_uploaded?(src)
  86. # we don't want to pull relative images
  87. return false if src =~ /\A\/[^\/]/i
  88. # parse the src
  89. begin
  90. uri = URI.parse(src)
  91. rescue URI::InvalidURIError
  92. return false
  93. end
  94. # we don't want to pull images hosted on the CDN (if we use one)
  95. return false if Discourse.asset_host.present? && URI.parse(Discourse.asset_host).hostname == uri.hostname
  96. return false if SiteSetting.s3_cdn_url.present? && URI.parse(SiteSetting.s3_cdn_url).hostname == uri.hostname
  97. # we don't want to pull images hosted on the main domain
  98. return false if URI.parse(Discourse.base_url_no_prefix).hostname == uri.hostname
  99. # check the domains blacklist
  100. SiteSetting.should_download_images?(src)
  101. end
  102. end
  103. end