2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
class TopicEmbed < ActiveRecord::Base
|
2017-04-25 02:29:04 +08:00
|
|
|
include Trashable
|
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
belongs_to :topic
|
|
|
|
belongs_to :post
|
|
|
|
validates_presence_of :embed_url
|
2015-06-16 00:08:55 +08:00
|
|
|
validates_uniqueness_of :embed_url
|
2014-01-01 03:37:43 +08:00
|
|
|
|
2017-04-25 02:29:04 +08:00
|
|
|
before_validation(on: :create) do
|
|
|
|
unless (topic_embed = TopicEmbed.with_deleted.where('deleted_at IS NOT NULL AND embed_url = ?', embed_url).first).nil?
|
|
|
|
topic_embed.destroy!
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-08-31 00:01:04 +08:00
|
|
|
class FetchResponse
|
|
|
|
attr_accessor :title, :body, :author
|
|
|
|
end
|
|
|
|
|
2014-03-20 04:33:21 +08:00
|
|
|
def self.normalize_url(url)
|
2014-04-04 03:35:31 +08:00
|
|
|
url.downcase.sub(/\/$/, '').sub(/\-+/, '-').strip
|
2014-03-20 04:33:21 +08:00
|
|
|
end
|
|
|
|
|
2014-04-03 03:54:21 +08:00
|
|
|
def self.imported_from_html(url)
|
|
|
|
"\n<hr>\n<small>#{I18n.t('embed.imported_from', link: "<a href='#{url}'>#{url}</a>")}</small>\n"
|
|
|
|
end
|
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
# Import an article from a source (RSS/Atom/Other)
|
|
|
|
def self.import(user, url, title, contents)
|
|
|
|
return unless url =~ /^https?\:\/\//
|
|
|
|
|
2014-03-19 06:02:33 +08:00
|
|
|
if SiteSetting.embed_truncate
|
|
|
|
contents = first_paragraph_from(contents)
|
|
|
|
end
|
2019-07-25 21:21:01 +08:00
|
|
|
contents ||= ''
|
2019-08-09 22:35:22 +08:00
|
|
|
contents = +contents << imported_from_html(url)
|
2014-01-01 03:37:43 +08:00
|
|
|
|
2014-03-27 11:24:57 +08:00
|
|
|
url = normalize_url(url)
|
|
|
|
|
2014-05-06 21:41:59 +08:00
|
|
|
embed = TopicEmbed.find_by("lower(embed_url) = ?", url)
|
2014-01-01 03:37:43 +08:00
|
|
|
content_sha1 = Digest::SHA1.hexdigest(contents)
|
|
|
|
post = nil
|
|
|
|
|
|
|
|
# If there is no embed, create a topic, post and the embed.
|
|
|
|
if embed.blank?
|
|
|
|
Topic.transaction do
|
2016-08-24 02:55:52 +08:00
|
|
|
eh = EmbeddableHost.record_for_url(url)
|
2015-08-19 05:15:46 +08:00
|
|
|
|
2018-03-11 10:26:47 +08:00
|
|
|
cook_method = if SiteSetting.embed_support_markdown
|
|
|
|
Post.cook_methods[:regular]
|
|
|
|
else
|
|
|
|
Post.cook_methods[:raw_html]
|
|
|
|
end
|
|
|
|
|
2020-04-14 03:17:02 +08:00
|
|
|
create_args = {
|
|
|
|
title: title,
|
|
|
|
raw: absolutize_urls(url, contents),
|
|
|
|
skip_validations: true,
|
|
|
|
cook_method: cook_method,
|
|
|
|
category: eh.try(:category_id)
|
|
|
|
}
|
|
|
|
if SiteSetting.embed_unlisted?
|
|
|
|
create_args[:visible] = false
|
|
|
|
end
|
|
|
|
|
|
|
|
creator = PostCreator.new(user, create_args)
|
2014-01-01 03:37:43 +08:00
|
|
|
post = creator.create
|
|
|
|
if post.present?
|
|
|
|
TopicEmbed.create!(topic_id: post.topic_id,
|
|
|
|
embed_url: url,
|
|
|
|
content_sha1: content_sha1,
|
|
|
|
post_id: post.id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
2014-03-19 06:02:33 +08:00
|
|
|
absolutize_urls(url, contents)
|
2014-01-01 03:37:43 +08:00
|
|
|
post = embed.post
|
2018-08-21 18:19:03 +08:00
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
# Update the topic if it changed
|
2018-08-21 18:19:03 +08:00
|
|
|
if post&.topic
|
|
|
|
if post.user != user
|
|
|
|
PostOwnerChanger.new(
|
|
|
|
post_ids: [post.id],
|
|
|
|
topic_id: post.topic_id,
|
|
|
|
new_owner: user,
|
|
|
|
acting_user: Discourse.system_user
|
|
|
|
).change_owner!
|
|
|
|
|
|
|
|
# make sure the post returned has the right author
|
|
|
|
post.reload
|
|
|
|
end
|
|
|
|
|
2020-04-21 02:31:24 +08:00
|
|
|
if (content_sha1 != embed.content_sha1) || (title && title != post&.topic&.title)
|
2020-04-21 02:27:43 +08:00
|
|
|
changes = { raw: absolutize_urls(url, contents) }
|
|
|
|
changes[:title] = title if title.present?
|
|
|
|
|
|
|
|
post.revise(user, changes, skip_validations: true, bypass_rate_limiter: true)
|
2018-08-28 14:25:04 +08:00
|
|
|
embed.update!(content_sha1: content_sha1)
|
2018-08-24 09:41:54 +08:00
|
|
|
end
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
post
|
|
|
|
end
|
|
|
|
|
2014-04-02 06:16:56 +08:00
|
|
|
def self.find_remote(url)
|
2014-01-01 03:37:43 +08:00
|
|
|
require 'ruby-readability'
|
|
|
|
|
2017-12-13 00:50:39 +08:00
|
|
|
url = UrlHelper.escape_uri(url)
|
2017-09-22 23:36:44 +08:00
|
|
|
original_uri = URI.parse(url)
|
2020-05-27 23:23:55 +08:00
|
|
|
fd = FinalDestination.new(
|
|
|
|
url,
|
|
|
|
validate_uri: true,
|
|
|
|
max_redirects: 5
|
|
|
|
)
|
|
|
|
|
|
|
|
url = fd.resolve
|
|
|
|
raise URI::InvalidURIError if url.blank?
|
2020-05-23 12:56:13 +08:00
|
|
|
|
2014-04-15 12:06:51 +08:00
|
|
|
opts = {
|
|
|
|
tags: %w[div p code pre h1 h2 h3 b em i strong a img ul li ol blockquote],
|
2015-09-25 06:20:59 +08:00
|
|
|
attributes: %w[href src class],
|
2014-04-15 12:06:51 +08:00
|
|
|
remove_empty_nodes: false
|
|
|
|
}
|
|
|
|
|
|
|
|
opts[:whitelist] = SiteSetting.embed_whitelist_selector if SiteSetting.embed_whitelist_selector.present?
|
|
|
|
opts[:blacklist] = SiteSetting.embed_blacklist_selector if SiteSetting.embed_blacklist_selector.present?
|
2015-09-25 06:20:59 +08:00
|
|
|
embed_classname_whitelist = SiteSetting.embed_classname_whitelist if SiteSetting.embed_classname_whitelist.present?
|
2014-04-15 12:06:51 +08:00
|
|
|
|
2016-08-31 00:01:04 +08:00
|
|
|
response = FetchResponse.new
|
2017-05-26 03:42:05 +08:00
|
|
|
begin
|
2017-09-22 23:36:44 +08:00
|
|
|
html = open(url, allow_redirections: :safe).read
|
2017-08-10 11:58:56 +08:00
|
|
|
rescue OpenURI::HTTPError, Net::OpenTimeout
|
2017-05-26 03:42:05 +08:00
|
|
|
return
|
|
|
|
end
|
2014-04-03 03:54:21 +08:00
|
|
|
|
2020-05-05 11:46:57 +08:00
|
|
|
raw_doc = Nokogiri::HTML5(html)
|
2016-08-31 00:01:04 +08:00
|
|
|
auth_element = raw_doc.at('meta[@name="author"]')
|
|
|
|
if auth_element.present?
|
|
|
|
response.author = User.where(username_lower: auth_element[:content].strip).first
|
|
|
|
end
|
|
|
|
|
|
|
|
read_doc = Readability::Document.new(html, opts)
|
|
|
|
|
2019-05-03 06:17:27 +08:00
|
|
|
title = +(raw_doc.title || '')
|
2016-08-23 00:43:02 +08:00
|
|
|
title.strip!
|
|
|
|
|
|
|
|
if SiteSetting.embed_title_scrubber.present?
|
|
|
|
title.sub!(Regexp.new(SiteSetting.embed_title_scrubber), '')
|
|
|
|
title.strip!
|
|
|
|
end
|
2016-08-31 00:01:04 +08:00
|
|
|
response.title = title
|
2020-05-05 11:46:57 +08:00
|
|
|
doc = Nokogiri::HTML5(read_doc.content)
|
2016-08-23 00:43:02 +08:00
|
|
|
|
2017-07-28 09:20:09 +08:00
|
|
|
tags = { 'img' => 'src', 'script' => 'src', 'a' => 'href' }
|
2014-04-03 03:54:21 +08:00
|
|
|
doc.search(tags.keys.join(',')).each do |node|
|
|
|
|
url_param = tags[node.name]
|
|
|
|
src = node[url_param]
|
2015-04-23 07:52:02 +08:00
|
|
|
unless (src.nil? || src.empty?)
|
2014-04-09 23:04:45 +08:00
|
|
|
begin
|
2017-12-13 00:50:39 +08:00
|
|
|
uri = URI.parse(UrlHelper.escape_uri(src))
|
2014-04-09 23:04:45 +08:00
|
|
|
unless uri.host
|
|
|
|
uri.scheme = original_uri.scheme
|
|
|
|
uri.host = original_uri.host
|
|
|
|
node[url_param] = uri.to_s
|
|
|
|
end
|
2018-08-14 18:23:32 +08:00
|
|
|
rescue URI::Error
|
2014-04-09 23:04:45 +08:00
|
|
|
# If there is a mistyped URL, just do nothing
|
2014-04-03 03:54:21 +08:00
|
|
|
end
|
|
|
|
end
|
2015-09-25 06:20:59 +08:00
|
|
|
# only allow classes in the whitelist
|
|
|
|
allowed_classes = if embed_classname_whitelist.blank? then [] else embed_classname_whitelist.split(/[ ,]+/i) end
|
2015-11-07 05:25:11 +08:00
|
|
|
doc.search('[class]:not([class=""])').each do |classnode|
|
2017-07-28 09:20:09 +08:00
|
|
|
classes = classnode[:class].split(' ').select { |classname| allowed_classes.include?(classname) }
|
2015-09-25 06:20:59 +08:00
|
|
|
if classes.length === 0
|
2015-11-07 05:25:11 +08:00
|
|
|
classnode.delete('class')
|
2015-09-25 06:20:59 +08:00
|
|
|
else
|
2015-11-07 05:25:11 +08:00
|
|
|
classnode[:class] = classes.join(' ')
|
2015-09-25 06:20:59 +08:00
|
|
|
end
|
|
|
|
end
|
2014-04-03 03:54:21 +08:00
|
|
|
end
|
|
|
|
|
2016-08-31 00:01:04 +08:00
|
|
|
response.body = doc.to_html
|
|
|
|
response
|
2014-04-02 06:16:56 +08:00
|
|
|
end
|
2014-01-01 03:37:43 +08:00
|
|
|
|
2017-07-28 09:20:09 +08:00
|
|
|
def self.import_remote(import_user, url, opts = nil)
|
2014-04-02 06:16:56 +08:00
|
|
|
opts = opts || {}
|
2016-08-31 00:01:04 +08:00
|
|
|
response = find_remote(url)
|
2017-09-22 23:36:44 +08:00
|
|
|
return if response.nil?
|
|
|
|
|
2016-08-31 00:01:04 +08:00
|
|
|
response.title = opts[:title] if opts[:title].present?
|
|
|
|
import_user = response.author if response.author.present?
|
|
|
|
|
|
|
|
TopicEmbed.import(import_user, url, response.title, response.body)
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Convert any relative URLs to absolute. RSS is annoying for this.
|
|
|
|
def self.absolutize_urls(url, contents)
|
2014-03-20 04:33:21 +08:00
|
|
|
url = normalize_url(url)
|
2020-02-07 23:54:24 +08:00
|
|
|
begin
|
|
|
|
uri = URI(UrlHelper.escape_uri(url))
|
|
|
|
rescue URI::Error
|
|
|
|
return contents
|
|
|
|
end
|
2014-01-01 03:37:43 +08:00
|
|
|
prefix = "#{uri.scheme}://#{uri.host}"
|
2020-03-25 23:57:31 +08:00
|
|
|
prefix += ":#{uri.port}" if uri.port != 80 && uri.port != 443
|
2014-01-01 03:37:43 +08:00
|
|
|
|
2020-05-05 11:46:57 +08:00
|
|
|
fragment = Nokogiri::HTML5.fragment("<div>#{contents}</div>")
|
2014-01-01 03:37:43 +08:00
|
|
|
fragment.css('a').each do |a|
|
|
|
|
href = a['href']
|
|
|
|
if href.present? && href.start_with?('/')
|
|
|
|
a['href'] = "#{prefix}/#{href.sub(/^\/+/, '')}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
fragment.css('img').each do |a|
|
|
|
|
src = a['src']
|
|
|
|
if src.present? && src.start_with?('/')
|
|
|
|
a['src'] = "#{prefix}/#{src.sub(/^\/+/, '')}"
|
|
|
|
end
|
|
|
|
end
|
2014-03-19 06:02:33 +08:00
|
|
|
fragment.at('div').inner_html
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.topic_id_for_embed(embed_url)
|
2018-01-05 02:13:17 +08:00
|
|
|
embed_url = normalize_url(embed_url).sub(/^https?\:\/\//, '')
|
2019-10-21 18:32:27 +08:00
|
|
|
TopicEmbed.where("embed_url ~* ?", "^https?://#{Regexp.escape(embed_url)}$").pluck_first(:topic_id)
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
|
|
|
|
2014-03-19 06:02:33 +08:00
|
|
|
def self.first_paragraph_from(html)
|
2020-05-05 11:46:57 +08:00
|
|
|
doc = Nokogiri::HTML5(html)
|
2014-03-19 06:02:33 +08:00
|
|
|
|
2019-05-03 06:17:27 +08:00
|
|
|
result = +""
|
2014-03-19 06:02:33 +08:00
|
|
|
doc.css('p').each do |p|
|
|
|
|
if p.text.present?
|
|
|
|
result << p.to_s
|
|
|
|
return result if result.size >= 100
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return result unless result.blank?
|
|
|
|
|
|
|
|
# If there is no first paragaph, return the first div (onebox)
|
2019-08-07 10:45:55 +08:00
|
|
|
doc.css('div').first.to_s
|
2014-03-19 06:02:33 +08:00
|
|
|
end
|
2014-04-03 23:30:43 +08:00
|
|
|
|
|
|
|
def self.expanded_for(post)
|
2019-11-27 09:35:14 +08:00
|
|
|
Discourse.cache.fetch("embed-topic:#{post.topic_id}", expires_in: 10.minutes) do
|
2019-10-21 18:32:27 +08:00
|
|
|
url = TopicEmbed.where(topic_id: post.topic_id).pluck_first(:embed_url)
|
2016-08-31 00:01:04 +08:00
|
|
|
response = TopicEmbed.find_remote(url)
|
|
|
|
|
|
|
|
body = response.body
|
2014-04-03 23:30:43 +08:00
|
|
|
body << TopicEmbed.imported_from_html(url)
|
|
|
|
body
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-01 03:37:43 +08:00
|
|
|
end
|
2014-02-07 08:07:36 +08:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: topic_embeds
|
|
|
|
#
|
2017-04-25 02:29:04 +08:00
|
|
|
# id :integer not null, primary key
|
|
|
|
# topic_id :integer not null
|
|
|
|
# post_id :integer not null
|
|
|
|
# embed_url :string(1000) not null
|
|
|
|
# content_sha1 :string(40)
|
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
|
|
|
# deleted_at :datetime
|
|
|
|
# deleted_by_id :integer
|
2014-02-07 08:07:36 +08:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
|
|
|
# index_topic_embeds_on_embed_url (embed_url) UNIQUE
|
|
|
|
#
|