Guest tokens
authorMarcel Werk <burntime@woltlab.com>
Sat, 8 Jun 2024 11:34:57 +0000 (13:34 +0200)
committerMarcel Werk <burntime@woltlab.com>
Sat, 8 Jun 2024 11:34:57 +0000 (13:34 +0200)
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
ts/WoltLabSuite/Core/Component/GuestTokenDialog.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/User.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/GuestTokenDialog.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/User.js
wcfsetup/install/files/lib/action/GuestTokenDialogAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/util/UserUtil.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 0f3878219531c927cda247035f91da1cd70c4787..b70463499f5e6a483e3b53ccab451bd162b028fa 100644 (file)
@@ -67,7 +67,8 @@ window.addEventListener('pageshow', function(event) {
                User.init(
                        {@$__wcf->user->userID},
                        {if $__wcf->user->userID}'{@$__wcf->user->username|encodeJS}'{else}''{/if},
-                       {if $__wcf->user->userID}'{@$__wcf->user->getLink()|encodeJS}'{else}''{/if}
+                       {if $__wcf->user->userID}'{@$__wcf->user->getLink()|encodeJS}'{else}''{/if},
+                       '{link controller='GuestTokenDialog'}{/link}'
                );
                
                BootstrapFrontend.setup({
diff --git a/ts/WoltLabSuite/Core/Component/GuestTokenDialog.ts b/ts/WoltLabSuite/Core/Component/GuestTokenDialog.ts
new file mode 100644 (file)
index 0000000..11ee433
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * Handles the creation of guest tokens.
+ *
+ * @author    Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since     6.1
+ */
+
+import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
+import User from "WoltLabSuite/Core/User";
+
+type Response = {
+  token: string;
+};
+
+export async function getGuestToken(): Promise<string | undefined> {
+  const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint<Response>(User.guestTokenDialogEndpoint);
+
+  if (ok) {
+    return result.token;
+  }
+
+  return undefined;
+}
index 27f7ee9683ef7d5c0eb2cbf1dc61be24ee213028..6673bac1c1cd202c1dcece3c8bc27dc64a62aeb3 100644 (file)
@@ -11,6 +11,7 @@ class User {
     readonly userId: number,
     readonly username: string,
     readonly link: string,
+    readonly guestTokenDialogEndpoint: string,
   ) {}
 }
 
@@ -28,12 +29,12 @@ export = {
   /**
    * Initializes the user object.
    */
-  init(userId: number, username: string, link: string): void {
+  init(userId: number, username: string, link: string, guestTokenDialogEndpoint: string = ""): void {
     if (user) {
       throw new Error("User has already been initialized.");
     }
 
-    user = new User(userId, username, link);
+    user = new User(userId, username, link, guestTokenDialogEndpoint);
   },
 
   get userId(): number {
@@ -43,4 +44,8 @@ export = {
   get username(): string {
     return user.username;
   },
+
+  get guestTokenDialogEndpoint(): string {
+    return user.guestTokenDialogEndpoint;
+  },
 };
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GuestTokenDialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/GuestTokenDialog.js
new file mode 100644 (file)
index 0000000..90777bf
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * Handles the creation of guest tokens.
+ *
+ * @author    Marcel Werk
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since     6.1
+ */
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/User"], function (require, exports, tslib_1, Dialog_1, User_1) {
+    "use strict";
+    Object.defineProperty(exports, "__esModule", { value: true });
+    exports.getGuestToken = void 0;
+    User_1 = tslib_1.__importDefault(User_1);
+    async function getGuestToken() {
+        const { ok, result } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(User_1.default.guestTokenDialogEndpoint);
+        if (ok) {
+            return result.token;
+        }
+        return undefined;
+    }
+    exports.getGuestToken = getGuestToken;
+});
index 4a5a9dc7b14d80e5b534ed185f9c9313877fda70..242a187bfb23ad4a3313118acfdc3bbcd208f384 100644 (file)
@@ -11,10 +11,12 @@ define(["require", "exports"], function (require, exports) {
         userId;
         username;
         link;
-        constructor(userId, username, link) {
+        guestTokenDialogEndpoint;
+        constructor(userId, username, link, guestTokenDialogEndpoint) {
             this.userId = userId;
             this.username = username;
             this.link = link;
+            this.guestTokenDialogEndpoint = guestTokenDialogEndpoint;
         }
     }
     let user;
@@ -29,11 +31,11 @@ define(["require", "exports"], function (require, exports) {
         /**
          * Initializes the user object.
          */
-        init(userId, username, link) {
+        init(userId, username, link, guestTokenDialogEndpoint = "") {
             if (user) {
                 throw new Error("User has already been initialized.");
             }
-            user = new User(userId, username, link);
+            user = new User(userId, username, link, guestTokenDialogEndpoint);
         },
         get userId() {
             return user.userId;
@@ -41,5 +43,8 @@ define(["require", "exports"], function (require, exports) {
         get username() {
             return user.username;
         },
+        get guestTokenDialogEndpoint() {
+            return user.guestTokenDialogEndpoint;
+        },
     };
 });
diff --git a/wcfsetup/install/files/lib/action/GuestTokenDialogAction.class.php b/wcfsetup/install/files/lib/action/GuestTokenDialogAction.class.php
new file mode 100644 (file)
index 0000000..872089a
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+
+namespace wcf\action;
+
+use Laminas\Diactoros\Response\JsonResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\form\builder\field\CaptchaFormField;
+use wcf\system\form\builder\field\user\UsernameFormField;
+use wcf\system\form\builder\Psr15DialogForm;
+use wcf\system\WCF;
+use wcf\util\UserUtil;
+
+/**
+ * Displays a dialog that guests can use to generate a guest token to authorize themselves for certain actions.
+ *
+ * @author      Marcel Werk
+ * @copyright   2001-2024 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since       6.1
+ */
+final class GuestTokenDialogAction implements RequestHandlerInterface
+{
+    /**
+     * @inheritDoc
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        if (WCF::getUser()->userID) {
+            throw new PermissionDeniedException();
+        }
+
+        $form = $this->getForm();
+
+        if ($request->getMethod() === 'GET') {
+            return $form->toResponse();
+        } elseif ($request->getMethod() === 'POST') {
+            $response = $form->validateRequest($request);
+            if ($response !== null) {
+                return $response;
+            }
+
+            $data = $form->getData()['data'];
+
+            return new JsonResponse([
+                'result' => [
+                    'token' => UserUtil::createGuestToken($data['username']),
+                ],
+            ]);
+        } else {
+            throw new \LogicException('Unreachable');
+        }
+    }
+
+    private function getForm(): Psr15DialogForm
+    {
+        $form = new Psr15DialogForm(
+            static::class,
+            WCF::getLanguage()->get('wcf.page.guestTokenDialog.title')
+        );
+        $form->appendChildren([
+            UsernameFormField::create()
+                ->required(),
+            CaptchaFormField::create()
+                ->objectType(\CAPTCHA_TYPE),
+        ]);
+
+        $form->markRequiredFields(false);
+        $form->build();
+
+        return $form;
+    }
+}
index cbb89c476b79d89327470038d8a978283071b340..64ae47de5e70d81dc13c446ceeba0864fe547a3f 100644 (file)
@@ -3,6 +3,7 @@
 namespace wcf\util;
 
 use wcf\system\email\Mailbox;
+use wcf\system\exception\SystemException;
 use wcf\system\WCF;
 
 /**
@@ -244,6 +245,55 @@ final class UserUtil
         return \mb_substr(FileUtil::unifyDirSeparator($REQUEST_URI), 0, 255);
     }
 
+    /**
+     * Creates a guest token for the given username.
+     *
+     * @since 6.1
+     */
+    public static function createGuestToken(string $username): string
+    {
+        return CryptoUtil::createSignedString(JSON::encode(
+            [
+                'username' => $username,
+                'time' => TIME_NOW,
+            ]
+        ));
+    }
+
+    /**
+     * Verifies the given guest token and returns the stored username if the token is valid,
+     * otherwise returns null.
+     *
+     * @since 6.1
+     */
+    public static function verifyGuestToken(string $token): ?string
+    {
+        if ($token === '') {
+            return null;
+        }
+
+        $json = CryptoUtil::getValueFromSignedString($token);
+        if ($json === null) {
+            return null;
+        }
+
+        try {
+            $data = JSON::decode($json);
+        } catch (SystemException $e) {
+            return null;
+        }
+
+        if (!\is_array($data) || !isset($data['username']) || !isset($data['time'])) {
+            return null;
+        }
+
+        if ($data['time'] < \TIME_NOW - 30) {
+            return null;
+        }
+
+        return $data['username'];
+    }
+
     /**
      * Forbid creation of UserUtil objects.
      */
index b85383e5be541a8facd8397ea9f30f15fff03f4e..7f05332c21103a3ef57d99ee77e96e1193fd7925 100644 (file)
@@ -4450,6 +4450,7 @@ Dateianhänge:
                <item name="wcf.page.requestedPage.condition.reverseLogic.description"><![CDATA[Wenn ausgewählt, darf die aufgerufene Seite <strong>keine</strong> der unter „Aufgerufene Seite“ ausgewählten Seiten sein.]]></item>
                <item name="wcf.page.box.edit"><![CDATA[Box bearbeiten]]></item>
                <item name="wcf.page.menu.outstandingItems"><![CDATA[({#$menuItemNode->getOutstandingItems()} {if $menuItemNode->getOutstandingItems() == 1}neuer Eintrag{else}neue Einträge{/if})]]></item>
+               <item name="wcf.page.guestTokenDialog.title"><![CDATA[Verifizierung erforderlich]]></item>
        </category>
        <category name="wcf.paidSubscription">
                <item name="wcf.paidSubscription.availableSubscriptions"><![CDATA[Verfügbare Mitgliedschaften]]></item>
index 6e47d5585fa90065f9351501730689c591629676..5108521a81383736a3cd3f28d7586c7b1ba0b6e9 100644 (file)
@@ -4402,6 +4402,7 @@ Attachments:
                <item name="wcf.page.requestedPage.condition.reverseLogic.description"><![CDATA[If selected, the requested page <strong>may not</strong> be one of the pages selected under “Requested Page”.]]></item>
                <item name="wcf.page.box.edit"><![CDATA[Edit Box]]></item>
                <item name="wcf.page.menu.outstandingItems"><![CDATA[({#$menuItemNode->getOutstandingItems()} {if $menuItemNode->getOutstandingItems() == 1}New Item{else}News Items{/if})]]></item>
+               <item name="wcf.page.guestTokenDialog.title"><![CDATA[Verification Required]]></item>
        </category>
        <category name="wcf.acp.page">
                <item name="wcf.acp.page.add"><![CDATA[Add Page]]></item>