You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

110 lines
2.8 KiB

  1. # frozen_string_literal: true
  2. class FetchOEmbedService
  3. ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
  4. attr_reader :url, :options, :format, :endpoint_url
  5. def call(url, options = {})
  6. @url = url
  7. @options = options
  8. if @options[:cached_endpoint]
  9. parse_cached_endpoint!
  10. else
  11. discover_endpoint!
  12. end
  13. fetch!
  14. end
  15. private
  16. def discover_endpoint!
  17. return if html.nil?
  18. @format = @options[:format]
  19. page = Nokogiri::HTML(html)
  20. if @format.nil? || @format == :json
  21. @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]')&.attribute('href')&.value
  22. @format ||= :json if @endpoint_url
  23. end
  24. if @format.nil? || @format == :xml
  25. @endpoint_url ||= page.at_xpath('//link[@type="text/xml+oembed"]')&.attribute('href')&.value
  26. @format ||= :xml if @endpoint_url
  27. end
  28. return if @endpoint_url.blank?
  29. @endpoint_url = begin
  30. base_url = Addressable::URI.parse(@url)
  31. # If the OEmbed endpoint is given as http but the URL we opened
  32. # was served over https, we can assume OEmbed will be available
  33. # through https as well
  34. (base_url + @endpoint_url).tap do |absolute_url|
  35. absolute_url.scheme = base_url.scheme if base_url.scheme == 'https'
  36. end.to_s
  37. end
  38. cache_endpoint!
  39. rescue Addressable::URI::InvalidURIError
  40. @endpoint_url = nil
  41. end
  42. def parse_cached_endpoint!
  43. cached = @options[:cached_endpoint]
  44. return if cached[:endpoint].nil? || cached[:format].nil?
  45. @endpoint_url = Addressable::Template.new(cached[:endpoint]).expand(url: @url).to_s
  46. @format = cached[:format]
  47. end
  48. def cache_endpoint!
  49. url_domain = Addressable::URI.parse(@url).normalized_host
  50. endpoint_hash = {
  51. endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
  52. format: @format,
  53. }
  54. Rails.cache.write("oembed_endpoint:#{url_domain}", endpoint_hash, expires_in: ENDPOINT_CACHE_EXPIRES_IN)
  55. end
  56. def fetch!
  57. return if @endpoint_url.blank?
  58. body = Request.new(:get, @endpoint_url).perform do |res|
  59. res.code != 200 ? nil : res.body_with_limit
  60. end
  61. validate(parse_for_format(body)) if body.present?
  62. rescue Oj::ParseError, Ox::ParseError
  63. nil
  64. end
  65. def parse_for_format(body)
  66. case @format
  67. when :json
  68. Oj.load(body, mode: :strict)&.with_indifferent_access
  69. when :xml
  70. Ox.load(body, mode: :hash_no_attrs)&.with_indifferent_access&.dig(:oembed)
  71. end
  72. end
  73. def validate(oembed)
  74. oembed if oembed[:version] == '1.0' && oembed[:type].present?
  75. end
  76. def html
  77. return @html if defined?(@html)
  78. @html = @options[:html] || Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
  79. res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
  80. end
  81. end
  82. end