From 8f379cfae7168ef176d3312a8241a84e6cf375f1 Mon Sep 17 00:00:00 2001 From: Alteras1 Date: Fri, 18 Sep 2026 22:42:29 +0000 Subject: [PATCH 1/2] FIX: restricts CSS injection code to within the post boundaries. --- assets/stylesheets/common/containment.scss | 10 ++++++++++ assets/stylesheets/common/index.scss | 5 +---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 assets/stylesheets/common/containment.scss diff --git a/assets/stylesheets/common/containment.scss b/assets/stylesheets/common/containment.scss new file mode 100644 index 0000000..003b894 --- /dev/null +++ b/assets/stylesheets/common/containment.scss @@ -0,0 +1,10 @@ +%bbcode-post-containment { + clip-path: border-box; + contain: layout; + position: relative; +} + +.cooked, +.d-editor-preview { + @extend %bbcode-post-containment; +} diff --git a/assets/stylesheets/common/index.scss b/assets/stylesheets/common/index.scss index 782fa6e..c6b2ce5 100644 --- a/assets/stylesheets/common/index.scss +++ b/assets/stylesheets/common/index.scss @@ -4,6 +4,7 @@ @import "blockquote"; @import "border-bg"; @import "check"; +@import "containment"; @import "divide"; @import "fieldset"; @import "heightrestrict"; @@ -26,7 +27,3 @@ @import "spoiler"; @import "tabs"; @import "textmessage"; - -.d-editor-preview { - position: relative; -} From 3afd975ebfcfd91b6f52db92110488c20846fd32 Mon Sep 17 00:00:00 2001 From: Alteras1 Date: Sat, 19 Sep 2026 00:12:45 +0000 Subject: [PATCH 2/2] FEAT: Rehost external images referenced in BBCode css url() This change extends core's `HotlinkedMedia`/`PostHotlinkedMedia` pipeline (via a plugin-local prepend, no core changes) to also discover and download CSS `url()` references, rewrites both `post.cooked` and `post.raw` to point at the resulting local upload the same way core does for images, and resolves leftover `upload://` references client-side so the composer preview renders correctly too. --- .../bbcode-class-styles.js} | 16 ++- .../initializers/bbcode-css-upload-urls.js | 115 ++++++++++++++++ lib/bb_code/css_hotlinked_media.rb | 125 ++++++++++++++++++ plugin.rb | 6 + 4 files changed, 258 insertions(+), 4 deletions(-) rename assets/javascripts/discourse/{api-initializers/class.js => initializers/bbcode-class-styles.js} (77%) create mode 100644 assets/javascripts/discourse/initializers/bbcode-css-upload-urls.js create mode 100644 lib/bb_code/css_hotlinked_media.rb diff --git a/assets/javascripts/discourse/api-initializers/class.js b/assets/javascripts/discourse/initializers/bbcode-class-styles.js similarity index 77% rename from assets/javascripts/discourse/api-initializers/class.js rename to assets/javascripts/discourse/initializers/bbcode-class-styles.js index 2ef1b19..2540b6f 100644 --- a/assets/javascripts/discourse/api-initializers/class.js +++ b/assets/javascripts/discourse/initializers/bbcode-class-styles.js @@ -1,7 +1,7 @@ /** * @file Initializes and adds any custom user styles to the post */ -import { apiInitializer } from "discourse/lib/api"; +import { withPluginApi } from "discourse/lib/plugin-api"; /** * Adds the inline styles for the style tag inside a given post @@ -30,6 +30,14 @@ function addClassStyleCode(post) { }); } -export default apiInitializer((api) => { - api.decorateCookedElement(addClassStyleCode, { id: "add class style code" }); -}); +export default { + name: "bbcode-class-styles", + after: "inject-objects", + initialize() { + withPluginApi((api) => { + api.decorateCookedElement(addClassStyleCode, { + id: "add class style code", + }); + }); + }, +}; diff --git a/assets/javascripts/discourse/initializers/bbcode-css-upload-urls.js b/assets/javascripts/discourse/initializers/bbcode-css-upload-urls.js new file mode 100644 index 0000000..a1de89d --- /dev/null +++ b/assets/javascripts/discourse/initializers/bbcode-css-upload-urls.js @@ -0,0 +1,115 @@ +/** + * @file Resolves upload:// references left behind in bbcode-generated CSS by + * client-side cooking (e.g. the composer preview): the server resolves these + * at cook time, but the preview's markdown-it engine has no upload lookup, so + * it cooks the literal upload:// text through unresolved. + */ +import { + lookupCachedUploadUrl, + lookupUncachedUploadUrls, + MISSING, +} from "pretty-text/upload-short-url"; +import { ajax } from "discourse/lib/ajax"; +import discourseDebounce from "discourse/lib/debounce"; +import { withPluginApi } from "discourse/lib/plugin-api"; + +const UPLOAD_URL_REGEX = /url\(\s*(['"]?)(upload:\/\/[^'")]+)\1\s*\)/gi; + +let queuedShortUrls; +let queuePromise; +let queueResolve; + +function queuePop() { + lookupUncachedUploadUrls(queuedShortUrls, ajax).then(queueResolve); + queuedShortUrls = queueResolve = null; +} + +// Batches short-url lookups across a debounce window (matching core's own +// composer-preview resolver) so multiple decorated posts on the same page +// share one request instead of firing one each. +function resolveUncachedShortUrls(shortUrls) { + if (!queuedShortUrls) { + queuedShortUrls = [...shortUrls]; + queuePromise = new Promise((resolve) => (queueResolve = resolve)); + discourseDebounce(null, queuePop, 450); + } else { + queuedShortUrls.push(...shortUrls); + } + return queuePromise; +} + +function cssOf(node) { + return node.tagName === "STYLE" + ? node.textContent + : node.getAttribute("style"); +} + +function setCssOf(node, css) { + if (node.tagName === "STYLE") { + node.textContent = css; + } else { + node.setAttribute("style", css); + } +} + +/** + * @param {HTMLElement} post the post itself + */ +async function resolveCssUploadUrls(post) { + const nodes = [ + ...post.querySelectorAll("style[data-rendered-class]"), + ...post.querySelectorAll("[style*='upload://']"), + ]; + + const shortUrls = new Set(); + nodes.forEach((node) => { + for (const match of cssOf(node).matchAll(UPLOAD_URL_REGEX)) { + shortUrls.add(match[2]); + } + }); + + if (shortUrls.size === 0) { + return; + } + + const uncached = [...shortUrls].filter( + (url) => !lookupCachedUploadUrl(url).url + ); + if (uncached.length > 0) { + await resolveUncachedShortUrls(uncached); + } + + const resolved = new Map(); + shortUrls.forEach((url) => { + const { url: resolvedUrl } = lookupCachedUploadUrl(url); + if (resolvedUrl && resolvedUrl !== MISSING) { + resolved.set(url, resolvedUrl); + } + }); + + if (resolved.size === 0) { + return; + } + + const replaced = (css) => + css.replace(UPLOAD_URL_REGEX, (fullMatch, quote, shortUrl) => { + const url = resolved.get(shortUrl); + return url ? `url(${quote}${url}${quote})` : fullMatch; + }); + + nodes.forEach((node) => setCssOf(node, replaced(cssOf(node)))); +} + +export default { + name: "bbcode-css-upload-urls", + // must run after class-tag CSS is materialized into a live