Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ Moved the following controllers:
- Deprecated `craft\models\SystemMessage` and `craft\records\SystemMessage`. `CraftCms\Cms\SystemMessage\Models\SystemMessage` should be used instead.
- Replaced `craft\controllers\SystemMessagesController` with `CraftCms\Cms\Http\Controllers\Utilities\SystemMessagesController`

## Tokens

- Deprecated `craft\services\Tokens`. `CraftCms\Cms\RouteToken\RouteTokens` should be used instead.
- Deprecated `craft\records\Token`. `CraftCms\Cms\RouteToken\Models\RouteToken` should be used instead.

## Translations

- Deprecated `craft\i18n\FormatConverter`. `CraftCms\Cms\Translation\FormatConverter` should be used instead.
Expand Down
32 changes: 32 additions & 0 deletions routes/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use CraftCms\Cms\Http\Controllers\PluginStore\InstallController as PluginStoreInstallController;
use CraftCms\Cms\Http\Controllers\PluginStore\PluginStoreController;
use CraftCms\Cms\Http\Controllers\PluginStore\RemoveController;
use CraftCms\Cms\Http\Controllers\PreviewController;
use CraftCms\Cms\Http\Controllers\Settings\EntryTypesController;
use CraftCms\Cms\Http\Controllers\Settings\GeneralSettingsController;
use CraftCms\Cms\Http\Controllers\Settings\RoutesController;
Expand All @@ -30,6 +31,7 @@
use CraftCms\Cms\Http\Controllers\StructuresController;
use CraftCms\Cms\Http\Controllers\Updates\UpdaterController;
use CraftCms\Cms\Http\Controllers\Updates\UpdatesController;
use CraftCms\Cms\Http\Controllers\Users\ImpersonationController;
use CraftCms\Cms\Http\Controllers\Utilities\ClearCachesController;
use CraftCms\Cms\Http\Controllers\Utilities\DbBackupController;
use CraftCms\Cms\Http\Controllers\Utilities\DeprecationErrorsController;
Expand All @@ -40,6 +42,22 @@
use CraftCms\Cms\Http\Controllers\Utilities\UtilitiesController;
use CraftCms\Cms\Http\Middleware\RequireAdmin;
use CraftCms\Cms\Http\Middleware\RequireAdminChanges;
use CraftCms\Cms\Http\Middleware\RequireElevatedSession;
use CraftCms\Cms\Http\Middleware\RequireToken;
use CraftCms\Cms\Support\Str;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;

/**
* Actions that should not have CSRF token verification. These are automatically
* mapped to `/{cpTrigger}/{actionTrigger}/route` and `/{actionTrigger}/{route}`
*/
VerifyCsrfToken::except(collect([
'preview/preview',
])->flatMap(fn (string $route) => [
$route,
Cms::config()->actionTrigger.Str::start($route, '/'),
Cms::config()->cpTrigger.'/'.Cms::config()->actionTrigger.Str::start($route, '/'),
])->all());

