From c452ffcad3e7b2ed9be184ed75d8e24cbefc9ca8 Mon Sep 17 00:00:00 2001 From: Lukas Bestle Date: Thu, 5 Dec 2024 21:11:33 +0100 Subject: [PATCH] New structure for preview tokens - Use URI instead of ID + template to be able to generate tokens for arbitrary custom blueprint preview URIs - Include version ID (in a secure way wrapped in JSON) to differentiate preview access by version - Shorten the token to 10 characters to make the preview URLs more manageable --- src/Content/Version.php | 33 +++++++++++++++++++--- tests/Cms/Pages/PageRenderTest.php | 30 ++++++++++++++++++++ tests/Cms/Pages/PageTest.php | 3 +- tests/Content/VersionTest.php | 45 ++++++++++++++++++++++-------- 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src/Content/Version.php b/src/Content/Version.php index a6801777dc..0ef61b751b 100644 --- a/src/Content/Version.php +++ b/src/Content/Version.php @@ -12,6 +12,7 @@ use Kirby\Exception\NotFoundException; use Kirby\Form\Form; use Kirby\Http\Uri; +use Kirby\Toolkit\Str; /** * The Version class handles all actions for a single @@ -393,7 +394,7 @@ protected function prepareFieldsForContent( /** * Returns a verification token for the authentication - * of draft previews + * of draft and version previews * @internal */ public function previewToken(): string @@ -413,10 +414,34 @@ public function previewToken(): string throw new LogicException('Invalid model type'); } - return $this->model->kirby()->contentToken( - $this->model, - $this->model->id() . $this->model->template() + return $this->previewTokenFromUrl($this->model->url()) + ?? throw new LogicException('Cannot produce local preview token for model'); + } + + /** + * Returns a verification token for the authentication + * of draft and version previews from a raw URL + * if the URL comes from the same site + */ + protected function previewTokenFromUrl(string $url): string|null + { + $localPrefix = $this->model->kirby()->url('base') . '/'; + + if (Str::startsWith($url, $localPrefix) === false) { + return null; + } + + $data = [ + 'uri' => Str::after($url, $localPrefix), + 'versionId' => $this->id->value() + ]; + + $token = $this->model->kirby()->contentToken( + null, + json_encode($data) ); + + return substr($token, 0, 10); } /** diff --git a/tests/Cms/Pages/PageRenderTest.php b/tests/Cms/Pages/PageRenderTest.php index f2bd2cf94f..b267fffd9d 100644 --- a/tests/Cms/Pages/PageRenderTest.php +++ b/tests/Cms/Pages/PageRenderTest.php @@ -785,6 +785,36 @@ public function testRenderVersionFromRequestAuthenticatedDraft() $this->assertSame('changes', $page->renderVersionFromRequest()->value()); } + /** + * @covers ::renderVersionFromRequest + */ + public function testRenderVersionFromRequestMismatch() + { + $page = $this->app->page('default'); + + $this->app->clone([ + 'request' => [ + 'query' => [ + '_token' => $page->version('changes')->previewToken(), + '_version' => 'latest' + ] + ] + ]); + + $this->assertSame('latest', $page->renderVersionFromRequest()->value()); + + $this->app->clone([ + 'request' => [ + 'query' => [ + '_token' => $page->version('latest')->previewToken(), + '_version' => 'changes' + ] + ] + ]); + + $this->assertSame('latest', $page->renderVersionFromRequest()->value()); + } + /** * @covers ::renderVersionFromRequest */ diff --git a/tests/Cms/Pages/PageTest.php b/tests/Cms/Pages/PageTest.php index adc12d23ec..f463c51ff7 100644 --- a/tests/Cms/Pages/PageTest.php +++ b/tests/Cms/Pages/PageTest.php @@ -677,9 +677,10 @@ public function testCustomPreviewUrl( ]); if ($draft === true && $expected !== null) { + $expectedToken = substr(hash_hmac('sha1', '{"uri":"' . $page->uri() . '","versionId":"latest"}', $page->kirby()->root('content')), 0, 10); $expected = str_replace( '{token}', - '_token=' . hash_hmac('sha1', $page->id() . $page->template(), $page->kirby()->root('content') . '/' . $page->id()), + '_token=' . $expectedToken, $expected ); } diff --git a/tests/Content/VersionTest.php b/tests/Content/VersionTest.php index e9c20dfc51..e317851a25 100644 --- a/tests/Content/VersionTest.php +++ b/tests/Content/VersionTest.php @@ -786,14 +786,16 @@ public function testPreviewToken() model: $this->app->site(), id: VersionId::latest() ); - $this->assertSame(hash_hmac('sha1', 'home' . 'default', static::TMP . '/content/home'), $version->previewToken()); + $expected = substr(hash_hmac('sha1', '{"uri":"","versionId":"latest"}', static::TMP . '/content'), 0, 10); + $this->assertSame($expected, $version->previewToken()); // page $version = new Version( model: $this->model, id: VersionId::latest() ); - $this->assertSame(hash_hmac('sha1', 'a-page' . 'default', static::TMP . '/content/a-page'), $version->previewToken()); + $expected = substr(hash_hmac('sha1', '{"uri":"a-page","versionId":"latest"}', static::TMP . '/content'), 0, 10); + $this->assertSame($expected, $version->previewToken()); } /** @@ -816,7 +818,8 @@ public function testPreviewTokenCustomSalt() id: VersionId::latest() ); - $this->assertSame(hash_hmac('sha1', 'a-page' . 'default', 'testsalt'), $version->previewToken()); + $expected = substr(hash_hmac('sha1', '{"uri":"a-page","versionId":"latest"}', 'testsalt'), 0, 10); + $this->assertSame($expected, $version->previewToken()); } /** @@ -829,20 +832,22 @@ public function testPreviewTokenCustomSaltCallback() $this->app = $this->app->clone([ 'options' => [ 'content' => [ - 'salt' => fn ($page) => $page->date() + 'salt' => function ($model) { + $this->assertNull($model); + + return 'salt-lake-city'; + } ] ] ]); - $this->app->impersonate('kirby'); - $model = $this->model->update(['date' => '2012-12-12']); - $version = new Version( - model: $model, + model: $this->model, id: VersionId::latest() ); - $this->assertSame(hash_hmac('sha1', 'a-page' . 'default', '2012-12-12'), $version->previewToken()); + $expected = substr(hash_hmac('sha1', '{"uri":"a-page","versionId":"latest"}', 'salt-lake-city'), 0, 10); + $this->assertSame($expected, $version->previewToken()); } /** @@ -1589,9 +1594,18 @@ public function testUrlPageCustom( ]); if ($expected !== null) { + $expectedToken = substr( + hash_hmac( + 'sha1', + '{"uri":"' . $page->uri() . '","versionId":"' . $versionId . '"}', + $page->kirby()->root('content') + ), + 0, + 10 + ); $expected = str_replace( '{token}', - '_token=' . hash_hmac('sha1', $page->id() . $page->template(), $page->kirby()->root('content') . '/' . $page->id()), + '_token=' . $expectedToken, $expected ); } @@ -1706,9 +1720,18 @@ public function testUrlSiteCustom( $site = $app->site(); if ($expected !== null) { + $expectedToken = substr( + hash_hmac( + 'sha1', + '{"uri":"","versionId":"' . $versionId . '"}', + $site->kirby()->root('content') + ), + 0, + 10 + ); $expected = str_replace( '{token}', - '_token=' . hash_hmac('sha1', 'home' . 'default', $site->kirby()->root('content') . '/home'), + '_token=' . $expectedToken, $expected ); }