FIX: Make discobot certificate faster/non blocking (#11344)

This moves the way we add the user avatar and site logo
to the discobot certificates from embeded base64 png to
just using the files urls in the href to the image tag.

This will make generation faster and the certificate
smaller overall, but it can't be used in a  `img` tag
anymore, since SVGs in `img` tags don't load the external images

In order to work around that we will move the certificate
in posts to an iframe, which works fine without any user
visible changes. For this to be possible the plugin automatically
adds the site current domain to the list of allowed iframe origins.
This commit is contained in:
Rafael dos Santos Silva 2021-02-01 20:49:32 -03:00 committed by GitHub
parent ca4a962766
commit bf5611f7eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 33 additions and 64 deletions

View File

@ -137,7 +137,7 @@ module DiscourseNarrativeBot
src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options) src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options)
alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt")) alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt"))
"<img class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{alt}'>" "<iframe class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{alt}'>"
end end
protected protected

View File

@ -2,9 +2,9 @@
module DiscourseNarrativeBot module DiscourseNarrativeBot
class CertificateGenerator class CertificateGenerator
def initialize(user, date, avatar_data) def initialize(user, date, avatar_url)
@user = user @user = user
@avatar_data = avatar_data @avatar_url = avatar_url
date = date =
begin begin
@ -23,14 +23,14 @@ module DiscourseNarrativeBot
def new_user_track def new_user_track
svg_default_width = 538.583 svg_default_width = 538.583
logo_container = logo_group(55, svg_default_width, 350) logo_container = logo_group(55, svg_default_width, 280)
ApplicationController.render(inline: read_template('new_user'), assigns: assign_options(svg_default_width, logo_container)) ApplicationController.render(inline: read_template('new_user'), assigns: assign_options(svg_default_width, logo_container))
end end
def advanced_user_track def advanced_user_track
svg_default_width = 722.8 svg_default_width = 722.8
logo_container = logo_group(40, svg_default_width, 280) logo_container = logo_group(40, svg_default_width, 350)
ApplicationController.render(inline: read_template('advanced_user'), assigns: assign_options(svg_default_width, logo_container)) ApplicationController.render(inline: read_template('advanced_user'), assigns: assign_options(svg_default_width, logo_container))
end end
@ -46,7 +46,7 @@ module DiscourseNarrativeBot
width: width, width: width,
discobot_user: @discobot_user, discobot_user: @discobot_user,
date: @date, date: @date,
avatar_url: base64_image_data(@avatar_data), avatar_url: @avatar_url,
logo_group: logo_group, logo_group: logo_group,
name: name name: name
} }
@ -59,48 +59,16 @@ module DiscourseNarrativeBot
def logo_group(size, width, height) def logo_group(size, width, height)
return unless SiteSetting.site_logo_small_url.present? return unless SiteSetting.site_logo_small_url.present?
begin uri = URI(SiteSetting.site_logo_small_url)
uri = URI(SiteSetting.site_logo_small_url)
logo_uri = logo_uri =
if uri.host.blank? || uri.scheme.blank? if uri.host.blank? || uri.scheme.blank?
URI("#{Discourse.base_url}/#{uri.path}") URI("#{Discourse.base_url}/#{uri.path}")
else else
uri uri
end end
<<~URL { size: size, width: width, height: height, logo_uri: logo_uri }
<g transform="translate(#{width / 2 - (size / 2)} #{height})">
<image height="#{size}px" width="#{size}px" #{base64_image_link(logo_uri)}/>
</g>
URL
rescue URI::InvalidURIError
''
end
end
def base64_image_data(data)
return "" if data.blank?
"xlink:href=\"data:image/png;base64,#{Base64.strict_encode64(data)}\""
end
def base64_image_link(url)
if image = fetch_image(url)
base64_image_data(image)
else
""
end
end
def fetch_image(url)
FileHelper.download(
url.to_s,
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
tmp_file_name: 'narrative-bot-logo',
follow_redirect: true
)&.read
rescue OpenURI::HTTPError
# Ignore if fetching image returns a non 200 response
end end
end end
end end

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 729 KiB

After

Width:  |  Height:  |  Size: 729 KiB

View File

