# frozen_string_literal: true require 'uri' Dir["#{Rails.root}/lib/onebox/engine/*_onebox.rb"].sort.each { |f| require f } module Oneboxer ONEBOX_CSS_CLASS = "onebox" AUDIO_REGEX = /^\.(mp3|og[ga]|opus|wav|m4[abpr]|aac|flac)$/i VIDEO_REGEX = /^\.(mov|mp4|m4v|webm|ogv|3gp)$/i # keep reloaders happy unless defined? Oneboxer::Result Result = Struct.new(:doc, :changed) do def to_html doc.to_html end def changed? changed end end end def self.ignore_redirects @ignore_redirects ||= ['http://www.dropbox.com', 'http://store.steampowered.com', 'http://vimeo.com', Discourse.base_url] end def self.force_get_hosts @force_get_hosts ||= ['http://us.battle.net'] end def self.force_custom_user_agent_hosts @force_custom_user_agent_hosts ||= ['http://codepen.io'] end def self.allowed_post_types @allowed_post_types ||= [Post.types[:regular], Post.types[:moderator_action]] end def self.preview(url, options = nil) options ||= {} invalidate(url) if options[:invalidate_oneboxes] onebox_raw(url, options)[:preview] end def self.onebox(url, options = nil) options ||= {} invalidate(url) if options[:invalidate_oneboxes] onebox_raw(url, options)[:onebox] end def self.cached_onebox(url) if c = Discourse.cache.read(onebox_cache_key(url)) c[:onebox] end rescue => e invalidate(url) Rails.logger.warn("invalid cached onebox for #{url} #{e}") "" end def self.cached_preview(url) if c = Discourse.cache.read(onebox_cache_key(url)) c[:preview] end rescue => e invalidate(url) Rails.logger.warn("invalid cached preview for #{url} #{e}") "" end def self.invalidate(url) Discourse.cache.delete(onebox_cache_key(url)) Discourse.cache.delete(onebox_failed_cache_key(url)) end # Parse URLs out of HTML, returning the document when finished. def self.each_onebox_link(string_or_doc, extra_paths: []) doc = string_or_doc doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String) onebox_links = doc.css("a.#{ONEBOX_CSS_CLASS}", *extra_paths) if onebox_links.present? onebox_links.each do |link| yield(link['href'], link) if link['href'].present? end end doc end HTML5_BLOCK_ELEMENTS ||= %w{address article aside blockquote canvas center dd div dl dt fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hgroup hr li main nav noscript ol output p pre section table tfoot ul video} def self.apply(string_or_doc, extra_paths: nil) doc = string_or_doc doc = Nokogiri::HTML::fragment(doc) if doc.is_a?(String) changed = false each_onebox_link(doc, extra_paths: extra_paths) do |url, element| onebox, _ = yield(url, element) if onebox parsed_onebox = Nokogiri::HTML::fragment(onebox) next unless parsed_onebox.children.count > 0 if element&.parent&.node_name&.downcase == "p" && element.parent.children.count == 1 && HTML5_BLOCK_ELEMENTS.include?(parsed_onebox.children[0].node_name.downcase) element = element.parent end changed = true element.swap parsed_onebox.to_html end end # strip empty
elements doc.css("p").each { |p| p.remove if p.children.empty? } Result.new(doc, changed) end def self.is_previewing?(user_id) Discourse.redis.get(preview_key(user_id)) == "1" end def self.preview_onebox!(user_id) Discourse.redis.setex(preview_key(user_id), 1.minute, "1") end def self.onebox_previewed!(user_id) Discourse.redis.del(preview_key(user_id)) end def self.engine(url) Onebox::Matcher.new(url).oneboxed end def self.recently_failed?(url) Discourse.cache.read(onebox_failed_cache_key(url)).present? end def self.cache_failed!(url) Discourse.cache.write(onebox_failed_cache_key(url), true, expires_in: 1.hour) end private def self.preview_key(user_id) "onebox:preview:#{user_id}" end def self.blank_onebox { preview: "", onebox: "" } end def self.onebox_cache_key(url) "onebox__#{url}" end def self.onebox_failed_cache_key(url) "onebox_failed__#{url}" end def self.onebox_raw(url, opts = {}) url = URI(url).to_s local_onebox(url, opts) || external_onebox(url) rescue => e # no point warning here, just cause we have an issue oneboxing a url # we can later hunt for failed oneboxes by searching logs if needed Rails.logger.info("Failed to onebox #{url} #{e} #{e.backtrace}") # return a blank hash, so rest of the code works blank_onebox end def self.local_onebox(url, opts = {}) return unless route = Discourse.route_for(url) html = case route[:controller] when "uploads" then local_upload_html(url) when "topics" then local_topic_html(url, route, opts) when "users" then local_user_html(url, route) end html = html.presence || "#{url}" { onebox: html, preview: html } end def self.local_upload_html(url) case File.extname(URI(url).path || "") when VIDEO_REGEX <<~HTML