diff --git a/frontend/discourse/app/lib/lightbox.js b/frontend/discourse/app/lib/lightbox.js index e2c37ccbbce7d..b0ba631a72544 100644 --- a/frontend/discourse/app/lib/lightbox.js +++ b/frontend/discourse/app/lib/lightbox.js @@ -5,13 +5,14 @@ import { isRailsTesting, isTesting } from "discourse/lib/environment"; import { helperContext } from "discourse/lib/helpers"; import { renderIcon } from "discourse/lib/icon-library"; import { SELECTORS } from "discourse/lib/lightbox/constants"; -import quoteImage, { canQuoteImage } from "discourse/lib/lightbox/quote-image"; +import quoteImage, { + canBuildImageQuote, +} from "discourse/lib/lightbox/quote-image"; import { isDocumentRTL } from "discourse/lib/text-direction"; import { escapeExpression, postRNWebviewMessage, } from "discourse/lib/utilities"; -import User from "discourse/models/user"; import { i18n } from "discourse-i18n"; export async function loadMagnificPopup() { @@ -27,10 +28,12 @@ export default async function lightbox( return; } + const currentUser = helperContext()?.currentUser; const caps = helperContext().capabilities; const imageClickNavigation = caps.touch; const canDownload = - !siteSettings.prevent_anons_from_downloading_files || User.current(); + !siteSettings.prevent_anons_from_downloading_files || !!currentUser; + const canQuoteImage = currentUser; if (siteSettings.experimental_lightbox) { const { default: PhotoSwipeLightbox } = await import("photoswipe/lightbox"); @@ -204,37 +207,39 @@ export default async function lightbox( }, }); - lightboxEl.pswp.ui.registerElement({ - name: "quote-image", - order: 10, - isButton: true, - title: i18n("lightbox.quote"), - html: { - isCustomSVG: true, - inner: - '', - outlineID: "pswp__icn-quote", - size: 512, - }, - onInit: (el, pswp) => { - pswp.on("change", () => { - const slideData = pswp.currSlide?.data; + if (canQuoteImage) { + lightboxEl.pswp.ui.registerElement({ + name: "quote-image", + order: 10, + isButton: true, + title: i18n("lightbox.quote"), + html: { + isCustomSVG: true, + inner: + '', + outlineID: "pswp__icn-quote", + size: 512, + }, + onInit: (el, pswp) => { + pswp.on("change", () => { + const slideData = pswp.currSlide?.data; + const slideElement = slideData?.element; + el.style.display = canBuildImageQuote(slideElement, slideData) + ? "" + : "none"; + }); + }, + onClick: () => { + const slideData = lightboxEl.pswp.currSlide?.data; const slideElement = slideData?.element; - el.style.display = canQuoteImage(slideElement, slideData) - ? "" - : "none"; - }); - }, - onClick: () => { - const slideData = lightboxEl.pswp.currSlide?.data; - const slideElement = slideData?.element; - quoteImage(slideElement, slideData).then((didQuote) => { - if (didQuote) { - lightboxEl.pswp.close(); - } - }); - }, - }); + quoteImage(slideElement, slideData).then((didQuote) => { + if (didQuote) { + lightboxEl.pswp.close(); + } + }); + }, + }); + } lightboxEl.pswp.ui.registerElement({ name: "custom-counter", diff --git a/frontend/discourse/app/lib/lightbox/quote-image.js b/frontend/discourse/app/lib/lightbox/quote-image.js index 5e7a1bde6d7a4..3a53c546ee852 100644 --- a/frontend/discourse/app/lib/lightbox/quote-image.js +++ b/frontend/discourse/app/lib/lightbox/quote-image.js @@ -1,6 +1,9 @@ import { getOwner } from "@ember/owner"; import { helperContext } from "discourse/lib/helpers"; -import { buildImageMarkdown as buildImageMarkdownShared } from "discourse/lib/markdown-image-builder"; +import { + buildImageMarkdown as buildImageMarkdownShared, + extensionFromUrl, +} from "discourse/lib/markdown-image-builder"; import { buildQuote } from "discourse/lib/quote"; import Composer from "discourse/models/composer"; import Draft from "discourse/models/draft"; @@ -16,7 +19,11 @@ function buildImageMarkdown(slideElement, slideData) { // Check for base62 SHA1 to use short upload:// URL format (same as to-markdown.js) if (slideData.base62SHA1) { + const extension = extensionFromUrl(slideData.origSrc); src = `upload://${slideData.base62SHA1}`; + if (extension) { + src += `.${extension}`; + } } else { // Prefer data-orig-src (same as to-markdown.js) src = slideData.origSrc || slideData.src; @@ -35,7 +42,14 @@ function buildImageMarkdown(slideElement, slideData) { }); } -export function canQuoteImage(slideElement, slideData) { +/** + * Checks if the slide element and data can be used to build a quote. + * + * @param {Element} slideElement + * @param {object} slideData + * @returns {boolean} + */ +export function canBuildImageQuote(slideElement, slideData) { return buildImageMarkdown(slideElement, slideData) !== null; } diff --git a/frontend/discourse/app/lib/markdown-image-builder.js b/frontend/discourse/app/lib/markdown-image-builder.js index cd4d6b1d79d97..5dd7e37aaf006 100644 --- a/frontend/discourse/app/lib/markdown-image-builder.js +++ b/frontend/discourse/app/lib/markdown-image-builder.js @@ -13,6 +13,22 @@ export function sanitizeAlt(text, options = {}) { return trimmed.replace(/\|/g, "|").replace(/([\\\[\]])/g, "\\$1"); } +/** + * Extracts the extension (without dot) from a URL or path. + * Returns null when no extension is present. + * + * @param {string} url + * @returns {string|null} + */ +export function extensionFromUrl(url) { + if (!url) { + return null; + } + + const match = url.match(/\.([a-zA-Z0-9]+)$/); + return match ? match[1] : null; +} + export function buildImageMarkdown(imageData) { const { src, diff --git a/frontend/discourse/app/lib/to-markdown.js b/frontend/discourse/app/lib/to-markdown.js index 83895edbf4c06..fc9ee0662891d 100644 --- a/frontend/discourse/app/lib/to-markdown.js +++ b/frontend/discourse/app/lib/to-markdown.js @@ -1,4 +1,7 @@ -import { buildImageMarkdown } from "discourse/lib/markdown-image-builder"; +import { + buildImageMarkdown, + extensionFromUrl, +} from "discourse/lib/markdown-image-builder"; const MSO_LIST_CLASSES = [ "MsoListParagraphCxSpFirst", @@ -365,6 +368,14 @@ export class Tag { if (base62SHA1) { href = `upload://${base62SHA1}`; + const extension = + extensionFromUrl(attr.href) || + extensionFromUrl(img.attributes.src) || + extensionFromUrl(attr["data-download-href"]); + + if (extension) { + href += `.${extension}`; + } } const width = img.attributes.width; @@ -413,6 +424,13 @@ export class Tag { const base62SHA1 = attr["data-base62-sha1"]; if (base62SHA1) { src = `upload://${base62SHA1}`; + const extension = + extensionFromUrl(attr.src || pAttr.src) || + extensionFromUrl(attr["data-orig-src"]); + + if (extension) { + src += `.${extension}`; + } } if (cssClass?.includes("emoji")) { diff --git a/frontend/discourse/tests/unit/lib/lightbox/quote-image-test.js b/frontend/discourse/tests/unit/lib/lightbox/quote-image-test.js index 8ddd61c313015..1191337a77cee 100644 --- a/frontend/discourse/tests/unit/lib/lightbox/quote-image-test.js +++ b/frontend/discourse/tests/unit/lib/lightbox/quote-image-test.js @@ -3,8 +3,12 @@ import Service from "@ember/service"; import { setupTest } from "ember-qunit"; import { module, test } from "qunit"; import sinon from "sinon"; -import quoteImage, { canQuoteImage } from "discourse/lib/lightbox/quote-image"; +import { createHelperContext, helperContext } from "discourse/lib/helpers"; +import quoteImage, { + canBuildImageQuote, +} from "discourse/lib/lightbox/quote-image"; import Draft from "discourse/models/draft"; +import { logIn } from "discourse/tests/helpers/qunit-helpers"; class ComposerStub extends Service { model = { viewOpen: false }; @@ -27,6 +31,15 @@ module("Unit | Lib | lightbox | quote image", function (hooks) { setupTest(hooks); hooks.beforeEach(function () { + this.originalHelperContext = helperContext(); + + logIn(this.owner); + + createHelperContext({ + ...(this.originalHelperContext || {}), + currentUser: this.owner.lookup("service:current-user"), + }); + this.owner.unregister("service:composer"); this.owner.unregister("service:app-events"); @@ -55,6 +68,7 @@ module("Unit | Lib | lightbox | quote image", function (hooks) { hooks.afterEach(function () { document.querySelectorAll(".topic-post").forEach((el) => el.remove()); this.draftGetStub.restore(); + createHelperContext(this.originalHelperContext); }); function buildLightbox(context, overrides = {}) { @@ -132,12 +146,12 @@ module("Unit | Lib | lightbox | quote image", function (hooks) { ); }); - test("canQuoteImage only returns true when context and metadata exist", function (assert) { + test("canBuildImageQuote only returns true when context and metadata exist", function (assert) { const invalid = document.createElement("a"); - assert.false(canQuoteImage(invalid, {})); + assert.false(canBuildImageQuote(invalid, {})); const { element, slideData } = buildLightbox(this); - assert.true(canQuoteImage(element, slideData)); + assert.true(canBuildImageQuote(element, slideData)); }); test("builds markdown using data-orig-src and dimensions when composer is closed", async function (assert) { @@ -187,7 +201,7 @@ module("Unit | Lib | lightbox | quote image", function (hooks) { ); }); - test("uses short upload:// URL when data-base62-sha1 is present", async function (assert) { + test("uses short upload:// URL with extension when data-base62-sha1 is present", async function (assert) { const { element, slideData } = buildLightbox(this, { base62SHA1: "a4bcwvmLAy8cGHKPUrK4G3AUbt9", href: "//localhost:4200/uploads/default/original/1X/468eb8aa1f0126f1ce7e7ea7a2f64f25da0b58db.png", @@ -200,9 +214,9 @@ module("Unit | Lib | lightbox | quote image", function (hooks) { const quote = this.composer.openCalls[0].quote; assert.true( quote.includes( - "![diagram|640x480](upload://a4bcwvmLAy8cGHKPUrK4G3AUbt9)" + "![diagram|640x480](upload://a4bcwvmLAy8cGHKPUrK4G3AUbt9.png)" ), - "uses short upload:// URL format with base62-sha1" + "uses short upload:// URL format with base62-sha1 and extension" ); }); diff --git a/frontend/discourse/tests/unit/lib/to-markdown-test.js b/frontend/discourse/tests/unit/lib/to-markdown-test.js index 69288e94c6e3f..b40caac4c07de 100644 --- a/frontend/discourse/tests/unit/lib/to-markdown-test.js +++ b/frontend/discourse/tests/unit/lib/to-markdown-test.js @@ -189,7 +189,7 @@ module("Unit | Utility | to-markdown", function (hooks) { html = ``; assert.strictEqual( toMarkdown(html), - `![|100x50](upload://${base62SHA1} "some title")` + `![|100x50](upload://${base62SHA1}.png "some title")` ); html = `
description
`; @@ -437,7 +437,7 @@ helloWorld();consectetur.`; sherlock3_sig.jpg5496×3664 2 MB `; - markdown = `![sherlock3_sig.jpg|689x459](upload://1frsimI7TOtFJyD2LLyKSHM8JWe)`; + markdown = `![sherlock3_sig.jpg|689x459](upload://1frsimI7TOtFJyD2LLyKSHM8JWe.jpeg)`; assert.strictEqual(toMarkdown(html), markdown); });