1 # frozen_string_literal: true
3 class FetchLinkCardService
< BaseService
6 (https
?:\
/\/) # $2 Protocol (required)
7 (#{Twitter::Regex[:valid_domain]}) # $3 Domain(s)
8 (?::(#{Twitter::Regex[:valid_port_number]}))? # $4 Port number (optional)
9 (/#{Twitter::Regex[:valid_url_path]}*)? # $5 URL Path and anchor
10 (\?#{Twitter::Regex[:valid_url_query_chars]}*#{Twitter::Regex[:valid_url_query_ending_chars]})? # $6 Query String
18 return if @url.nil? || @status.preview_cards
.any
?
20 @mentions = status
.mentions
23 RedisLock
.acquire(lock_options
) do |lock
|
25 @card = PreviewCard
.find_by(url
: @url)
26 process_url
if @card.nil? || @card.updated_at
<= 2.weeks
.ago
28 raise Mastodon
::RaceConditionError
32 attach_card
if @card&.persisted
?
33 rescue HTTP
::Error, Addressable
::URI::InvalidURIError, Mastodon
::HostValidationError, Mastodon
::LengthValidationError => e
34 Rails
.logger
.debug
"Error fetching link #{@url}: #{e}"
41 @card ||= PreviewCard
.new(url
: @url)
43 failed
= Request
.new(:head, @url).perform
do |res
|
44 res
.code !
= 405 && res
.code !
= 501 && (res
.code !
= 200 || res
.mime_type !
= 'text/html')
49 Request
.new(:get, @url).perform
do |res
|
50 if res
.code
== 200 && res
.mime_type
== 'text/html'
51 @html = res
.body_with_limit
52 @html_charset = res
.charset
61 attempt_oembed
|| attempt_opengraph
65 @status.preview_cards
<< @card
66 Rails
.cache
.delete(@status)
71 urls
= @status.text
.scan(URL_PATTERN
).map
{ |array
| Addressable
::URI.parse(array
[0]).normalize
}
73 html
= Nokogiri
::HTML(@status.text
)
75 urls
= links
.map
{ |a
| Addressable
::URI.parse(a
['href']).normalize
unless skip_link
?(a
) }.compact
78 urls
.reject
{ |uri
| bad_url
?(uri
) }.first
82 # Avoid local instance URLs and invalid URLs
83 uri
.host
.blank
? || TagManager
.instance
.local_url
?(uri
.to_s
) || !
%w(http https
).include?(uri
.scheme
)
87 return false if @mentions.nil?
88 @mentions.any
? do |mention
|
89 a
['href'] == TagManager
.instance
.url_for(mention
.target
)
94 # Avoid links for hashtags and mentions (microformats)
95 a
['rel']&.include?('tag') || a
['class']&.include?('u-url') || mention_link
?(a
)
99 service
= FetchOEmbedService
.new
100 embed
= service
.call(@url, html
: @html)
101 url
= Addressable
::URI.parse(service
.endpoint_url
)
103 return false if embed
.nil?
105 @card.type
= embed
[:type]
106 @card.title
= embed
[:title] || ''
107 @card.author_name
= embed
[:author_name] || ''
108 @card.author_url
= embed
[:author_url].present
? ? (url + embed
[:author_url]).to_s
: ''
109 @card.provider_name
= embed
[:provider_name] || ''
110 @card.provider_url
= embed
[:provider_url].present
? ? (url + embed
[:provider_url]).to_s
: ''
116 @card.image_remote_url
= (url + embed
[:thumbnail_url]).to_s
if embed
[:thumbnail_url].present
?
118 return false if embed
[:url].blank
?
120 @card.embed_url
= (url + embed
[:url]).to_s
121 @card.image_remote_url
= (url + embed
[:url]).to_s
122 @card.width
= embed
[:width].presence
|| 0
123 @card.height
= embed
[:height].presence
|| 0
125 @card.width
= embed
[:width].presence
|| 0
126 @card.height
= embed
[:height].presence
|| 0
127 @card.html
= Formatter
.instance
.sanitize(embed
[:html], Sanitize
::Config::MASTODON_OEMBED)
128 @card.image_remote_url
= (url + embed
[:thumbnail_url]).to_s
if embed
[:thumbnail_url].present
?
130 # Most providers rely on <script> tags, which is a no-no
134 @card.save_with_optional_image!
137 def attempt_opengraph
138 detector
= CharlockHolmes
::EncodingDetector.new
139 detector
.strip_tags
= true
141 guess
= detector
.detect(@html, @html_charset)
142 page
= Nokogiri
::HTML(@html, nil, guess
&.fetch(:encoding, nil))
144 if meta_property(page
, 'twitter:player')
146 @card.width
= meta_property(page
, 'twitter:player:width') || 0
147 @card.height
= meta_property(page
, 'twitter:player:height') || 0
148 @card.html
= content_tag(:iframe, nil, src
: meta_property(page
, 'twitter:player'),
150 height
: @card.height
,
151 allowtransparency
: 'true',
158 @card.title
= meta_property(page
, 'og:title').presence
|| page
.at_xpath('//title')&.content
|| ''
159 @card.description
= meta_property(page
, 'og:description').presence
|| meta_property(page
, 'description') || ''
160 @card.image_remote_url
= (Addressable
::URI.parse(@url) +
meta_property(page
, 'og:image')).to_s
if meta_property(page
, 'og:image')
162 return if @card.title
.blank
? && @card.html
.blank
?
164 @card.save_with_optional_image!
167 def meta_property(page
, property
)
168 page
.at_xpath("//meta[@property=\"#{property}\"]")&.attribute('content')&.value
|| page
.at_xpath("//meta[@name=\"#{property}\"]")&.attribute('content')&.value
172 { redis
: Redis
.current
, key
: "fetch:#{@url}" }