From d23d625c646020d95d2352cfa9f0dfcec6606902 Mon Sep 17 00:00:00 2001 From: Niklan Date: Tue, 21 Apr 2026 21:16:17 +0500 Subject: [PATCH 1/2] refactor(image): use native private path and PrivateKey for dynamic styles --- app/modules/image/app_image.deploy.php | 25 ++++++ app/modules/image/app_image.install | 24 ------ .../DynamicImageStyle/DynamicImageStyle.php | 78 +++++++++---------- .../image/src/Hook/Deploy/Deploy0001.php | 28 +++++++ .../image/src/Hook/Deploy/Deploy0002.php | 40 ++++++++++ .../DynamicImageStylePathProcessor.php | 6 +- .../image/src/Routing/RouteProvider.php | 2 +- 7 files changed, 133 insertions(+), 70 deletions(-) create mode 100644 app/modules/image/app_image.deploy.php delete mode 100644 app/modules/image/app_image.install create mode 100644 app/modules/image/src/Hook/Deploy/Deploy0001.php create mode 100644 app/modules/image/src/Hook/Deploy/Deploy0002.php diff --git a/app/modules/image/app_image.deploy.php b/app/modules/image/app_image.deploy.php new file mode 100644 index 00000000..ba029b93 --- /dev/null +++ b/app/modules/image/app_image.deploy.php @@ -0,0 +1,25 @@ +set('app_image.dynamic_image_style_key', Crypt::randomBytesBase64()); -} - -/** - * Implements hook_uninstall(). - */ -function app_image_uninstall(): void { - \Drupal::state()->delete('app_image.dynamic_image_style_key'); -} diff --git a/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php index cd1b3514..82f0e27f 100644 --- a/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php +++ b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php @@ -7,8 +7,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Site\Settings; -use Drupal\Core\State\StateInterface; +use Drupal\Core\PrivateKey; use Drupal\Core\StreamWrapper\LocalStream; use Drupal\Core\StreamWrapper\StreamWrapperManager; use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; @@ -34,7 +33,7 @@ public function __construct( private EntityTypeManagerInterface $entityTypeManager, private StreamWrapperManagerInterface $streamWrapperManager, - private StateInterface $state, + private PrivateKey $privateKey, ) {} public function effect(string $id, array $data = []): DynamicImageStyleBuilder { @@ -45,52 +44,19 @@ public function effect(string $id, array $data = []): DynamicImageStyleBuilder { * @param list}> $effects */ public function buildUrl(string $uri, array $effects): string { - $scheme = StreamWrapperManager::getScheme($uri); - $target = StreamWrapperManager::getTarget($uri); - Assert::string($scheme); - Assert::string($target); - $compressed = $this->compressEffects($effects); + [$scheme, $target, $compressed, $hash] = $this->resolveDerivativePath($uri, $effects); $itok = $this->generateToken($compressed, $uri); - $hash = Crypt::hashBase64($compressed); - - $image_style = $this->createImageStyle($effects); - $original_extension = \pathinfo($target, \PATHINFO_EXTENSION); - $derivative_extension = $image_style->getDerivativeExtension($original_extension); - - $path = $target; - if ($original_extension !== $derivative_extension) { - $path .= '.' . $derivative_extension; - } - + $encoded_effects = \urlencode($compressed); $base_path = $this->getBaseUrlPath($scheme); - - return '/' . $base_path . '/styles/dynamic/' . $hash . '/' . $scheme . '/' . $path - . '?effects=' . \urlencode($compressed) - . '&itok=' . $itok; + return "/$base_path/styles/dynamic/$hash/$scheme/$target?effects=$encoded_effects&itok=$itok"; } /** * @param list}> $effects */ public function buildUri(string $uri, array $effects): string { - $scheme = StreamWrapperManager::getScheme($uri); - $target = StreamWrapperManager::getTarget($uri); - Assert::string($scheme); - Assert::string($target); - - $compressed = $this->compressEffects($effects); - $hash = Crypt::hashBase64($compressed); - - $image_style = $this->createImageStyle($effects); - $original_extension = \pathinfo($target, \PATHINFO_EXTENSION); - $derivative_extension = $image_style->getDerivativeExtension($original_extension); - - $path = $target; - if ($original_extension !== $derivative_extension) { - $path .= '.' . $derivative_extension; - } - - return "$scheme://styles/dynamic/$hash/$scheme/$path"; + [$scheme, $target, , $hash] = $this->resolveDerivativePath($uri, $effects); + return "$scheme://styles/dynamic/$hash/$scheme/$target"; } /** @@ -121,8 +87,34 @@ public function decompressEffects(string $compressed): array { } public function generateToken(string $compressed, string $uri): string { - $key = $this->state->get('app_image.dynamic_image_style_key', Settings::getHashSalt()); - return \substr(Crypt::hmacBase64($compressed . ':' . $uri, $key), 0, 8); + return \substr(Crypt::hmacBase64($compressed . ':' . $uri, $this->privateKey->get()), 0, 8); + } + + private function hashEffects(string $compressed): string { + return \substr(Crypt::hashBase64($compressed), 0, 8); + } + + /** + * @param list}> $effects + * @return array{string, string, string, string} + */ + private function resolveDerivativePath(string $uri, array $effects): array { + $scheme = StreamWrapperManager::getScheme($uri); + $target = StreamWrapperManager::getTarget($uri); + Assert::string($scheme); + Assert::string($target); + + $image_style = $this->createImageStyle($effects); + $original_extension = \pathinfo($target, \PATHINFO_EXTENSION); + $derivative_extension = $image_style->getDerivativeExtension($original_extension); + if ($original_extension !== $derivative_extension) { + $target .= '.' . $derivative_extension; + } + + $compressed = $this->compressEffects($effects); + $hash = $this->hashEffects($compressed); + + return [$scheme, $target, $compressed, $hash]; } /** diff --git a/app/modules/image/src/Hook/Deploy/Deploy0001.php b/app/modules/image/src/Hook/Deploy/Deploy0001.php new file mode 100644 index 00000000..4fdf1298 --- /dev/null +++ b/app/modules/image/src/Hook/Deploy/Deploy0001.php @@ -0,0 +1,28 @@ +get(StateInterface::class), + ); + } + + public function __construct( + private StateInterface $state, + ) {} + + public function __invoke(): string { + $this->state->delete('app_image.dynamic_image_style_key'); + return 'Deleted app_image.dynamic_image_style_key from state.'; + } + +} diff --git a/app/modules/image/src/Hook/Deploy/Deploy0002.php b/app/modules/image/src/Hook/Deploy/Deploy0002.php new file mode 100644 index 00000000..dd895bd1 --- /dev/null +++ b/app/modules/image/src/Hook/Deploy/Deploy0002.php @@ -0,0 +1,40 @@ +get('file_system'), + ); + } + + public function __construct( + private FileSystemInterface $fileSystem, + ) {} + + public function __invoke(): string { + $deleted = []; + foreach (['public', 'private'] as $scheme) { + $dir = $scheme . '://styles/dynamic'; + if (\is_dir($dir)) { + $this->fileSystem->deleteRecursive($dir); + $deleted[] = $scheme; + } + } + + if ($deleted === []) { + return 'No dynamic style directories found.'; + } + + return \sprintf('Deleted styles/dynamic in: %s.', \implode(', ', $deleted)); + } + +} diff --git a/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php b/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php index 6943c33d..3a8ccdd9 100644 --- a/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php +++ b/app/modules/image/src/PathProcessor/DynamicImageStylePathProcessor.php @@ -40,10 +40,12 @@ public function processInbound($path, Request $request): string { } // Private files: /system/files/styles/dynamic/... - // Rewrite to internal path that won't be caught by PathProcessorFiles. + // Set 'file' query param to block PathProcessorFiles (it skips processing + // when 'file' is already present), then rewrite to the registered route. $private_prefix = '/system/files/styles/dynamic/'; if (\str_starts_with($path, $private_prefix)) { - return '/app-image/dynamic-private'; + $request->query->set('file', 'dynamic'); + return '/system/files/styles/dynamic'; } return $path; diff --git a/app/modules/image/src/Routing/RouteProvider.php b/app/modules/image/src/Routing/RouteProvider.php index d151634d..7a344911 100644 --- a/app/modules/image/src/Routing/RouteProvider.php +++ b/app/modules/image/src/Routing/RouteProvider.php @@ -37,7 +37,7 @@ public function __invoke(): RouteCollection { )); $routes->add('app_image.dynamic_image_style_private', new Route( - path: '/app-image/dynamic-private', + path: '/system/files/styles/dynamic', defaults: [ '_controller' => DynamicImageStyleController::class, '_disable_route_normalizer' => TRUE, From 0c6d40e59c0e45d0cfde6e0686391af404a180f5 Mon Sep 17 00:00:00 2001 From: Niklan Date: Tue, 21 Apr 2026 21:23:05 +0500 Subject: [PATCH 2/2] refactor(image): use native private path and PrivateKey for dynamic styles --- .../DynamicImageStyle/DynamicImageStyle.php | 30 +++++++++---------- .../image/src/Hook/Deploy/Deploy0002.php | 8 +++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php index 82f0e27f..33f72116 100644 --- a/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php +++ b/app/modules/image/src/DynamicImageStyle/DynamicImageStyle.php @@ -90,6 +90,21 @@ public function generateToken(string $compressed, string $uri): string { return \substr(Crypt::hmacBase64($compressed . ':' . $uri, $this->privateKey->get()), 0, 8); } + /** + * @param list}> $effects + */ + public function createImageStyle(array $effects): ImageStyleInterface { + $storage = $this->entityTypeManager->getStorage('image_style'); + $image_style = $storage->create(['name' => 'dynamic']); + Assert::isInstanceOf($image_style, ImageStyleInterface::class); + + foreach ($effects as [$id, $data]) { + $image_style->addImageEffect(['id' => $id, 'data' => $data]); + } + + return $image_style; + } + private function hashEffects(string $compressed): string { return \substr(Crypt::hashBase64($compressed), 0, 8); } @@ -117,21 +132,6 @@ private function resolveDerivativePath(string $uri, array $effects): array { return [$scheme, $target, $compressed, $hash]; } - /** - * @param list}> $effects - */ - public function createImageStyle(array $effects): ImageStyleInterface { - $storage = $this->entityTypeManager->getStorage('image_style'); - $image_style = $storage->create(['name' => 'dynamic']); - Assert::isInstanceOf($image_style, ImageStyleInterface::class); - - foreach ($effects as [$id, $data]) { - $image_style->addImageEffect(['id' => $id, 'data' => $data]); - } - - return $image_style; - } - /** * Returns the base URL path for the given stream wrapper scheme. * diff --git a/app/modules/image/src/Hook/Deploy/Deploy0002.php b/app/modules/image/src/Hook/Deploy/Deploy0002.php index dd895bd1..99d7acf6 100644 --- a/app/modules/image/src/Hook/Deploy/Deploy0002.php +++ b/app/modules/image/src/Hook/Deploy/Deploy0002.php @@ -24,10 +24,12 @@ public function __invoke(): string { $deleted = []; foreach (['public', 'private'] as $scheme) { $dir = $scheme . '://styles/dynamic'; - if (\is_dir($dir)) { - $this->fileSystem->deleteRecursive($dir); - $deleted[] = $scheme; + if (!\is_dir($dir)) { + continue; } + + $this->fileSystem->deleteRecursive($dir); + $deleted[] = $scheme; } if ($deleted === []) {