Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ The `php craft fields:merge` and `php craft entry-types:merge` commands will now
## Request

- Added `Request::isPreview()` macro for detecting preview requests via `x-craft-preview` or `x-craft-live-preview` parameters.
- Added `Request::isCpRequest()`, `Request::isSiteRequest()`, `Request::isActionRequest()`, `Request::actionSegments()`, `Request::actionSegmentsToRoute()`, `Request::pageNumber()`, `Request::duplicateWithUri()`, `Request::getToken()`, and `Request::getSigned()` macros.

## Security

Expand Down Expand Up @@ -923,6 +924,13 @@ Moved the following controllers:
- Added `CraftCms\Cms\View\TemplateMode` enum.
- Added `CraftCms\Cms\View\Events\RegisterCpTemplateRoots`.
- Added `CraftCms\Cms\View\Events\RegisterSiteTemplateRoots`.
- Added `CraftCms\Cms\View\TemplateCaches`.
- Added `CraftCms\Cms\View\CacheCollectors\DependencyCollector`.
- Added `CraftCms\Cms\View\CacheCollectors\ResourceCollector`.
- Added `CraftCms\Cms\View\Contracts\CacheCollectorInterface`.
- Added `CraftCms\Cms\View\Data\TemplateCacheContext`.
- Added `CraftCms\Cms\View\Events\RegisterTemplateCacheCollectors`.
- Deprecated `craft\services\TemplateCaches`. `CraftCms\Cms\View\TemplateCaches` should be used instead.
- Deprecated `craft\web\View::registerJs()`. `CraftCms\Cms\View\HtmlStack::js()` should be used instead.
- Deprecated `craft\web\View::registerJsWithVars()`. `CraftCms\Cms\View\HtmlStack::jsWithVars()` should be used instead.
- Deprecated `craft\web\View::registerJsFile()`. `CraftCms\Cms\View\HtmlStack::jsFile()` should be used instead.
Expand Down
8 changes: 4 additions & 4 deletions src/Element/Queries/Concerns/CollectsCacheTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace CraftCms\Cms\Element\Queries\Concerns;

use Craft;
use CraftCms\Cms\Element\Queries\Events\DefineCacheTags;
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\View\CacheCollectors\DependencyCollector;