@ -452,12 +452,14 @@
<clipPath id="clipCircle"> <clipPath id="clipCircle">
<circle r="15" cx="15" cy="15"/> <circle r="15" cx="15" cy="15"/>
</clipPath> </clipPath>
<image clip-path="url(#clipCircle)" height="30px" width="30px" <%= sanitize(@avatar_url) %>/> <image clip-path="url(#clipCircle)" height="30px" width="30px" href="<%= @avatar_url %>"/>
</g> </g>
<text x="<%= @width / 2 %>" y="240.94" text-anchor="middle" font-size="24" fill="#020403" font-family="Tangerine, Tangerine"> <text x="<%= @width / 2 %>" y="240.94" text-anchor="middle" font-size="24" fill="#020403" font-family="Tangerine, Tangerine">
<%= @name %> <%= @name %>
</text> </text>
<%== @logo_group %> <g transform="translate(<%= @logo_group[:width] / 2 - (@logo_group[:size] / 2) %> <%= @logo_group[:height] %>)">
<image height="<%= @logo_group[:size] %>px" width="<%= @logo_group[:size] %>px" href="<%= @logo_group[:logo_uri] %>"/>
</g>
</g> </g>
<g id="Layer_1-2" data-name="Layer 1"> <g id="Layer_1-2" data-name="Layer 1">
<path d="M339.328 47.187c-.034 1.193 3.25 2.2 6.51 3.383 5.5 1.994 13.1 3.73 17.68 3.112 5.05-.55 4.982-2.217 4.054-4.8-.1-4.647.258-9.493.156-14.142l-27.128-.458c.5 4.698-1.778 8.207-1.272 12.905z" transform="translate(-.108)" fill-rule="evenodd" fill="url(#GradientFill_24)"/> <path d="M339.328 47.187c-.034 1.193 3.25 2.2 6.51 3.383 5.5 1.994 13.1 3.73 17.68 3.112 5.05-.55 4.982-2.217 4.054-4.8-.1-4.647.258-9.493.156-14.142l-27.128-.458c.5 4.698-1.778 8.207-1.272 12.905z" transform="translate(-.108)" fill-rule="evenodd" fill="url(#GradientFill_24)"/>

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 495 KiB

View File

@ -50,6 +50,11 @@ after_initialize do
# Disable welcome message because that is what the bot is supposed to replace. # Disable welcome message because that is what the bot is supposed to replace.
SiteSetting.send_welcome_message = false if SiteSetting.send_welcome_message SiteSetting.send_welcome_message = false if SiteSetting.send_welcome_message
certificate_path = "#{Discourse.base_url}/discobot/certificate.svg"
if SiteSetting.discourse_narrative_bot_enabled && !SiteSetting.allowed_iframes.include?(certificate_path)
SiteSetting.allowed_iframes = SiteSetting.allowed_iframes.split('|').append("#{Discourse.base_url}/discobot/certificate.svg").join('|')
end
require_dependency 'plugin_store' require_dependency 'plugin_store'
module ::DiscourseNarrativeBot module ::DiscourseNarrativeBot
@ -94,8 +99,7 @@ after_initialize do
raise Discourse::NotFound if user.blank? raise Discourse::NotFound if user.blank?
hijack do hijack do
avatar_data = fetch_avatar(user) generator = CertificateGenerator.new(user, params[:date], avatar_url(user))
generator = CertificateGenerator.new(user, params[:date], avatar_data)
svg = params[:type] == 'advanced' ? generator.advanced_user_track : generator.new_user_track svg = params[:type] == 'advanced' ? generator.advanced_user_track : generator.new_user_track
@ -107,16 +111,8 @@ after_initialize do
private private
def fetch_avatar(user) def avatar_url(user)
avatar_url = UrlHelper.absolute(Discourse.base_path + user.avatar_template.gsub('{size}', '250')) UrlHelper.absolute(Discourse.base_path + user.avatar_template.gsub('{size}', '250'))
FileHelper.download(
avatar_url.to_s,
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
tmp_file_name: 'narrative-bot-avatar',
follow_redirect: true
)&.read
rescue OpenURI::HTTPError
# Ignore if fetching image returns a non 200 response
end end
end end
end end

View File

@ -36,7 +36,9 @@ describe "Discobot Certificate" do
get '/discobot/certificate.svg', params: params get '/discobot/certificate.svg', params: params
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.body).to include('<image height="55px" width="55px" />') expect(response.body).to include('<svg')
expect(response.body).to include(user.avatar_template.gsub('{size}', '250'))
expect(response.body).to include(SiteSetting.site_logo_small_url)
end end
describe 'when params are missing' do describe 'when params are missing' do