From: Alexander Ebert Date: Tue, 12 Mar 2024 15:44:58 +0000 (+0100) Subject: Add support for `DELETE` requests X-Git-Tag: 6.1.0_Alpha_1~148^2~11 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=529c3e1b8b557a299b37dbdc1e53e985dd694afd;p=GitHub%2FWoltLab%2FWCF.git Add support for `DELETE` requests --- diff --git a/com.woltlab.wcf/templates/accountSecurity.tpl b/com.woltlab.wcf/templates/accountSecurity.tpl index 852e764dd6..7786a5c869 100644 --- a/com.woltlab.wcf/templates/accountSecurity.tpl +++ b/com.woltlab.wcf/templates/accountSecurity.tpl @@ -107,12 +107,10 @@ diff --git a/ts/WoltLabSuite/Core/Ajax/Backend.ts b/ts/WoltLabSuite/Core/Ajax/Backend.ts index cc71f3e497..5f0e612ce7 100644 --- a/ts/WoltLabSuite/Core/Ajax/Backend.ts +++ b/ts/WoltLabSuite/Core/Ajax/Backend.ts @@ -19,6 +19,7 @@ import { import { extend, getXsrfToken } from "../Core"; const enum RequestType { + DELETE, GET, POST, } @@ -32,6 +33,10 @@ class SetupRequest { this.url = url; } + delete(): BackendRequest { + return new BackendRequest(this.url, RequestType.DELETE); + } + get(): GetRequest { return new GetRequest(this.url, RequestType.GET); } diff --git a/ts/WoltLabSuite/Core/Api/Error.ts b/ts/WoltLabSuite/Core/Api/Error.ts new file mode 100644 index 0000000000..b09ba2697e --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Error.ts @@ -0,0 +1,10 @@ +type RequestFailureType = "api_error" | "invalid_request_error"; + +export class ApiError { + constructor( + public readonly type: RequestFailureType, + public readonly code: string, + public readonly message: string, + public readonly param: string, + ) {} +} diff --git a/ts/WoltLabSuite/Core/Api/Result.ts b/ts/WoltLabSuite/Core/Api/Result.ts new file mode 100644 index 0000000000..138e1990f1 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Result.ts @@ -0,0 +1,69 @@ +import { StatusNotOk } from "../Ajax/Error"; +import { isPlainObject } from "../Core"; +import { ApiError } from "./Error"; + +export type ApiResult = + | { + ok: true; + value: T; + unwrap(): T; + } + | { + ok: false; + error: ApiError; + unwrap(): never; + }; + +export function apiResultFromValue(value: T): ApiResult { + return { + ok: true, + value, + unwrap() { + return value; + }, + }; +} + +export function apiResultFromError(error: ApiError): ApiResult { + return { + ok: false, + error, + unwrap() { + throw error; + }, + }; +} + +export async function apiResultFromStatusNotOk(e: StatusNotOk): Promise> { + const { response } = e; + + if (response === undefined) { + // Aborted requests do not have a return value. + throw e; + } + + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + throw e; + } + + let json: unknown; + try { + json = await response.json(); + } catch { + throw e; + } + + if ( + isPlainObject(json) && + Object.hasOwn(json, "type") && + (json.type === "api_error" || json.type === "invalid_request_error") && + typeof json.code === "string" && + typeof json.message === "string" && + typeof json.param === "string" + ) { + return apiResultFromError(new ApiError(json.type, json.code, json.message, json.param)); + } + + throw e; +} diff --git a/ts/WoltLabSuite/Core/Api/Sessions/DeleteSession.ts b/ts/WoltLabSuite/Core/Api/Sessions/DeleteSession.ts new file mode 100644 index 0000000000..b7c37fe730 --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Sessions/DeleteSession.ts @@ -0,0 +1,12 @@ +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result"; + +export async function deleteSession(sessionId: string): Promise> { + try { + await prepareRequest(`${window.WSC_API_URL}index.php?api/rpc/core/sessions/${sessionId}`).delete().fetchAsJson(); + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue([]); +} diff --git a/ts/WoltLabSuite/Core/Core.ts b/ts/WoltLabSuite/Core/Core.ts index 247fe203f1..d76a8bb70d 100644 --- a/ts/WoltLabSuite/Core/Core.ts +++ b/ts/WoltLabSuite/Core/Core.ts @@ -143,7 +143,7 @@ export function inherit(constructor: new () => any, superConstructor: new () => /** * Returns true if `obj` is an object literal. */ -export function isPlainObject(obj: unknown): boolean { +export function isPlainObject(obj: unknown): obj is Record { if (typeof obj !== "object" || obj === null) { return false; } diff --git a/ts/WoltLabSuite/Core/Ui/Search/Page.ts b/ts/WoltLabSuite/Core/Ui/Search/Page.ts index 712a48fdbb..663c637a1e 100644 --- a/ts/WoltLabSuite/Core/Ui/Search/Page.ts +++ b/ts/WoltLabSuite/Core/Ui/Search/Page.ts @@ -24,7 +24,7 @@ function click(event: MouseEvent): void { const data = JSON.parse(target.dataset.parameters || ""); if (Core.isPlainObject(data)) { Object.keys(data).forEach((key) => { - parameters.set(key, data[key]); + parameters.set(key, data[key] as string); }); } } catch (e) { diff --git a/ts/WoltLabSuite/Core/Ui/User/Session/Delete.ts b/ts/WoltLabSuite/Core/Ui/User/Session/Delete.ts index ffda6f24e7..7c241e8337 100644 --- a/ts/WoltLabSuite/Core/Ui/User/Session/Delete.ts +++ b/ts/WoltLabSuite/Core/Ui/User/Session/Delete.ts @@ -7,73 +7,26 @@ * @woltlabExcludeBundle all */ -import * as Ajax from "../../../Ajax"; -import { AjaxCallbackObject, AjaxCallbackSetup, DatabaseObjectActionResponse } from "../../../Ajax/Data"; import * as UiNotification from "../../Notification"; import * as UiConfirmation from "../../Confirmation"; import * as Language from "../../../Language"; -import * as Core from "../../../Core"; +import { deleteSession } from "WoltLabSuite/Core/Api/Sessions/DeleteSession"; -export class UiUserSessionDelete implements AjaxCallbackObject { - private readonly knownElements = new Map(); +function onClick(button: HTMLElement): void { + UiConfirmation.show({ + message: Language.get("wcf.user.security.deleteSession.confirmMessage"), + confirm: async (_parameters) => { + (await deleteSession(button.dataset.sessionId!)).unwrap(); - /** - * Initializes the session delete buttons. - */ - constructor() { - document.querySelectorAll(".sessionDeleteButton").forEach((element: HTMLElement) => { - if (!element.dataset.sessionId) { - throw new Error(`No sessionId for session delete button given.`); - } + button.closest("li")?.remove(); - if (!this.knownElements.has(element.dataset.sessionId)) { - element.addEventListener("click", (ev) => this.delete(element, ev)); - - this.knownElements.set(element.dataset.sessionId, element); - } - }); - } - - /** - * Opens the user trophy list for a specific user. - */ - private delete(element: HTMLElement, event: MouseEvent): void { - event.preventDefault(); - - UiConfirmation.show({ - message: Language.get("wcf.user.security.deleteSession.confirmMessage"), - confirm: (_parameters) => { - Ajax.api(this, { - t: Core.getXsrfToken(), - sessionID: element.dataset.sessionId, - }); - }, - }); - } - - _ajaxSuccess(data: AjaxResponse): void { - const element = this.knownElements.get(data.sessionID); - - if (element !== undefined) { - const sessionItem = element.closest("li"); - - if (sessionItem !== null) { - sessionItem.remove(); - } - } - - UiNotification.show(); - } - - _ajaxSetup(): ReturnType { - return { - url: "index.php?delete-session/", - }; - } + UiNotification.show(); + }, + }); } -export default UiUserSessionDelete; - -interface AjaxResponse extends DatabaseObjectActionResponse { - sessionID: string; +export function setup() { + document.querySelectorAll(".sessionDeleteButton").forEach((element: HTMLElement) => { + element.addEventListener("click", () => onClick(element)); + }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js index aeb976768d..38b61308d5 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Backend.js @@ -16,11 +16,14 @@ define(["require", "exports", "tslib", "./Status", "./Error", "../Core"], functi constructor(url) { this.url = url; } + delete() { + return new BackendRequest(this.url, 0 /* RequestType.DELETE */); + } get() { - return new GetRequest(this.url, 0 /* RequestType.GET */); + return new GetRequest(this.url, 1 /* RequestType.GET */); } post(payload) { - return new BackendRequest(this.url, 1 /* RequestType.POST */, payload); + return new BackendRequest(this.url, 2 /* RequestType.POST */, payload); } } let ignoreConnectionErrors = false; @@ -89,7 +92,7 @@ define(["require", "exports", "tslib", "./Status", "./Error", "../Core"], functi cache: this.#allowCaching ? "default" : "no-store", redirect: "error", }, requestOptions); - if (this.#type === 1 /* RequestType.POST */) { + if (this.#type === 2 /* RequestType.POST */) { init.method = "POST"; if (this.#payload) { if (this.#payload instanceof FormData) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js new file mode 100644 index 0000000000..cb571bbf8a --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Error.js @@ -0,0 +1,18 @@ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.ApiError = void 0; + class ApiError { + type; + code; + message; + param; + constructor(type, code, message, param) { + this.type = type; + this.code = code; + this.message = message; + this.param = param; + } + } + exports.ApiError = ApiError; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Result.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Result.js new file mode 100644 index 0000000000..76fa08ce86 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Result.js @@ -0,0 +1,53 @@ +define(["require", "exports", "../Core", "./Error"], function (require, exports, Core_1, Error_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.apiResultFromStatusNotOk = exports.apiResultFromError = exports.apiResultFromValue = void 0; + function apiResultFromValue(value) { + return { + ok: true, + value, + unwrap() { + return value; + }, + }; + } + exports.apiResultFromValue = apiResultFromValue; + function apiResultFromError(error) { + return { + ok: false, + error, + unwrap() { + throw error; + }, + }; + } + exports.apiResultFromError = apiResultFromError; + async function apiResultFromStatusNotOk(e) { + const { response } = e; + if (response === undefined) { + // Aborted requests do not have a return value. + throw e; + } + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + throw e; + } + let json; + try { + json = await response.json(); + } + catch { + throw e; + } + if ((0, Core_1.isPlainObject)(json) && + Object.hasOwn(json, "type") && + (json.type === "api_error" || json.type === "invalid_request_error") && + typeof json.code === "string" && + typeof json.message === "string" && + typeof json.param === "string") { + return apiResultFromError(new Error_1.ApiError(json.type, json.code, json.message, json.param)); + } + throw e; + } + exports.apiResultFromStatusNotOk = apiResultFromStatusNotOk; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Sessions/DeleteSession.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Sessions/DeleteSession.js new file mode 100644 index 0000000000..5df2e89751 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Sessions/DeleteSession.js @@ -0,0 +1,15 @@ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "../Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.deleteSession = void 0; + async function deleteSession(sessionId) { + try { + await (0, Backend_1.prepareRequest)(`${window.WSC_API_URL}index.php?api/rpc/core/sessions/${sessionId}`).delete().fetchAsJson(); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)([]); + } + exports.deleteSession = deleteSession; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Session/Delete.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Session/Delete.js index 97d52d61a3..6c1aa6e892 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Session/Delete.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Session/Delete.js @@ -6,62 +6,27 @@ * @license GNU Lesser General Public License * @woltlabExcludeBundle all */ -define(["require", "exports", "tslib", "../../../Ajax", "../../Notification", "../../Confirmation", "../../../Language", "../../../Core"], function (require, exports, tslib_1, Ajax, UiNotification, UiConfirmation, Language, Core) { +define(["require", "exports", "tslib", "../../Notification", "../../Confirmation", "../../../Language", "WoltLabSuite/Core/Api/Sessions/DeleteSession"], function (require, exports, tslib_1, UiNotification, UiConfirmation, Language, DeleteSession_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.UiUserSessionDelete = void 0; - Ajax = tslib_1.__importStar(Ajax); + exports.setup = void 0; UiNotification = tslib_1.__importStar(UiNotification); UiConfirmation = tslib_1.__importStar(UiConfirmation); Language = tslib_1.__importStar(Language); - Core = tslib_1.__importStar(Core); - class UiUserSessionDelete { - knownElements = new Map(); - /** - * Initializes the session delete buttons. - */ - constructor() { - document.querySelectorAll(".sessionDeleteButton").forEach((element) => { - if (!element.dataset.sessionId) { - throw new Error(`No sessionId for session delete button given.`); - } - if (!this.knownElements.has(element.dataset.sessionId)) { - element.addEventListener("click", (ev) => this.delete(element, ev)); - this.knownElements.set(element.dataset.sessionId, element); - } - }); - } - /** - * Opens the user trophy list for a specific user. - */ - delete(element, event) { - event.preventDefault(); - UiConfirmation.show({ - message: Language.get("wcf.user.security.deleteSession.confirmMessage"), - confirm: (_parameters) => { - Ajax.api(this, { - t: Core.getXsrfToken(), - sessionID: element.dataset.sessionId, - }); - }, - }); - } - _ajaxSuccess(data) { - const element = this.knownElements.get(data.sessionID); - if (element !== undefined) { - const sessionItem = element.closest("li"); - if (sessionItem !== null) { - sessionItem.remove(); - } - } - UiNotification.show(); - } - _ajaxSetup() { - return { - url: "index.php?delete-session/", - }; - } + function onClick(button) { + UiConfirmation.show({ + message: Language.get("wcf.user.security.deleteSession.confirmMessage"), + confirm: async (_parameters) => { + (await (0, DeleteSession_1.deleteSession)(button.dataset.sessionId)).unwrap(); + button.closest("li")?.remove(); + UiNotification.show(); + }, + }); } - exports.UiUserSessionDelete = UiUserSessionDelete; - exports.default = UiUserSessionDelete; + function setup() { + document.querySelectorAll(".sessionDeleteButton").forEach((element) => { + element.addEventListener("click", () => onClick(element)); + }); + } + exports.setup = setup; }); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index c085a92292..3f6fb50bbb 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -86,6 +86,7 @@ return static function (): void { $eventHandler->register(ControllerCollecting::class, static function (ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\messages\MentionSuggestions); + $event->register(new \wcf\system\endpoint\controller\core\sessions\DeleteSession); }); try { diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/sessions/DeleteSession.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/sessions/DeleteSession.class.php new file mode 100644 index 0000000000..aabf40ba60 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/sessions/DeleteSession.class.php @@ -0,0 +1,41 @@ +isOwnSessionID($sessionID)) { + throw new IllegalLinkException(); + } + + SessionHandler::getInstance()->deleteUserSession($sessionID); + + return new JsonResponse([]); + } + + private function isOwnSessionID(string $sessionID): bool + { + foreach (SessionHandler::getInstance()->getUserSessions(WCF::getUser()) as $session) { + if ($session->getSessionID() === $sessionID) { + return true; + } + } + + return false; + } +}