diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index 047c73ce026..1799033a684 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -23,6 +23,7 @@ import { notEmpty } from "@ember/object/computed"; import { setting } from "discourse/lib/computed"; import { userPath } from "discourse/lib/url"; import { helperContext } from "discourse-common/lib/helpers"; +import { emojiBasePath } from "discourse/lib/settings"; export default Controller.extend( ModalFunctionality, @@ -84,7 +85,7 @@ export default Controller.extend( // random number between 2 -6 to render multiple skin tone waving hands const random = Math.floor(Math.random() * (7 - 2) + 2); - return getURL(`/images/emoji/${emojiSet}/wave/${random}.png`); + return getURL(`${emojiBasePath()}/${emojiSet}/wave/${random}.png`); }, @discourseComputed( diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index 5d25aec81c7..a1ff3240b92 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -19,6 +19,7 @@ import { isEmpty } from "@ember/utils"; import { setting } from "discourse/lib/computed"; import showModal from "discourse/lib/show-modal"; import { helperContext } from "discourse-common/lib/helpers"; +import { emojiBasePath } from "discourse/lib/settings"; // This is happening outside of the app via popup const AuthErrors = [ @@ -71,7 +72,7 @@ export default Controller.extend(ModalFunctionality, { // random number between 2 -6 to render multiple skin tone waving hands const random = Math.floor(Math.random() * (7 - 2) + 2); - return getURL(`/images/emoji/${emojiSet}/wave/${random}.png`); + return getURL(`${emojiBasePath()}/${emojiSet}/wave/${random}.png`); }, @discourseComputed("showSecondFactor", "showSecurityKey") diff --git a/app/assets/javascripts/discourse/app/lib/settings.js b/app/assets/javascripts/discourse/app/lib/settings.js index 2de9fb7f352..4351817c129 100644 --- a/app/assets/javascripts/discourse/app/lib/settings.js +++ b/app/assets/javascripts/discourse/app/lib/settings.js @@ -7,3 +7,11 @@ export function prioritizeNameInUx(name) { !siteSettings.prioritize_username_in_ux && name && name.trim().length > 0 ); } + +export function emojiBasePath() { + let siteSettings = helperContext().siteSettings; + + return siteSettings.external_emoji_url === "" + ? "/images/emojis" + : siteSettings.external_emoji_url; +} diff --git a/app/assets/javascripts/discourse/app/lib/text.js b/app/assets/javascripts/discourse/app/lib/text.js index 588d0dab3d4..f12acbfab7e 100644 --- a/app/assets/javascripts/discourse/app/lib/text.js +++ b/app/assets/javascripts/discourse/app/lib/text.js @@ -90,6 +90,7 @@ function emojiOptions() { emojiSet: siteSettings.emoji_set, enableEmojiShortcuts: siteSettings.enable_emoji_shortcuts, inlineEmoji: siteSettings.enable_inline_emoji_translation, + emojiCDNUrl: siteSettings.external_emoji_url, }; } diff --git a/app/assets/javascripts/discourse/tests/helpers/site-settings.js b/app/assets/javascripts/discourse/tests/helpers/site-settings.js index 74de61bff18..3726d181087 100644 --- a/app/assets/javascripts/discourse/tests/helpers/site-settings.js +++ b/app/assets/javascripts/discourse/tests/helpers/site-settings.js @@ -97,6 +97,7 @@ const ORIGINAL_SETTINGS = { enable_personal_messages: true, unicode_usernames: false, secure_media: false, + external_emoji_url: "", }; let siteSettings = Object.assign({}, ORIGINAL_SETTINGS); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js index a00ad68b541..a02cf063b22 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js @@ -17,6 +17,7 @@ const rawOpts = { enable_emoji_shortcuts: true, enable_mentions: true, emoji_set: "google_classic", + external_emoji_url: "", highlighted_languages: "json|ruby|javascript", default_code_lang: "auto", enable_markdown_linkify: true, @@ -1519,6 +1520,19 @@ var bar = 'bar'; ); }); + test("emoji - emojiCDN", function (assert) { + assert.cookedOptions( + ":smile:", + { + siteSettings: { + emoji_set: "twitter", + external_emoji_url: "https://emoji.hosting.service", + }, + }, + `

:smile:

` + ); + }); + test("emoji - registerEmoji", function (assert) { registerEmoji("foo", "/images/d-logo-sketch.png"); diff --git a/app/assets/javascripts/pretty-text/addon/emoji.js b/app/assets/javascripts/pretty-text/addon/emoji.js index 09c9f773b6f..2237fe851d1 100644 --- a/app/assets/javascripts/pretty-text/addon/emoji.js +++ b/app/assets/javascripts/pretty-text/addon/emoji.js @@ -180,6 +180,12 @@ export function buildEmojiUrl(code, opts) { } const noToneMatch = code.match(/([^:]+):?/); + + let emojiBasePath = "/images/emoji"; + if (opts.emojiCDNUrl) { + emojiBasePath = opts.emojiCDNUrl; + } + if ( noToneMatch && !url && @@ -187,7 +193,7 @@ export function buildEmojiUrl(code, opts) { aliasHash.hasOwnProperty(noToneMatch[1])) ) { url = opts.getURL( - `/images/emoji/${opts.emojiSet}/${code.replace(/:t/, "/")}.png` + `${emojiBasePath}/${opts.emojiSet}/${code.replace(/:t/, "/")}.png` ); } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js index 56e3f2d9108..0806a97b33a 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js @@ -322,6 +322,7 @@ export function setup(helper) { opts.features.inlineEmoji = !!siteSettings.enable_inline_emoji_translation; opts.emojiSet = siteSettings.emoji_set || ""; opts.customEmoji = state.customEmoji; + opts.emojiCDNUrl = siteSettings.external_emoji_url; }); helper.registerPlugin((md) => { diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js index 45dc70ebcd0..a58bf5298f4 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js @@ -124,6 +124,7 @@ const rule = { title = performEmojiUnescape(topicInfo.title, { getURL: options.getURL, emojiSet: options.emojiSet, + emojiCDNUrl: options.emojiCDNUrl, enableEmojiShortcuts: options.enableEmojiShortcuts, inlineEmoji: options.inlineEmoji, }); @@ -160,6 +161,7 @@ export function setup(helper) { helper.registerOptions((opts, siteSettings) => { opts.enableEmoji = siteSettings.enable_emoji; opts.emojiSet = siteSettings.emoji_set; + opts.emojiCDNUrl = siteSettings.external_emoji_url; opts.enableEmojiShortcuts = siteSettings.enable_emoji_shortcuts; opts.inlineEmoji = siteSettings.enable_inline_emoji_translation; }); diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 83c9705d839..2cfe9469828 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -194,6 +194,7 @@ module Jobs local_bases = [ Discourse.base_url, Discourse.asset_host, + SiteSetting.external_emoji_url.presence ].compact.map { |s| normalize_src(s) } if Discourse.store.has_been_uploaded?(src) || normalize_src(src).start_with?(*local_bases) || src =~ /\A\/[^\/]/i diff --git a/app/models/emoji.rb b/app/models/emoji.rb index b4698c568e9..4a9012c3071 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -73,7 +73,11 @@ class Emoji def self.url_for(name) name = name.delete_prefix(':').delete_suffix(':').gsub(/(.+):t([1-6])/, '\1/\2') - "#{Discourse.base_path}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}" + if SiteSetting.external_emoji_url.blank? + "#{Discourse.base_path}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}" + else + "#{SiteSetting.external_emoji_url}/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}" + end end def self.cache_key(name) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 659510261c8..562928b8b0a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1782,6 +1782,7 @@ en: external_system_avatars_enabled: "Use external system avatars service." external_system_avatars_url: "URL of the external system avatars service. Allowed substitutions are {username} {first_letter} {color} {size}" + external_emoji_url: "URL of the external service for emoji images. Leave blank to disable." use_site_small_logo_as_system_avatar: "Use the site's small logo instead of the system user's avatar. Requires the logo to be present." restrict_letter_avatar_colors: "A list of 6-digit hexadecimal color values to be used for letter avatar background." diff --git a/config/site_settings.yml b/config/site_settings.yml index f870b7189f6..aa6e6002751 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1339,6 +1339,9 @@ files: default: "/letter_avatar_proxy/v4/letter/{first_letter}/{color}/{size}.png" client: true regex: '^((https?:)?\/)?\/.+[^\/]' + external_emoji_url: + default: "" + client: true restrict_letter_avatar_colors: default: "" type: list diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 399403402a7..ae666bc269d 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -238,6 +238,7 @@ module PrettyText __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set}, + emojiCDNUrl: #{SiteSetting.external_emoji_url.blank? ? "''" : SiteSetting.external_emoji_url}, customEmoji: #{custom}, enableEmojiShortcuts: #{SiteSetting.enable_emoji_shortcuts}, inlineEmoji: #{SiteSetting.enable_inline_emoji_translation} diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 814ee851657..c0ae2ac6a05 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -151,6 +151,45 @@ describe PrettyText do expect(cook(md)).to eq(html.strip) end + + it "does use emoji CDN when enabled" do + SiteSetting.external_emoji_url = "https://emoji.cdn.com" + + html = <<~HTML +
+

This is a quote with a regular emoji :upside_down_face:

+
+
+

This is a quote with an emoji shortcut :slight_smile:

+
+
+

This is a quote with a Unicode emoji :sunglasses:

+
+ HTML + + expect(cook(md)).to eq(html.strip) + end + + it "does use emoji CDN when others CDNs are also enabled" do + set_cdn_url('https://cdn.com') + setup_s3 + SiteSetting.s3_cdn_url = "https://s3.cdn.com" + SiteSetting.external_emoji_url = "https://emoji.cdn.com" + + html = <<~HTML +
+

This is a quote with a regular emoji :upside_down_face:

+
+
+

This is a quote with an emoji shortcut :slight_smile:

+
+
+

This is a quote with a Unicode emoji :sunglasses:

+
+ HTML + + expect(cook(md)).to eq(html.strip) + end end it "do off topic quoting of posts from secure categories" do diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index 64ed3f3b7b1..3faa9195b81 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -440,6 +440,23 @@ describe Jobs::PullHotlinkedImages do expect(subject.should_download_image?(src)).to eq(false) end + it "returns false for emoji when emoji CDN configured" do + SiteSetting.external_emoji_url = "https://emoji.cdn.com" + + src = UrlHelper.cook_url(Emoji.url_for("testemoji.png")) + expect(subject.should_download_image?(src)).to eq(false) + end + + it "returns false for emoji when app, S3 *and* emoji CDNs configured" do + setup_s3 + SiteSetting.s3_cdn_url = "https://s3.cdn.com" + SiteSetting.external_emoji_url = "https://emoji.cdn.com" + set_cdn_url "https://mydomain.cdn/test" + + src = UrlHelper.cook_url(Emoji.url_for("testemoji.png")) + expect(subject.should_download_image?(src)).to eq(false) + end + it "returns false for plugin assets" do src = UrlHelper.cook_url("/plugins/discourse-amazing-plugin/myasset.png") expect(subject.should_download_image?(src)).to eq(false)