/**
* Actions that are accessible without CP can be registered here.
Expand All @@ -50,6 +68,11 @@
Route::middleware(['auth'])->group(function () {
Route::post('entries/save-entry', StoreEntryController::class);
});

Route::middleware([RequireToken::class])->group(function () {
Route::any('preview/preview', [PreviewController::class, 'preview'])->name('preview');
Route::any('users/impersonate-with-token', [ImpersonationController::class, 'withToken']);
});
});

Route::prefix(implode('/', [
Expand Down Expand Up @@ -142,6 +165,9 @@
// Migrations
Route::post('utilities/apply-new-migrations', MigrationsController::class);

// Preview
Route::any('preview/create-token', [PreviewController::class, 'createToken']);

// Widgets
Route::post('dashboard/create-widget', [WidgetsController::class, 'store']);
Route::post('dashboard/save-widget-settings', [WidgetsController::class, 'update']);
Expand Down Expand Up @@ -238,6 +264,12 @@
Route::post('app/check-for-updates', [UpdatesController::class, 'check']);
Route::post('app/cache-updates', [UpdatesController::class, 'cache']);

// Users
Route::middleware([RequireElevatedSession::class])->group(function () {
Route::post('users/impersonate', [ImpersonationController::class, 'impersonate']);
Route::post('users/get-impersonation-url', [ImpersonationController::class, 'getUrl']);
});

// Pluginstore
Route::middleware([
RequireAdmin::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use CraftCms\Cms\Database\Migration;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
if (Schema::hasTable('routetokens')) {
Schema::dropIfExists('tokens');

return;
}

Schema::rename('tokens', 'routetokens');
}

public function down(): void
{
Schema::rename('routetokens', 'tokens');
}
};
28 changes: 14 additions & 14 deletions src/Database/Migrations/Install.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,18 @@ public function createTables(): void
$table->char('uid', 36)->default('0');
});

Schema::create('routetokens', function (Blueprint $table) {
$table->integer('id', true);
$table->char('token', 32);
$table->text('route')->nullable();
$table->unsignedTinyInteger('usageLimit')->nullable();
$table->unsignedTinyInteger('usageCount')->nullable();
$table->dateTime('expiryDate');
$table->dateTime('dateCreated');
$table->dateTime('dateUpdated');
$table->char('uid', 36)->default('0');
});

Schema::create(Table::SEARCHINDEXQUEUE, function (Blueprint $table) {
$table->integer('id', true);
$table->integer('elementId');
Expand Down Expand Up @@ -739,18 +751,6 @@ public function createTables(): void
$table->dateTime('dateUpdated');
});

Schema::create(Table::TOKENS, function (Blueprint $table) {
$table->integer('id', true);
$table->char('token', 32);
$table->text('route')->nullable();
$table->unsignedTinyInteger('usageLimit')->nullable();
$table->unsignedTinyInteger('usageCount')->nullable();
$table->dateTime('expiryDate');
$table->dateTime('dateCreated');
$table->dateTime('dateUpdated');
$table->char('uid', 36)->default('0');
});

Schema::create(Table::USERGROUPS, function (Blueprint $table) {
$table->integer('id', true);
$table->string('name');
Expand Down Expand Up @@ -996,8 +996,8 @@ public function createIndexes(): void
Schema::createIndex(Table::TAGGROUPS, ['handle']);
Schema::createIndex(Table::TAGGROUPS, ['dateDeleted']);
Schema::createIndex(Table::TAGS, ['groupId']);
Schema::createIndex(Table::TOKENS, ['token'], unique: true);
Schema::createIndex(Table::TOKENS, ['expiryDate']);
Schema::createIndex(Table::ROUTETOKENS, ['token'], unique: true);
Schema::createIndex(Table::ROUTETOKENS, ['expiryDate']);
Schema::createIndex(Table::USERGROUPS, ['handle']);
Schema::createIndex(Table::USERGROUPS, ['name']);
Schema::createIndex(Table::USERGROUPS_USERS, ['groupId', 'userId'], unique: true);
Expand Down
4 changes: 2 additions & 2 deletions src/Database/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@

public const string REVISIONS = 'revisions';

public const string ROUTETOKENS = 'routetokens';

public const string SEARCHINDEX = 'searchindex';

public const string SEARCHINDEXQUEUE = 'searchindexqueue';
Expand Down Expand Up @@ -127,8 +129,6 @@

public const string TAGS = 'tags';

public const string TOKENS = 'tokens';

public const string USERGROUPS = 'usergroups';

public const string USERGROUPS_USERS = 'usergroups_users';
Expand Down
104 changes: 104 additions & 0 deletions src/Http/Controllers/PreviewController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Http\Controllers;

use craft\helpers\ElementHelper;
use CraftCms\Cms\Http\EnforcesPermissions;
use CraftCms\Cms\Http\Middleware\HandleTokenRequest;
use CraftCms\Cms\RouteToken\Data\RouteToken;
use CraftCms\Cms\RouteToken\RouteTokens;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Context;

use function CraftCms\Cms\t;

final readonly class PreviewController
{
use EnforcesPermissions;

public function createToken(Request $request, RouteTokens $tokens, RouteToken $tokenData): JsonResponse|RedirectResponse
{
match (true) {
isset($tokenData->draftId) => $this->requirePermission("previewDraft:{$tokenData->draftId}"),
isset($tokenData->revisionId) => $this->requirePermission("previewRevision:{$tokenData->revisionId}"),
default => $this->requirePermission("previewElement:{$tokenData->getCanonicalId()}"),
};

$token = $tokens->createPreviewToken([
route('craft.actions.preview', absolute: false), [
'elementType' => $tokenData->elementType,
'canonicalId' => $tokenData->getCanonicalId(),
'siteId' => $tokenData->siteId,
'draftId' => $tokenData->draftId ?? null,
'revisionId' => $tokenData->revisionId ?? null,
'userId' => $request->user()->id,
],
], token: $tokenData->previewToken);

abort_if($token === false, 500, t('Could not create a preview token.'));

if (isset($tokenData->redirect)) {
return redirect($tokenData->redirect);
}

return new JsonResponse(compact('token'));
}

public function preview(Request $request, Kernel $kernel, RouteToken $tokenData): mixed
{
$query = $tokenData->elementType::find()
->siteId($tokenData->siteId)
->status(null);

$elementFn = match (true) {
! is_null($tokenData->draftId) => fn () => $query->draftId($tokenData->draftId)->one(),
! is_null($tokenData->revisionId) => fn () => $query->revisionId($tokenData->revisionId)->one(),
! is_null($tokenData->userId) => function () use ($tokenData, $query) {
ElementHelper::setProvisionalDraftUser($tokenData->userId);

$element = (clone $query)
->draftOf($tokenData->canonicalId)
->provisionalDrafts()
->draftCreator($tokenData->userId)
->one();

return $element ?? $query->id($tokenData->canonicalId)->one();
},
default => fn () => null,
};

if ($element = $elementFn()) {
if (! $element->lft && $element->getIsDerivative()) {
// See if we can add structure data to it
$canonical = $element->getCanonical(true);
$element->structureId = $canonical->structureId;
$element->root = $canonical->root;
$element->lft = $canonical->lft;
$element->rgt = $canonical->rgt;
$element->level = $canonical->level;
}

$element->previewing = true;
\Craft::$app->getElements()->setPlaceholderElement($element);
}

/** @var \Illuminate\Support\Uri $originalUri */
$originalUri = Context::pullHidden(HandleTokenRequest::ORIGINAL_URI_KEY);

$response = $kernel->handle($request->duplicateWithUri(
newUri: $originalUri->value(),
query: $originalUri->query()->all()
));

return match (true) {
$response instanceof Response => $response->setNoCacheHeaders(),
default => $response,
};
}
}
2 changes: 1 addition & 1 deletion src/Http/Controllers/Settings/SectionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public function store(
Sections $sections,
Section $section,
): Response {
$sectionId = $request->input('sectionId');
$sectionId = $request->integer('sectionId');

if ($sectionId) {
abort_if(is_null($sections->getSectionById($sectionId)), 404, "Invalid section ID: $sectionId");
Expand Down
Loading
Loading