diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 38524d0327e..8ea654c528d 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -27,6 +27,7 @@ com.woltlab.wcf.message.quote + wcf\system\message\quote\IMessageQuoteHandler com.woltlab.wcf.user.recentActivityEvent diff --git a/ts/WoltLabSuite/Core/Api/Messages/Author.ts b/ts/WoltLabSuite/Core/Api/Messages/Author.ts deleted file mode 100644 index a2a08210dcf..00000000000 --- a/ts/WoltLabSuite/Core/Api/Messages/Author.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Requests render a full quote of a message. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - * @woltlabExcludeBundle tiny - */ - -import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; -import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; - -type Response = { - objectID: number; - authorID: number; - author: string; - time: string; - title: string; - link: string; - avatar: string; -}; - -export async function getMessageAuthor(className: string, objectID: number): Promise> { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/message-author"); - url.searchParams.set("className", className); - url.searchParams.set("objectID", objectID.toString()); - - let response: Response; - try { - response = (await prepareRequest(url).get().allowCaching().fetchAsJson()) as Response; - } catch (e) { - return apiResultFromError(e); - } - - return apiResultFromValue(response); -} diff --git a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts index f426872ea73..449b0b67fab 100644 --- a/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts +++ b/ts/WoltLabSuite/Core/Api/Messages/RenderQuote.ts @@ -13,11 +13,8 @@ import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; type Response = { objectID: number; - authorID: number | null; author: string; - time: string; link: string; - title: string; avatar: string; message: string | null; rawMessage: string | null; @@ -25,13 +22,12 @@ type Response = { export async function renderQuote( objectType: string, - className: string, objectID: number, + isFullQuote: boolean, ): Promise> { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("className", className); - url.searchParams.set("fullQuote", "true"); + url.searchParams.set("isFullQuote", String(isFullQuote)); url.searchParams.set("objectID", objectID.toString()); let response: Response; diff --git a/ts/WoltLabSuite/Core/Component/Quote/List.ts b/ts/WoltLabSuite/Core/Component/Quote/List.ts index 1b51acec89b..0c951898b33 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/List.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/List.ts @@ -78,10 +78,15 @@ class QuoteList { fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => { markQuoteAsUsed(this.#editorId, uuid); + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, - isText: quote.rawMessage === undefined, + content, + isText: !quote.rawMessage, link: message.link, }); }); @@ -142,21 +147,23 @@ export function setup(editorId: string, containerId?: string): void { throw new Error(`The editor '${editorId}' does not exist.`); } - listenToCkeditor(editor).ready(({ ckeditor }) => { - if (ckeditor.features.quoteBlock) { - quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); - } - - if (ckeditor.isVisible()) { - setActiveEditor(ckeditor, ckeditor.features.quoteBlock); - } + listenToCkeditor(editor) + .ready(({ ckeditor }) => { + if (ckeditor.features.quoteBlock) { + quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); + } - ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { - if (isFocused) { + if (ckeditor.isVisible()) { setActiveEditor(ckeditor, ckeditor.features.quoteBlock); } + + ckeditor.focusTracker.on("change:isFocused", (_evt: unknown, _name: unknown, isFocused: boolean) => { + if (isFocused) { + setActiveEditor(ckeditor, ckeditor.features.quoteBlock); + } + }); + }) + .destroy(() => { + removeActiveEditor(editor); }); - }).destroy(() => { - removeActiveEditor(editor); - }); } diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts index 9f6ce5283f7..30606ee7dd1 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Message.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Message.ts @@ -17,20 +17,20 @@ import { getFullQuoteUuid, saveFullQuote, markQuoteAsUsed, - isFullQuoted, getKey, removeQuotes, } from "WoltLabSuite/Core/Component/Quote/Storage"; import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; import { dispatchToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event"; -interface Container { +type Container = { element: HTMLElement; messageBodySelector: string; objectType: string; - className: string; objectId: number; -} + /** @deprecated 6.2 Used for legacy implementations only. */ + className: string | undefined; +}; let selectedMessage: | undefined @@ -39,12 +39,12 @@ let selectedMessage: container: Container; }; -interface ElementBoundaries { +type ElementBoundaries = { bottom: number; left: number; right: number; top: number; -} +}; const containers = new Map(); const quoteMessageButtons = new Map(); @@ -57,19 +57,19 @@ const copyQuote = document.createElement("div"); export function registerContainer( containerSelector: string, messageBodySelector: string, - className: string, objectType: string, + className?: string, ): void { wheneverFirstSeen(containerSelector, (container: HTMLElement) => { const id = DomUtil.identify(container); - const objectId = ~~container.dataset.objectId!; + const objectId = parseInt(container.dataset.objectId || "0"); containers.set(id, { element: container, - messageBodySelector: messageBodySelector, - objectType: objectType, - className: className, - objectId: objectId, + messageBodySelector, + objectType, + objectId, + className, }); if (container.classList.contains("jsInvalidQuoteTarget")) { @@ -80,42 +80,53 @@ export function registerContainer( container.classList.add("jsQuoteMessageContainer"); const quoteMessage = container.querySelector(".jsQuoteMessage"); - let quoteMessageButton = quoteMessage?.querySelector(".button"); - if (!quoteMessageButton && quoteMessage?.classList.contains("button")) { + if (quoteMessage === null) { + return; + } + + let quoteMessageButton = quoteMessage.querySelector(".button"); + if (!quoteMessageButton && quoteMessage.classList.contains("button")) { quoteMessageButton = quoteMessage; } - if (quoteMessageButton) { + if (quoteMessageButton !== null) { quoteMessageButtons.set(getKey(objectType, objectId), quoteMessageButton); - if (isFullQuoted(objectType, objectId)) { + if (getFullQuoteUuid(objectType, objectId) !== undefined) { quoteMessageButton.classList.add("active"); } } - quoteMessage?.addEventListener( + quoteMessage.addEventListener( "click", promiseMutex(async (event: MouseEvent) => { event.preventDefault(); - if (isFullQuoted(objectType, objectId)) { - removeQuotes([getFullQuoteUuid(objectType, objectId)!]); - quoteMessageButton!.classList.remove("active"); + const uuid = getFullQuoteUuid(objectType, objectId); + if (uuid !== undefined) { + removeQuotes([uuid]); + quoteMessageButton?.classList.remove("active"); + return; } - const quoteMessage = await saveFullQuote(objectType, className, ~~container.dataset.objectId!); - quoteMessageButton!.classList.add("active"); + const quote = await saveFullQuote(objectType, objectId, className); + quoteMessageButton?.classList.add("active"); if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); + markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid); } }), ); @@ -152,17 +163,22 @@ function setup() { buttonSaveQuote.addEventListener( "click", promiseMutex(async () => { + if (selectedMessage === undefined) { + return; + } + await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, + selectedMessage.container.objectType, + selectedMessage.container.objectId, + selectedMessage.message, + selectedMessage.container.className, ); removeSelection(); }), ); copyQuote.appendChild(buttonSaveQuote); + const buttonSaveAndInsertQuote = document.createElement("button"); buttonSaveAndInsertQuote.type = "button"; buttonSaveAndInsertQuote.hidden = true; @@ -171,22 +187,31 @@ function setup() { buttonSaveAndInsertQuote.addEventListener( "click", promiseMutex(async () => { - const quoteMessage = await saveQuote( - selectedMessage!.container.objectType, - selectedMessage!.container.objectId, - selectedMessage!.container.className, - selectedMessage!.message, + if (selectedMessage === undefined) { + return; + } + + const quote = await saveQuote( + selectedMessage.container.objectType, + selectedMessage.container.objectId, + selectedMessage.message, + selectedMessage.container.className, ); if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } + dispatchToCkeditor(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - markQuoteAsUsed(activeEditor.sourceElement.id, quoteMessage.uuid); + markQuoteAsUsed(activeEditor.sourceElement.id, quote.uuid); } removeSelection(); diff --git a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts index ba1b9f0d681..38bbb9f18b9 100644 --- a/ts/WoltLabSuite/Core/Component/Quote/Storage.ts +++ b/ts/WoltLabSuite/Core/Component/Quote/Storage.ts @@ -10,24 +10,21 @@ import * as Core from "WoltLabSuite/Core/Core"; import { renderQuote } from "WoltLabSuite/Core/Api/Messages/RenderQuote"; -import { getMessageAuthor } from "WoltLabSuite/Core/Api/Messages/Author"; import { refreshQuoteLists } from "WoltLabSuite/Core/Component/Quote/List"; import { resetRemovalQuotes } from "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes"; import { removeQuoteStatus } from "WoltLabSuite/Core/Component/Quote/Message"; +import { dboAction } from "WoltLabSuite/Core/Ajax"; interface Message { objectID: number; - time: string; - title: string; link: string; - authorID: number | null; author: string; avatar: string; } interface Quote { - message: string; - rawMessage?: string; + message: string | null; + rawMessage: string | null; } interface StorageData { @@ -35,28 +32,51 @@ interface StorageData { messages: Map; } +type LegacyQuoteData = { + count: number; + fullQuoteMessageIDs: number[]; + renderedQuote: Message & Quote; +}; + const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map>(); export async function saveQuote( objectType: string, objectId: number, - objectClassName: string, message: string, + /** @deprecated 6.2 Used for legacy implementations only. */ + className?: string, ): Promise { - const result = await getMessageAuthor(objectClassName, objectId); - if (!result.ok) { - throw new Error("Error fetching author data"); + let quote: Message & Quote; + + if (className !== undefined) { + const result = (await dboAction("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch()) as LegacyQuoteData; + quote = result.renderedQuote; + } else { + const result = await renderQuote(objectType, objectId, false); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } + + quote = result.value; } - const uuid = storeQuote(objectType, result.value, { + const uuid = storeQuote(objectType, quote, { message, + rawMessage: null, }); refreshQuoteLists(); return { - ...result.value, + ...quote, message, uuid, }; @@ -64,36 +84,30 @@ export async function saveQuote( export async function saveFullQuote( objectType: string, - objectClassName: string, objectId: number, + /** @deprecated 6.2 Used for legacy implementations only. */ + className?: string, ): Promise { - const result = await renderQuote(objectType, objectClassName, objectId); - if (!result.ok) { - throw new Error("Error fetching quote data"); - } - - const message = { - objectID: result.value.objectID, - time: result.value.time, - title: result.value.title, - link: result.value.link, - authorID: result.value.authorID, - author: result.value.author, - avatar: result.value.avatar, - }; + let message: Message & Quote; + + if (className !== undefined) { + const result = (await dboAction("saveFullQuote", className).objectIds([objectId]).dispatch()) as LegacyQuoteData; + message = result.renderedQuote; + } else { + const result = await renderQuote(objectType, objectId, true); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } - const quote = { - message: result.value.message!, - rawMessage: result.value.rawMessage!, - }; + message = result.value; + } - const uuid = storeQuote(objectType, message, quote); + const uuid = storeQuote(objectType, message, message); refreshQuoteLists(); return { ...message, - ...quote, uuid, }; } @@ -167,7 +181,7 @@ export function clearQuotesForEditor(editorId: string): void { usedQuotes.get(editorId)?.forEach((uuid) => { for (const [key, quotes] of storage.quotes) { const quote = quotes.get(uuid); - if (quote?.rawMessage !== undefined) { + if (quote?.rawMessage !== null) { fullQuotes.push(key); } @@ -192,24 +206,6 @@ export function clearQuotesForEditor(editorId: string): void { }); } -export function isFullQuoted(objectType: string, objectId: number): boolean { - const key = getKey(objectType, objectId); - const storage = getStorage(); - const quotes = storage.quotes.get(key); - - if (quotes === undefined) { - return false; - } - - return ( - Array.from(quotes).filter(([, quote]) => { - if (quote.rawMessage !== undefined) { - return true; - } - }).length > 0 - ); -} - function storeQuote(objectType: string, message: Message, quote: Quote): string { const storage = getStorage(); @@ -221,7 +217,7 @@ function storeQuote(objectType: string, message: Message, quote: Quote): string storage.messages.set(key, message); for (const [uuid, q] of storage.quotes.get(key)!) { - if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { + if ((q.rawMessage !== null && q.rawMessage === null) || q.message === quote.message) { return uuid; } } @@ -235,11 +231,14 @@ function storeQuote(objectType: string, message: Message, quote: Quote): string } export function getFullQuoteUuid(objectType: string, objectId: number): string | undefined { - const storage = getStorage(); const key = getKey(objectType, objectId); + const quotes = getStorage().quotes.get(key); + if (quotes === undefined) { + return undefined; + } - for (const [uuid, q] of storage.quotes.get(key)!) { - if (q.rawMessage !== undefined && q.message !== undefined) { + for (const [uuid, q] of quotes) { + if (q.rawMessage !== null && q.message !== null) { return uuid; } } @@ -303,7 +302,7 @@ window.addEventListener("storage", (event) => { // Update the quote status if the quote was removed in another tab for (const [key, quotes] of oldValue.quotes) { for (const [, quote] of quotes) { - if (quote.rawMessage !== undefined && !newValue.quotes.has(key)) { + if (quote.rawMessage !== null && !newValue.quotes.has(key)) { removeQuoteStatus(key); } } diff --git a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts index a3dad998ce7..c039368b0d1 100644 --- a/ts/WoltLabSuite/Core/Ui/Message/Quote.ts +++ b/ts/WoltLabSuite/Core/Ui/Message/Quote.ts @@ -6,18 +6,15 @@ import { registerContainer } from "WoltLabSuite/Core/Component/Quote/Message"; -// see WCF.Message.Quote.Manager -export interface WCFMessageQuoteManager { - supportPaste: () => boolean; - updateCount: (number, object) => void; -} - +/** + * @deprecated 6.2 Use `registerContainer()` without the className parameter. + */ export class UiMessageQuote { /** * Initializes the quote handler for given object type. */ constructor( - _quoteManager: WCFMessageQuoteManager, + _quoteManager: typeof window.WCF.Message.Quote.Manager, className: string, objectType: string, containerSelector: string, @@ -25,12 +22,7 @@ export class UiMessageQuote { _messageContentSelector: string, _supportDirectInsert: boolean, ) { - // remove "Action" from className - if (className.endsWith("Action")) { - className = className.substring(0, className.length - 6); - } - - registerContainer(containerSelector, messageBodySelector, className, objectType); + registerContainer(containerSelector, messageBodySelector, objectType, className); } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js deleted file mode 100644 index be9deb91cb5..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/Author.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Requests render a full quote of a message. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 6.2 - * @woltlabExcludeBundle tiny - */ -define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.getMessageAuthor = getMessageAuthor; - async function getMessageAuthor(className, objectID) { - const url = new URL(window.WSC_RPC_API_URL + "core/messages/message-author"); - url.searchParams.set("className", className); - url.searchParams.set("objectID", objectID.toString()); - let response; - try { - response = (await (0, Backend_1.prepareRequest)(url).get().allowCaching().fetchAsJson()); - } - catch (e) { - return (0, Result_1.apiResultFromError)(e); - } - return (0, Result_1.apiResultFromValue)(response); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js index 5d1c7fd4849..08a5570ee62 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Messages/RenderQuote.js @@ -11,11 +11,10 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], fu "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderQuote = renderQuote; - async function renderQuote(objectType, className, objectID) { + async function renderQuote(objectType, objectID, isFullQuote) { const url = new URL(window.WSC_RPC_API_URL + "core/messages/render-quote"); url.searchParams.set("objectType", objectType); - url.searchParams.set("className", className); - url.searchParams.set("fullQuote", "true"); + url.searchParams.set("isFullQuote", String(isFullQuote)); url.searchParams.set("objectID", objectID.toString()); let response; try { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js index cd8e56d4ef3..1be071b13bb 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js @@ -61,10 +61,14 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve `); fragment.querySelector('button[data-action="insert"]').addEventListener("click", () => { (0, Storage_1.markQuoteAsUsed)(this.#editorId, uuid); + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({ author: message.author, - content: quote.rawMessage === undefined ? quote.message : quote.rawMessage, - isText: quote.rawMessage === undefined, + content, + isText: !quote.rawMessage, link: message.link, }); }); @@ -110,7 +114,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve if (editor === null) { throw new Error(`The editor '${editorId}' does not exist.`); } - (0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => { + (0, Event_1.listenToCkeditor)(editor) + .ready(({ ckeditor }) => { if (ckeditor.features.quoteBlock) { quoteLists.set(editorId, new QuoteList(editorId, editor, containerId)); } @@ -122,7 +127,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve (0, Message_1.setActiveEditor)(ckeditor, ckeditor.features.quoteBlock); } }); - }).destroy(() => { + }) + .destroy(() => { (0, Message_1.removeActiveEditor)(editor); }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js index 84db64b7535..044b833170a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js @@ -23,16 +23,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui let timerSelectionChange = undefined; let isMouseDown = false; const copyQuote = document.createElement("div"); - function registerContainer(containerSelector, messageBodySelector, className, objectType) { + function registerContainer(containerSelector, messageBodySelector, objectType, className) { (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => { const id = Util_1.default.identify(container); - const objectId = ~~container.dataset.objectId; + const objectId = parseInt(container.dataset.objectId || "0"); containers.set(id, { element: container, - messageBodySelector: messageBodySelector, - objectType: objectType, - className: className, - objectId: objectId, + messageBodySelector, + objectType, + objectId, + className, }); if (container.classList.contains("jsInvalidQuoteTarget")) { return; @@ -40,33 +40,41 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui container.addEventListener("mousedown", (event) => onMouseDown(event)); container.classList.add("jsQuoteMessageContainer"); const quoteMessage = container.querySelector(".jsQuoteMessage"); - let quoteMessageButton = quoteMessage?.querySelector(".button"); - if (!quoteMessageButton && quoteMessage?.classList.contains("button")) { + if (quoteMessage === null) { + return; + } + let quoteMessageButton = quoteMessage.querySelector(".button"); + if (!quoteMessageButton && quoteMessage.classList.contains("button")) { quoteMessageButton = quoteMessage; } - if (quoteMessageButton) { + if (quoteMessageButton !== null) { quoteMessageButtons.set((0, Storage_1.getKey)(objectType, objectId), quoteMessageButton); - if ((0, Storage_1.isFullQuoted)(objectType, objectId)) { + if ((0, Storage_1.getFullQuoteUuid)(objectType, objectId) !== undefined) { quoteMessageButton.classList.add("active"); } } - quoteMessage?.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { + quoteMessage.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => { event.preventDefault(); - if ((0, Storage_1.isFullQuoted)(objectType, objectId)) { - (0, Storage_1.removeQuotes)([(0, Storage_1.getFullQuoteUuid)(objectType, objectId)]); - quoteMessageButton.classList.remove("active"); + const uuid = (0, Storage_1.getFullQuoteUuid)(objectType, objectId); + if (uuid !== undefined) { + (0, Storage_1.removeQuotes)([uuid]); + quoteMessageButton?.classList.remove("active"); return; } - const quoteMessage = await (0, Storage_1.saveFullQuote)(objectType, className, ~~container.dataset.objectId); - quoteMessageButton.classList.add("active"); + const quote = await (0, Storage_1.saveFullQuote)(objectType, objectId, className); + quoteMessageButton?.classList.add("active"); if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quote.uuid); } })); }); @@ -93,7 +101,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveQuote.classList.add("jsQuoteManagerStore"); buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected"); buttonSaveQuote.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + if (selectedMessage === undefined) { + return; + } + await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message, selectedMessage.container.className); removeSelection(); })); copyQuote.appendChild(buttonSaveQuote); @@ -103,15 +114,22 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSui buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert"); buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply"); buttonSaveAndInsertQuote.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async () => { - const quoteMessage = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.container.className, selectedMessage.message); + if (selectedMessage === undefined) { + return; + } + const quote = await (0, Storage_1.saveQuote)(selectedMessage.container.objectType, selectedMessage.container.objectId, selectedMessage.message, selectedMessage.container.className); if (activeEditor !== undefined) { + const content = quote.rawMessage || quote.message; + if (content === null) { + throw new Error("Expected either the `rawMessage` or `message` to be a string."); + } (0, Event_1.dispatchToCkeditor)(activeEditor.sourceElement).insertQuote({ - author: quoteMessage.author, - content: quoteMessage.rawMessage === undefined ? quoteMessage.message : quoteMessage.rawMessage, - isText: quoteMessage.rawMessage === undefined, - link: quoteMessage.link, + author: quote.author, + content, + isText: quote.rawMessage === null, + link: quote.link, }); - (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quoteMessage.uuid); + (0, Storage_1.markQuoteAsUsed)(activeEditor.sourceElement.id, quote.uuid); } removeSelection(); })); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js index aa394d70483..a40a03e253a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js @@ -7,7 +7,7 @@ * @since 6.2 * @woltlabExcludeBundle tiny */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Api/Messages/Author", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, RenderQuote_1, Author_1, List_1, ResetRemovalQuotes_1, Message_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Api/Messages/RenderQuote", "WoltLabSuite/Core/Component/Quote/List", "WoltLabSuite/Core/Api/Messages/ResetRemovalQuotes", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Ajax"], function (require, exports, tslib_1, Core, RenderQuote_1, List_1, ResetRemovalQuotes_1, Message_1, Ajax_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.saveQuote = saveQuote; @@ -19,50 +19,62 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C exports.markQuoteAsUsed = markQuoteAsUsed; exports.getUsedQuotes = getUsedQuotes; exports.clearQuotesForEditor = clearQuotesForEditor; - exports.isFullQuoted = isFullQuoted; exports.getFullQuoteUuid = getFullQuoteUuid; exports.getKey = getKey; Core = tslib_1.__importStar(Core); const STORAGE_KEY = Core.getStoragePrefix() + "quotes"; const usedQuotes = new Map(); - async function saveQuote(objectType, objectId, objectClassName, message) { - const result = await (0, Author_1.getMessageAuthor)(objectClassName, objectId); - if (!result.ok) { - throw new Error("Error fetching author data"); + async function saveQuote(objectType, objectId, message, + /** @deprecated 6.2 Used for legacy implementations only. */ + className) { + let quote; + if (className !== undefined) { + const result = (await (0, Ajax_1.dboAction)("saveQuote", className) + .objectIds([objectId]) + .payload({ + message, + renderQuote: true, + }) + .dispatch()); + quote = result.renderedQuote; } - const uuid = storeQuote(objectType, result.value, { + else { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, false); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } + quote = result.value; + } + const uuid = storeQuote(objectType, quote, { message, + rawMessage: null, }); (0, List_1.refreshQuoteLists)(); return { - ...result.value, + ...quote, message, uuid, }; } - async function saveFullQuote(objectType, objectClassName, objectId) { - const result = await (0, RenderQuote_1.renderQuote)(objectType, objectClassName, objectId); - if (!result.ok) { - throw new Error("Error fetching quote data"); - } - const message = { - objectID: result.value.objectID, - time: result.value.time, - title: result.value.title, - link: result.value.link, - authorID: result.value.authorID, - author: result.value.author, - avatar: result.value.avatar, - }; - const quote = { - message: result.value.message, - rawMessage: result.value.rawMessage, - }; - const uuid = storeQuote(objectType, message, quote); + async function saveFullQuote(objectType, objectId, + /** @deprecated 6.2 Used for legacy implementations only. */ + className) { + let message; + if (className !== undefined) { + const result = (await (0, Ajax_1.dboAction)("saveFullQuote", className).objectIds([objectId]).dispatch()); + message = result.renderedQuote; + } + else { + const result = await (0, RenderQuote_1.renderQuote)(objectType, objectId, true); + if (!result.ok) { + throw new Error("Error fetching quote data"); + } + message = result.value; + } + const uuid = storeQuote(objectType, message, message); (0, List_1.refreshQuoteLists)(); return { ...message, - ...quote, uuid, }; } @@ -118,7 +130,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C usedQuotes.get(editorId)?.forEach((uuid) => { for (const [key, quotes] of storage.quotes) { const quote = quotes.get(uuid); - if (quote?.rawMessage !== undefined) { + if (quote?.rawMessage !== null) { fullQuotes.push(key); } quotes.delete(uuid); @@ -137,19 +149,6 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C (0, Message_1.removeQuoteStatus)(key); }); } - function isFullQuoted(objectType, objectId) { - const key = getKey(objectType, objectId); - const storage = getStorage(); - const quotes = storage.quotes.get(key); - if (quotes === undefined) { - return false; - } - return (Array.from(quotes).filter(([, quote]) => { - if (quote.rawMessage !== undefined) { - return true; - } - }).length > 0); - } function storeQuote(objectType, message, quote) { const storage = getStorage(); const key = getKey(objectType, message.objectID); @@ -158,7 +157,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C } storage.messages.set(key, message); for (const [uuid, q] of storage.quotes.get(key)) { - if ((q.rawMessage !== undefined && q.rawMessage === quote.rawMessage) || q.message === quote.message) { + if ((q.rawMessage !== null && q.rawMessage === null) || q.message === quote.message) { return uuid; } } @@ -168,10 +167,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C return uuid; } function getFullQuoteUuid(objectType, objectId) { - const storage = getStorage(); const key = getKey(objectType, objectId); - for (const [uuid, q] of storage.quotes.get(key)) { - if (q.rawMessage !== undefined && q.message !== undefined) { + const quotes = getStorage().quotes.get(key); + if (quotes === undefined) { + return undefined; + } + for (const [uuid, q] of quotes) { + if (q.rawMessage !== null && q.message !== null) { return uuid; } } @@ -220,7 +222,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C // Update the quote status if the quote was removed in another tab for (const [key, quotes] of oldValue.quotes) { for (const [, quote] of quotes) { - if (quote.rawMessage !== undefined && !newValue.quotes.has(key)) { + if (quote.rawMessage !== null && !newValue.quotes.has(key)) { (0, Message_1.removeQuoteStatus)(key); } } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js index 0d65a51114d..6e25bf60374 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js @@ -7,16 +7,15 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], func "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiMessageQuote = void 0; + /** + * @deprecated 6.2 Use `registerContainer()` without the className parameter. + */ class UiMessageQuote { /** * Initializes the quote handler for given object type. */ constructor(_quoteManager, className, objectType, containerSelector, messageBodySelector, _messageContentSelector, _supportDirectInsert) { - // remove "Action" from className - if (className.endsWith("Action")) { - className = className.substring(0, className.length - 6); - } - (0, Message_1.registerContainer)(containerSelector, messageBodySelector, className, objectType); + (0, Message_1.registerContainer)(containerSelector, messageBodySelector, objectType, className); } } exports.UiMessageQuote = UiMessageQuote; diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index e3205fd1895..5d5865c6ca8 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -152,7 +152,6 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\listViews\GetItem()); $event->register(new \wcf\system\endpoint\controller\core\messages\GetMentionSuggestions()); $event->register(new \wcf\system\endpoint\controller\core\messages\RenderQuote()); - $event->register(new \wcf\system\endpoint\controller\core\messages\GetMessageAuthor()); $event->register(new \wcf\system\endpoint\controller\core\messages\ResetRemovalQuotes()); $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession()); $event->register(new \wcf\system\endpoint\controller\core\versionTrackers\RevertVersion()); diff --git a/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php b/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php deleted file mode 100644 index da483e1eb68..00000000000 --- a/wcfsetup/install/files/lib/data/IEmbeddedMessageObject.class.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @since 6.2 - */ -interface IEmbeddedMessageObject -{ - /** - * Loads embedded objects for the given object type and object IDs. - */ - public function loadEmbeddedObjects(): void; -} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php deleted file mode 100644 index ba531a7e9bd..00000000000 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/GetMessageAuthor.class.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @since 6.2 - */ -#[GetRequest('/core/messages/message-author')] -final class GetMessageAuthor implements IController -{ - #[\Override] - public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface - { - $parameters = Helper::mapApiParameters($request, GetMessageAuthorParameters::class); - - // @phpstan-ignore argument.templateType - $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); - \assert($object instanceof IMessage && $object instanceof DatabaseObject); - - $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); - - return new JsonResponse( - [ - "objectID" => $object->getObjectID(), - "authorID" => $userProfile->getUserID(), - "author" => $userProfile->getUsername(), - "title" => $object->getTitle(), - "avatar" => $userProfile->getAvatar()->getURL(), - "time" => (new \DateTime('@' . $object->getTime()))->format("c"), - "link" => $object->getLink(), - ], - 200, - [ - 'cache-control' => [ - 'max-age=300', - ], - ] - ); - } -} - -/** @internal */ -final class GetMessageAuthorParameters -{ - public function __construct( - /** @var non-empty-string */ - public readonly string $className, - /** @var positive-int */ - public readonly int $objectID, - ) {} -} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php index 979e32d759e..5991b154ac8 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/messages/RenderQuote.class.php @@ -5,21 +5,23 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use wcf\data\DatabaseObject; -use wcf\data\IEmbeddedMessageObject; use wcf\data\IMessage; +use wcf\data\object\type\ObjectTypeCache; use wcf\data\user\UserProfile; use wcf\http\Helper; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\endpoint\GetRequest; use wcf\system\endpoint\IController; +use wcf\system\exception\NotImplementedException; +use wcf\system\exception\PermissionDeniedException; use wcf\system\html\input\HtmlInputProcessor; +use wcf\system\message\quote\IMessageQuoteHandler; /** * Retrieves data for the rendering of a quote. * * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.2 */ @@ -31,33 +33,38 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res { $parameters = Helper::mapApiParameters($request, GetRenderQuoteParameters::class); - // @phpstan-ignore argument.templateType - $object = Helper::fetchObjectFromRequestParameter($parameters->objectID, $parameters->className); - \assert($object instanceof IMessage && $object instanceof DatabaseObject); + $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName( + 'com.woltlab.wcf.message.quote', + $parameters->objectType, + ); + $processor = $objectType->getProcessor(); + \assert($processor instanceof IMessageQuoteHandler); - $userProfile = UserProfileRuntimeCache::getInstance()->getObject($object->getUserID()); - if ($userProfile === null) { - $userProfile = UserProfile::getGuestUserProfile($object->getUsername()); + $message = null; + try { + $message = $processor->getMessage($parameters->objectID); + } catch (NotImplementedException) { + // This can happen for legacy implementations that do not yet + // implement the new `getMessage()` method. } - if ($object instanceof IEmbeddedMessageObject) { - $object->loadEmbeddedObjects(); + if ($message === null) { + throw new PermissionDeniedException(); } - return new JsonResponse( - [ - "objectID" => $object->getObjectID(), - "authorID" => $userProfile->getUserID(), - "author" => $userProfile->getUsername(), - "avatar" => $userProfile->getAvatar()->getURL(), - "time" => (new \DateTime('@' . $object->getTime()))->format("c"), - "title" => $object->getTitle(), - "link" => $object->getLink(), - "rawMessage" => $parameters->fullQuote ? $this->renderFullQuote($object) : null, - "message" => $parameters->fullQuote ? $object->getFormattedMessage() : null - ], - 200, - ); + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($message->getUserID()); + if ($userProfile === null) { + $userProfile = UserProfile::getGuestUserProfile($message->getUsername()); + } + + return new JsonResponse([ + 'objectID' => $parameters->objectID, + 'author' => $userProfile->getUsername(), + 'avatar' => $userProfile->getAvatar()->getURL(), + 'link' => $message->getLink(), + 'rawMessage' => $parameters->isFullQuote ? $this->renderFullQuote($message) : null, + 'message' => $parameters->isFullQuote ? $message->getFormattedMessage() : null + ]); } private function renderFullQuote(IMessage $object): string @@ -78,9 +85,10 @@ final class GetRenderQuoteParameters { public function __construct( /** @var non-empty-string */ - public readonly string $className, + public readonly string $objectType, /** @var positive-int */ public readonly int $objectID, - public readonly bool $fullQuote = false, + + public readonly bool $isFullQuote, ) {} } diff --git a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php index 05b5cc78299..d7a1548db27 100644 --- a/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/AbstractMessageQuoteHandler.class.php @@ -2,35 +2,42 @@ namespace wcf\system\message\quote; +use wcf\data\IMessage; use wcf\system\cache\runtime\UserProfileRuntimeCache; +use wcf\system\exception\NotImplementedException; use wcf\system\SingletonFactory; use wcf\system\WCF; /** * Default implementation for quote handlers. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * - * @deprecated 6.2 + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ abstract class AbstractMessageQuoteHandler extends SingletonFactory implements IMessageQuoteHandler { /** * template name * @var string + * @deprecated 6.2 */ public $templateName = 'messageQuoteList'; /** * list of quoted message * @var QuotedMessage[] + * @deprecated 6.2 */ public $quotedMessages = []; /** - * @inheritDoc + * Renders a template for given quotes. + * + * @param mixed[][] $data + * @param bool $supportPaste + * @return string + * @deprecated 6.2 Implement `getMessage()` instead. */ public function render(array $data, $supportPaste = false) { @@ -58,8 +65,12 @@ public function render(array $data, $supportPaste = false) } /** - * @inheritDoc - * @param bool $renderAsString + * Renders a list of quotes for insertation. + * + * @param mixed[][] $data + * @param bool $render + * @return string[] + * @deprecated 6.2 Implement `getMessage()` instead. */ public function renderQuotes(array $data, $render = true, $renderAsString = true) { @@ -94,6 +105,7 @@ public function renderQuotes(array $data, $render = true, $renderAsString = true * * @param QuotedMessage[] $messages * @return void + * @deprecated 6.2 */ protected function overrideIsFullQuote(array $messages) { @@ -110,6 +122,27 @@ protected function overrideIsFullQuote(array $messages) * * @param mixed[][] $data * @return QuotedMessage[] + * @deprecated 6.2 Implement `getMessage()` instead. + */ + protected function getMessages(array $data) + { + throw new NotImplementedException(); + } + + /** + * @return list + * @deprecated 6.2 */ - abstract protected function getMessages(array $data); + public function legacyGetMessages(int $objectID, string $marker): array + { + return $this->getMessages([ + $objectID => [$marker], + ]); + } + + #[\Override] + public function getMessage(int $objectID): ?IMessage + { + throw new NotImplementedException(); + } } diff --git a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php index 8b6543815f7..8daee52f60d 100644 --- a/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/IMessageQuoteHandler.class.php @@ -2,32 +2,24 @@ namespace wcf\system\message\quote; +use wcf\data\IMessage; + /** * Default interface for quote handlers. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * - * @deprecated 6.2 + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ interface IMessageQuoteHandler { /** - * Renders a template for given quotes. + * Returns the message identified by the provided object id. * - * @param mixed[][] $data - * @param bool $supportPaste - * @return string - */ - public function render(array $data, $supportPaste = false); - - /** - * Renders a list of quotes for insertation. + * If the object does not exist or is inaccessible by the current user, + * `null` must be returned instead. * - * @param mixed[][] $data - * @param bool $render - * @return string[] + * @since 6.2 */ - public function renderQuotes(array $data, $render = true); + public function getMessage(int $objectID): ?IMessage; } diff --git a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php index 0e9d21a9aca..b0030e31f4b 100644 --- a/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php +++ b/wcfsetup/install/files/lib/system/message/quote/MessageQuoteManager.class.php @@ -3,7 +3,11 @@ namespace wcf\system\message\quote; use wcf\data\IMessage; +use wcf\data\object\type\ObjectTypeCache; +use wcf\data\user\avatar\DefaultAvatar; +use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\event\EventHandler; +use wcf\system\html\input\HtmlInputProcessor; use wcf\system\SingletonFactory; use wcf\system\WCF; use wcf\util\ArrayUtil; @@ -15,7 +19,7 @@ * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License */ -class MessageQuoteManager extends SingletonFactory +final class MessageQuoteManager extends SingletonFactory { /** * list of quote ids to be removed @@ -30,6 +34,19 @@ class MessageQuoteManager extends SingletonFactory */ protected array $usedQuotes = []; + /** + * @var array{ + * objectType: string, + * parentObjectID: int, + * objectID: int, + * message: string, + * fullQuote: string, + * } + */ + private array $legacyQuoteData; + + private const LEGACY_QUOTE_MARKER = '@@@legacy_quote@@@'; + /** * @inheritDoc */ @@ -65,7 +82,33 @@ public function addQuote( $fullQuote = '', $returnFalseIfExists = true ) { - return false; + if (isset($this->legacyQuoteData)) { + throw new \RuntimeException("Cannot store another quote, there is already one legacy quote present."); + } + + if ($fullQuote !== '') { + $htmlInputProcessor = new HtmlInputProcessor(); + $htmlInputProcessor->processIntermediate($fullQuote); + + if (MESSAGE_MAX_QUOTE_DEPTH) { + $htmlInputProcessor->enforceQuoteDepth(MESSAGE_MAX_QUOTE_DEPTH - 1, true); + } + + $parameters = ['htmlInputProcessor' => $htmlInputProcessor]; + EventHandler::getInstance()->fireAction($this, 'addFullQuote', $parameters); + + $fullQuote = $htmlInputProcessor->getHtml(); + } + + $this->legacyQuoteData = [ + 'objectType' => $objectType, + 'parentObjectID' => $parentObjectID, + 'objectID' => $objectID, + 'message' => $message, + 'fullQuote' => $fullQuote, + ]; + + return self::LEGACY_QUOTE_MARKER; } /** @@ -107,7 +150,48 @@ public function removeQuote($quoteID) */ public function getQuoteComponents($quoteID) { - return false; + if ($quoteID !== self::LEGACY_QUOTE_MARKER) { + throw new \RuntimeException("Encountered an unexpected quote id, found '{$quoteID}'."); + } + + \assert(isset($this->legacyQuoteData)); + + $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message.quote', $this->legacyQuoteData['objectType']); + if ($objectType === null) { + throw new \RuntimeException("Cannot find the object type '{$this->legacyQuoteData['objectType']}' for quotes."); + } + + $quoteHandler = \call_user_func([$objectType->className, 'getInstance']); + \assert($quoteHandler instanceof AbstractMessageQuoteHandler); + + $messages = $quoteHandler->legacyGetMessages($this->legacyQuoteData['objectID'], self::LEGACY_QUOTE_MARKER); + $message = \current($messages); + \assert($message !== false); + + $avatar = ''; + if ($message->getUserID()) { + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($message->getUserID()); + if ($userProfile !== null) { + $avatar = $userProfile->getAvatar()->getURL(); + } + } + + if ($avatar === '') { + $avatar = (new DefaultAvatar($message->getUsername()))->getURL(); + } + + $messageData = $quoteHandler->renderQuotes([ + $this->legacyQuoteData['objectID'] => [self::LEGACY_QUOTE_MARKER], + ]); + + return [ + 'objectID' => $this->legacyQuoteData['objectID'], + 'author' => $message->getUsername(), + 'avatar' => $avatar, + 'link' => $message->getLink(), + 'message' => $messageData[0], + 'rawMessage' => $this->isFullQuote(self::LEGACY_QUOTE_MARKER) ? $message->getMessage() : $this->legacyQuoteData['message'], + ]; } /** @@ -168,6 +252,13 @@ public function getQuotesByParentObjectID($objectType, $parentObjectID, $markFor */ public function getQuote($quoteID, $useFullQuote = true) { + \assert(isset($this->legacyQuoteData)); + if ($useFullQuote && $this->legacyQuoteData['fullQuote'] !== '') { + return $this->legacyQuoteData['fullQuote']; + } else { + return $this->legacyQuoteData['message']; + } + return null; } @@ -356,7 +447,9 @@ public function removeOrphanedQuotes(array $quoteIDs) {} */ public function isFullQuote($quoteID) { - return false; + \assert(isset($this->legacyQuoteData)); + + return $this->legacyQuoteData['fullQuote'] !== ''; } /**