/**
* @internal
Expand All @@ -31,10 +31,10 @@ protected function initCollectsCacheTags(): void
return;
}

$elementsService = Craft::$app->getElements();
$collector = app(DependencyCollector::class);

if ($elementsService->getIsCollectingCacheInfo()) {
$elementsService->collectCacheTags($cacheTags);
if ($collector->isCollecting()) {
$collector->collectTags($cacheTags);
}
});
}
Expand Down
8 changes: 5 additions & 3 deletions src/Element/Queries/Concerns/HydratesElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CraftCms\Cms\Support\Arr;
use CraftCms\Cms\Support\Json;
use CraftCms\Cms\Support\Str;
use CraftCms\Cms\View\CacheCollectors\DependencyCollector;
use Illuminate\Support\Collection;
use stdClass;

Expand Down Expand Up @@ -56,20 +57,21 @@ public function hydrate(array $items): array
$elements = $this->afterHydrate($elements)
->unless($this->asArray, function (Collection $elements) {
$elementsService = Craft::$app->getElements();
$dependencyCollector = app(DependencyCollector::class);

$allElements = $elements->all();

$elements = $elements->map(function (ElementInterface $element) use ($allElements, $elementsService) {
$elements = $elements->map(function (ElementInterface $element) use ($allElements, $dependencyCollector) {
// Set the full query result on the element, in case it's needed for lazy eager loading
$element->elementQueryResult = $allElements;

// If we're collecting cache info and the element is expirable, register its expiry date
if (
$element instanceof ExpirableElementInterface &&
$elementsService->getIsCollectingCacheInfo() &&
$dependencyCollector->isCollecting() &&
($expiryDate = $element->getExpiryDate()) !== null
) {
$elementsService->setCacheExpiryDate($expiryDate);
$dependencyCollector->setExpiryDate($expiryDate);
}

return $element;
Expand Down
224 changes: 224 additions & 0 deletions src/Http/Mixins/RequestMixin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?php

declare(strict_types=1);

namespace CraftCms\Cms\Http\Mixins;

use Closure;
use CraftCms\Cms\Cms;
use CraftCms\Cms\Http\Middleware\HandleTokenRequest;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Facades\Crypt;

final class RequestMixin
{
public function isCpRequest(): Closure
{
return function (): bool {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

return $request->is(
Cms::config()->cpTrigger,
Cms::config()->cpTrigger.'/*',
);
};
}

public function getToken(): Closure
{
return function (): ?string {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

return $request->input(Cms::config()->tokenParam, $request->header(HandleTokenRequest::TOKEN_HEADER));
};
}

public function isSiteRequest(): Closure
{
return function (): bool {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

return ! $request->isCpRequest();
};
}

public function isActionRequest(): Closure
{
return function (): bool {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

return $request->actionSegments() !== [];
};
}

public function isPreview(): Closure
{
return function (): bool {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;
$previewParamValue = $request->input('x-craft-preview') ?? $request->input('x-craft-live-preview') ?? $request->header('X-Craft-Preview-Token');

if ($previewParamValue === null || $previewParamValue === '') {
return false;
}

try {
Crypt::decrypt($previewParamValue);
} catch (DecryptException) {
return false;
}

return Context::hasHidden(HandleTokenRequest::TOKEN_KEY);
};
}

public function actionSegments(): Closure
{
return function (): array {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;
$actionTrigger = Cms::config()->actionTrigger;
$segmentIndex = $request->isCpRequest() ? 2 : 1;

if ($request->segment($segmentIndex) === $actionTrigger && count($request->segments()) > $segmentIndex) {
return array_slice($request->segments(), $segmentIndex);
}

$actionParam = $request->get('action');

if ($actionParam !== null) {
if (! is_string($actionParam)) {
abort(400, 'Invalid action param');
}

return array_values(array_filter(explode('/', $actionParam)));
}

return [];
};
}

public function actionSegmentsToRoute(): Closure
{
return function (?array $actionSegments = null): string {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

$actionSegments ??= $request->actionSegments();

return implode('/', array_filter([
'',
$request->isCpRequest() ? Cms::config()->cpTrigger : null,
Cms::config()->actionTrigger,
...$actionSegments,
], fn ($value) => $value !== null));
};
}

public function pageNumber(): Closure
{
return function (): int {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;
$pageTrigger = $request->isCpRequest() ? 'p' : Cms::config()->getPageTrigger();

if (str_starts_with($pageTrigger, '?')) {
return max(1, (int) $request->query(trim($pageTrigger, '?='), '1'));
}

$path = trim($request->decodedPath(), '/');

if ($path === '') {
return 1;
}

$pageTriggerPattern = preg_quote($pageTrigger, '/');

if (preg_match("/^(?:(.*)\\/)?{$pageTriggerPattern}(\\d+)$/", $path, $matches)) {
return max(1, (int) $matches[2]);
}

return 1;
};
}

public function duplicateWithUri(): Closure
{
return function (string $newUri, ?array $query = null, array $server = []): Request {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;

return $request->duplicate(
query: $query ?? $request->query->all(),
server: array_merge($request->server->all(), $server, [
'REQUEST_URI' => $newUri,
]),
);
};
}

public function getSigned(): Closure
{
return function (string $key, mixed $default = null): mixed {
/**
* @var Request $request
*
* @phpstan-ignore-next-line
*/
$request = $this;
$value = $request->get($key);

if ($value === null) {
return $default;
}

try {
return Crypt::decrypt($value);
} catch (DecryptException) {
abort(400, 'Request contained an invalid body param');
}
};
}
}
Loading