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..33f72116 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,7 @@ 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); } /** @@ -140,6 +105,33 @@ public function createImageStyle(array $effects): ImageStyleInterface { return $image_style; } + 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]; + } + /** * Returns the base URL path for the given stream wrapper scheme. * 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..99d7acf6 --- /dev/null +++ b/app/modules/image/src/Hook/Deploy/Deploy0002.php @@ -0,0 +1,42 @@ +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)) { + continue; + } + + $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,