diff --git a/README.md b/README.md index c5ddb90..d8b4c2d 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,14 @@ extensions: user: '^user_avatar\/' # the noimage "user" will be used for missing files with paths that matches this regex presets: my_preset: - w: 150 - ar: '2x1.5' + modifiers: + w: 150 + ar: '2x1.5' + my_preset_2: + modifiers: + ar: 1x2 + w: [300, 600, 900] + defaultW: 600 ``` ### Animated GIFs diff --git a/src/Bridge/Nette/DI/Config/PresetConfig.php b/src/Bridge/Nette/DI/Config/PresetConfig.php new file mode 100644 index 0000000..0a28e49 --- /dev/null +++ b/src/Bridge/Nette/DI/Config/PresetConfig.php @@ -0,0 +1,21 @@ + */ + public array $modifiers = []; + + /** @var array */ + public array $w = []; + + /** @var list */ + public array $x = []; + + public int|null $defaultW = null; + + public int|float|null $defaultX = null; +} diff --git a/src/Bridge/Nette/DI/Config/StorageConfig.php b/src/Bridge/Nette/DI/Config/StorageConfig.php index fceea0a..3faec25 100644 --- a/src/Bridge/Nette/DI/Config/StorageConfig.php +++ b/src/Bridge/Nette/DI/Config/StorageConfig.php @@ -21,7 +21,7 @@ final class StorageConfig /** @var array */ public array $no_image_patterns; - /** @var array> */ + /** @var array */ public array $presets; /** @var array */ diff --git a/src/Bridge/Nette/DI/ImageStorageExtension.php b/src/Bridge/Nette/DI/ImageStorageExtension.php index d58965b..10dad92 100644 --- a/src/Bridge/Nette/DI/ImageStorageExtension.php +++ b/src/Bridge/Nette/DI/ImageStorageExtension.php @@ -31,6 +31,7 @@ use SixtyEightPublishers\ImageStorage\Bridge\Nette\Application\ImageServerPresenter; use SixtyEightPublishers\ImageStorage\Bridge\Nette\Application\ImageServerRoute; use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\ImageStorageConfig; +use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\PresetConfig; use SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config\StorageConfig; use SixtyEightPublishers\ImageStorage\Bridge\Nette\ImageServer\ResponseFactory; use SixtyEightPublishers\ImageStorage\Bridge\Symfony\Console\Configurator\CleanCommandConfigurator; @@ -52,11 +53,14 @@ use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface; use SixtyEightPublishers\ImageStorage\Modifier; use SixtyEightPublishers\ImageStorage\Modifier\Applicator; +use SixtyEightPublishers\ImageStorage\Modifier\Codec\Codec; +use SixtyEightPublishers\ImageStorage\Modifier\Codec\PresetCodec; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollection; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionFactoryInterface; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeFactory; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeFactoryInterface; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollection; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionFactoryInterface; use SixtyEightPublishers\ImageStorage\Modifier\Validator; @@ -65,13 +69,22 @@ use SixtyEightPublishers\ImageStorage\Persistence\ImagePersister; use SixtyEightPublishers\ImageStorage\Persistence\ImagePersisterInterface; use SixtyEightPublishers\ImageStorage\Resource\ResourceFactory; +use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\WDescriptor; +use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\XDescriptor; use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGeneratorFactoryInterface; +use SixtyEightPublishers\ImageStorage\Security\KnownModifiers; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategy; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; use function array_diff; +use function array_fill_keys; use function array_keys; +use function array_map; +use function array_merge; +use function array_unique; use function assert; use function is_array; +use function is_string; +use function is_subclass_of; use function sprintf; final class ImageStorageExtension extends CompilerExtension implements FileStorageDefinitionFactoryInterface @@ -120,7 +133,17 @@ public function getConfigSchema(): Schema 'no_image_patterns' => Expect::arrayOf('string', 'string') ->default([]), 'presets' => Expect::arrayOf( - Expect::arrayOf(Expect::scalar(), 'string'), + Expect::structure([ + 'modifiers' => Expect::arrayOf(Expect::scalar(), 'string'), + 'w' => Expect::listOf(Expect::int())->default([]), + 'x' => Expect::listOf(Expect::anyOf(Expect::int(), Expect::float()))->default([]), + 'defaultW' => Expect::int()->nullable(), + 'defaultX' => Expect::anyOf(Expect::int(), Expect::float())->nullable(), + ])->castTo(PresetConfig::class) + ->assert( + handler: static fn (PresetConfig $preset): bool => !([] !== $preset->w && [] !== $preset->x), + description: 'A preset cannot have both "w" and "x" properties.', + ), 'string', )->default([]), @@ -329,13 +352,30 @@ public function createFileStorage(string $name, FileStorageConfig $config): Serv ->setFactory(Config::class, [$config->config]) ->setAutowired(false); + $presets = array_map( + callback: static fn (PresetConfig $preset): Statement => new Statement(Preset::class, [ + 'modifiers' => $preset->modifiers, + 'descriptor' => match (true) { + [] !== $preset->w => new Statement(WDescriptor::class, $preset->w), + [] !== $preset->x => new Statement(XDescriptor::class, $preset->x), + default => null, + }, + 'defaultDescriptorValue' => match (true) { + [] !== $preset->w => $preset->defaultW, + [] !== $preset->x => $preset->defaultX, + default => null, + }, + ]), + array: $imageStorageConfig->presets, + ); + $builder->addDefinition($this->prefix('modifier_facade.' . $name)) ->setType(ModifierFacadeInterface::class) ->setFactory(new Statement([$this->prefix('@modifiers.modifier_facade_factory'), 'create'], [ new Reference($this->prefix('config.' . $name)), ])) ->addSetup('setModifiers', [$imageStorageConfig->modifiers]) - ->addSetup('setPresets', [$imageStorageConfig->presets]) + ->addSetup('setPresets', [$presets]) ->addSetup('setApplicators', [$imageStorageConfig->applicators]) ->addSetup('setValidators', [$imageStorageConfig->validators]) ->setAutowired(false); @@ -354,7 +394,13 @@ public function createFileStorage(string $name, FileStorageConfig $config): Serv $signatureStrategyDefinition = $builder->addDefinition($this->prefix('signature_strategy.' . $name)) ->setType(SignatureStrategyInterface::class) ->setFactory(SignatureStrategy::class, [ - new Reference($this->prefix('config.' . $name)), + 'config' => new Reference($this->prefix('config.' . $name)), + 'knownModifiers' => new Statement(KnownModifiers::class, [ + 'list' => $this->buildKnownModifiers( + config: $config, + extConfig: $imageStorageConfig, + ), + ]), ]) ->setAutowired(false); } @@ -506,4 +552,92 @@ private function registerImageServerPresenter(): void $this->imageServerPresenterRegistered = true; } + + /** + * @return array + */ + private function buildKnownModifiers(FileStorageConfig $config, StorageConfig $extConfig): array + { + $presets = array_map( + callback: static function (PresetConfig $conf): Preset { + $descriptor = match (true) { + [] !== $conf->w => new WDescriptor(...$conf->w), + [] !== $conf->x => new XDescriptor(...$conf->x), + default => null, + }; + + $defaultDescriptorValue = match (true) { + [] !== $conf->w => $conf->defaultW, + [] !== $conf->x => $conf->defaultX, + default => null, + }; + + return new Preset( + modifiers: $conf->modifiers, + descriptor: $descriptor, + defaultDescriptorValue: $defaultDescriptorValue, + ); + }, + array: $extConfig->presets, + ); + + $modifiers = array_map( + callback: static function (Statement $modifier): Modifier\ModifierInterface { + $entity = $modifier->getEntity(); + $params = $modifier->arguments; + assert(is_string($entity) && is_subclass_of($entity, Modifier\AbstractModifier::class) && is_array($params)); + + return new $entity(...$params); + }, + array: $extConfig->modifiers, + ); + + $presetsCollection = new PresetCollection(); + + foreach ($presets as $name => $preset) { + $presetsCollection->add(presetAlias: $name, preset: $preset); + } + + $modifierCollection = new ModifierCollection(); + + foreach ($modifiers as $modifier) { + $modifierCollection->add(modifier: $modifier); + } + + $cnf = new Config($config->config); + + $codec = new PresetCodec( + codec: new Codec( + config: $cnf, + modifierCollection: $modifierCollection, + ), + config: $cnf, + modifierCollection: $modifierCollection, + presetCollection: $presetsCollection, + ); + + $known = []; + + foreach ($presets as $preset) { + if (null === $preset->descriptor) { + $known[] = $codec->modifiersToPath($preset->modifiers); + + continue; + } + + $modifiers = $preset->modifiers; + + foreach ($preset->descriptor->iterateModifiers($modifierCollection) as $mod) { + $known[] = $codec->modifiersToPath(array_merge( + $modifiers, + $mod, + )); + } + } + + return array_fill_keys( + keys: array_unique($known), + value: true, + ); + } } diff --git a/src/Config/Config.php b/src/Config/Config.php index 39e5b03..72b4338 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -11,6 +11,7 @@ final class Config extends FileStorageConfig public const SIGNATURE_PARAMETER_NAME = 'signature_parameter_name'; public const SIGNATURE_KEY = 'signature_key'; public const SIGNATURE_ALGORITHM = 'signature_algorithm'; + public const DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS = 'disable_signature_on_known_modifiers'; public const MODIFIER_SEPARATOR = 'modifier_separator'; public const MODIFIER_ASSIGNER = 'modifier_assigner'; public const ALLOWED_PIXEL_DENSITY = 'allowed_pixel_density'; @@ -28,6 +29,7 @@ final class Config extends FileStorageConfig self::SIGNATURE_PARAMETER_NAME => '_s', self::SIGNATURE_KEY => null, self::SIGNATURE_ALGORITHM => 'sha256', + self::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS => false, self::ALLOWED_PIXEL_DENSITY => [], self::ALLOWED_RESOLUTIONS => [], self::ALLOWED_QUALITIES => [], diff --git a/src/FileInfo.php b/src/FileInfo.php index a191921..bff09cc 100644 --- a/src/FileInfo.php +++ b/src/FileInfo.php @@ -20,7 +20,7 @@ public function __construct(ImageLinkGeneratorInterface $linkGenerator, PathInfo parent::__construct($linkGenerator, $pathInfo, $imageStorageName); } - public function srcSet(DescriptorInterface $descriptor, bool $absolute = true): SrcSet + public function srcSet(?DescriptorInterface $descriptor = null, bool $absolute = true): SrcSet { assert($this->linkGenerator instanceof ImageLinkGeneratorInterface); diff --git a/src/FileInfoInterface.php b/src/FileInfoInterface.php index b9d9f93..dda593d 100644 --- a/src/FileInfoInterface.php +++ b/src/FileInfoInterface.php @@ -10,5 +10,5 @@ interface FileInfoInterface extends BaseFileInfoInterface, PathInfoInterface { - public function srcSet(DescriptorInterface $descriptor, bool $absolute = true): SrcSet; + public function srcSet(?DescriptorInterface $descriptor = null, bool $absolute = true): SrcSet; } diff --git a/src/ImageServer/LocalImageServer.php b/src/ImageServer/LocalImageServer.php index 00717bd..8430420 100644 --- a/src/ImageServer/LocalImageServer.php +++ b/src/ImageServer/LocalImageServer.php @@ -148,12 +148,7 @@ private function validateSignature(RequestInterface $request, string $path): voi $signatureParameterName = $this->imageStorage->getConfig()[Config::SIGNATURE_PARAMETER_NAME]; assert(is_string($signatureParameterName)); - $token = $request->getQueryParameter($signatureParameterName) ?? ''; - assert(is_string($token)); - - if (empty($token)) { - throw new SignatureException('Missing signature in request.'); - } + $token = (string) ($request->getQueryParameter($signatureParameterName) ?? ''); # @phpstan-ignore-line if (!$signatureStrategy->verifyToken($token, $path)) { throw new SignatureException('Request contains invalid signature.'); diff --git a/src/ImageStorage.php b/src/ImageStorage.php index b2406bf..f54ea4e 100644 --- a/src/ImageStorage.php +++ b/src/ImageStorage.php @@ -90,7 +90,7 @@ public function resolveNoImage(string $path): ImagePathInfoInterface return $this->noImageResolver->resolveNoImage($path); } - public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor, bool $absolute = true): SrcSet + public function srcSet(ImagePathInfoInterface $info, ?DescriptorInterface $descriptor = null, bool $absolute = true): SrcSet { assert($this->linkGenerator instanceof ImageLinkGeneratorInterface); diff --git a/src/LinkGenerator/LinkGenerator.php b/src/LinkGenerator/LinkGenerator.php index 4e1df0a..fbf82df 100644 --- a/src/LinkGenerator/LinkGenerator.php +++ b/src/LinkGenerator/LinkGenerator.php @@ -17,6 +17,7 @@ use SixtyEightPublishers\ImageStorage\Responsive\SrcSetGeneratorFactoryInterface; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategyInterface; use function assert; +use function explode; use function is_string; use function sprintf; @@ -36,14 +37,16 @@ public function __construct( public function link(FilePathInfoInterface $pathInfo, bool $absolute = true): string { if (!$pathInfo instanceof ImagePathInfoInterface) { - throw new InvalidArgumentException(sprintf( - 'Path info passed into the method %s() must be an instance of %s.', - __METHOD__, - ImagePathInfoInterface::class, - )); + throw new InvalidArgumentException( + message: sprintf( + 'Path info passed into the method %s() must be an instance of %s.', + __METHOD__, + ImagePathInfoInterface::class, + ), + ); } - if (null === $pathInfo->getModifiers()) { + if (null === $pathInfo->getModifiers() || [] === $pathInfo->getModifiers()) { $pathInfo = $pathInfo->withModifiers(['original' => true]); } @@ -53,8 +56,14 @@ public function link(FilePathInfoInterface $pathInfo, bool $absolute = true): st ); } - public function srcSet(ImagePathInfoInterface $info, DescriptorInterface $descriptor, bool $absolute = true): SrcSet + public function srcSet(ImagePathInfoInterface $info, ?DescriptorInterface $descriptor = null, bool $absolute = true): SrcSet { + if (null === $descriptor) { + $descriptor = $this->resolveDescriptor( + pathInfo: $info, + ); + } + if (null === $this->srcSetGenerator) { $this->srcSetGenerator = $this->srcSetGeneratorFactory->create($this, $this->modifierFacade); } @@ -79,9 +88,38 @@ protected function buildQueryParams(FilePathInfoInterface $pathInfo): array $signatureParameterName = $this->config[Config::SIGNATURE_PARAMETER_NAME]; assert(is_string($signatureParameterName)); - $params[$signatureParameterName] = $this->signatureStrategy->createToken($pathInfo->getPath()); + $token = $this->signatureStrategy->createToken($pathInfo->getPath()); + + if (null !== $token) { + $params[$signatureParameterName] = $token; + } } return $params; } + + private function resolveDescriptor(ImagePathInfoInterface $pathInfo): DescriptorInterface + { + $modifiers = $pathInfo->getModifiers(); + + if (is_string($modifiers)) { + $presets = $this->modifierFacade->getPresetCollection(); + $assigner = $this->config[Config::MODIFIER_ASSIGNER]; + $assigner = empty($assigner) ? ':' : $assigner; + [$presetAlias] = explode($assigner, $modifiers, 2); + $preset = $presets->get(presetAlias: $presetAlias); + + if (null !== $preset->descriptor) { + return $preset->descriptor; + } + } + + throw new InvalidArgumentException( + message: sprintf( + 'Unable to resolve descriptor for path info %s. Descriptor must be provided to the method %s::srcSet() manually.', + $pathInfo, + __CLASS__, + ), + ); + } } diff --git a/src/LinkGenerator/LinkGeneratorInterface.php b/src/LinkGenerator/LinkGeneratorInterface.php index badedbe..88583df 100644 --- a/src/LinkGenerator/LinkGeneratorInterface.php +++ b/src/LinkGenerator/LinkGeneratorInterface.php @@ -12,7 +12,7 @@ interface LinkGeneratorInterface extends BaseLinkGeneratorInterface { - public function srcSet(PathInfoInterface $info, DescriptorInterface $descriptor, bool $absolute = true): SrcSet; + public function srcSet(PathInfoInterface $info, ?DescriptorInterface $descriptor = null, bool $absolute = true): SrcSet; public function getSignatureStrategy(): ?SignatureStrategyInterface; } diff --git a/src/Modifier/AbstractModifier.php b/src/Modifier/AbstractModifier.php index 36dcd5f..423f425 100644 --- a/src/Modifier/AbstractModifier.php +++ b/src/Modifier/AbstractModifier.php @@ -11,7 +11,7 @@ abstract class AbstractModifier implements ModifierInterface { protected ?string $alias = null; - public function __construct(?string $alias = null) + final public function __construct(?string $alias = null) { if (null !== $alias) { $this->alias = $alias; diff --git a/src/Modifier/Codec/Codec.php b/src/Modifier/Codec/Codec.php index 97a9954..aa05d4e 100644 --- a/src/Modifier/Codec/Codec.php +++ b/src/Modifier/Codec/Codec.php @@ -7,16 +7,14 @@ use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\ImageStorage\Config\Config; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\ValueInterface; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\ParsableModifierInterface; -use Stringable; use function assert; use function count; use function explode; -use function gettype; use function implode; use function is_array; +use function is_string; use function ksort; use function sprintf; @@ -27,30 +25,30 @@ public function __construct( private readonly ModifierCollectionInterface $modifierCollection, ) {} - public function encode(ValueInterface $value): string + public function modifiersToPath(string|array $value): string { - $parameters = $value->getValue(); - - if (!is_array($parameters)) { - throw new InvalidArgumentException(sprintf( - 'Can not decode value of type %s, the value must be array.', - gettype($parameters), - )); + if (!is_array($value)) { + throw new InvalidArgumentException( + message: 'Can not transform value of type string, the value must be array.', + ); } - if (empty($parameters)) { + if (empty($value)) { throw new InvalidArgumentException('Value can not be an empty array.'); } - /** @var array $parameters */ $assigner = $this->config[Config::MODIFIER_ASSIGNER]; $separator = $this->config[Config::MODIFIER_SEPARATOR]; + + $assigner = empty($assigner) ? ':' : $assigner; + $separator = empty($separator) ? ',' : $separator; + $result = []; - assert(\is_string($assigner) && \is_string($separator)); + assert(is_string($assigner) && is_string($separator)); - ksort($parameters); + ksort($value); - foreach ($parameters as $k => $v) { + foreach ($value as $k => $v) { $modifier = $this->modifierCollection->getByAlias($k); if (!$modifier instanceof ParsableModifierInterface) { @@ -67,39 +65,28 @@ public function encode(ValueInterface $value): string return implode($separator, $result); } - public function decode(ValueInterface $value): array + public function pathToModifiers(string $value): array { - $path = $value->getValue(); - - if (!is_string($path) && !$path instanceof Stringable) { - throw new InvalidArgumentException(sprintf( - 'Can not decode value of type %s, the value must be string or Stringable object.', - gettype($path), - )); - } - - $path = (string) $path; - - if (empty($path)) { + if (empty($value)) { throw new InvalidArgumentException('Value can not be an empty string.'); } $parameters = []; $assigner = $this->config[Config::MODIFIER_ASSIGNER]; $separator = $this->config[Config::MODIFIER_SEPARATOR]; - assert(\is_string($assigner) && \is_string($separator)); + assert(is_string($assigner) && is_string($separator)); $assigner = empty($assigner) ? ':' : $assigner; $separator = empty($separator) ? ',' : $separator; - foreach (explode($separator, $path) as $modifier) { + foreach (explode($separator, $value) as $modifier) { $modifier = explode($assigner, $modifier); $count = count($modifier); if (2 < $count) { throw new InvalidArgumentException(sprintf( 'An invalid path "%s" passed, the modifier "%s" has an invalid format.', - $path, + $value, implode($assigner, $modifier), )); } @@ -109,7 +96,7 @@ public function decode(ValueInterface $value): array if (1 === $count && $modifierObject instanceof ParsableModifierInterface) { throw new InvalidArgumentException(sprintf( 'An invalid path "%s" passed, the modifier "%s" must have a value.', - $path, + $value, $modifierObject->getAlias(), )); } @@ -117,7 +104,7 @@ public function decode(ValueInterface $value): array if (2 === $count && !$modifierObject instanceof ParsableModifierInterface) { throw new InvalidArgumentException(sprintf( 'An invalid path "%s" passed, the modifier "%s" can not have a value.', - $path, + $value, $modifierObject->getAlias(), )); } @@ -127,4 +114,15 @@ public function decode(ValueInterface $value): array return $parameters; } + + public function expandModifiers(array|string $value): array + { + if (!is_array($value)) { + throw new InvalidArgumentException( + message: 'Can not expand value of type string, the value must be array.', + ); + } + + return $value; + } } diff --git a/src/Modifier/Codec/CodecInterface.php b/src/Modifier/Codec/CodecInterface.php index a785428..bbd09f4 100644 --- a/src/Modifier/Codec/CodecInterface.php +++ b/src/Modifier/Codec/CodecInterface.php @@ -4,14 +4,22 @@ namespace SixtyEightPublishers\ImageStorage\Modifier\Codec; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\ValueInterface; - interface CodecInterface { - public function encode(ValueInterface $value): string; + /** + * @param string|array $value + */ + public function modifiersToPath(string|array $value): string; + + /** + * @return array + */ + public function pathToModifiers(string $value): array; /** + * @param string|array $value + * * @return array */ - public function decode(ValueInterface $value): array; + public function expandModifiers(string|array $value): array; } diff --git a/src/Modifier/Codec/PresetCodec.php b/src/Modifier/Codec/PresetCodec.php index 6116bf6..7304cb8 100644 --- a/src/Modifier/Codec/PresetCodec.php +++ b/src/Modifier/Codec/PresetCodec.php @@ -4,33 +4,76 @@ namespace SixtyEightPublishers\ImageStorage\Modifier\Codec; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\ValueInterface; +use SixtyEightPublishers\FileStorage\Config\ConfigInterface; +use SixtyEightPublishers\ImageStorage\Config\Config; +use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; +use function array_merge; +use function explode; +use function is_string; +use function str_contains; final class PresetCodec implements CodecInterface { public function __construct( private readonly CodecInterface $codec, + private readonly ConfigInterface $config, + private readonly ModifierCollectionInterface $modifierCollection, private readonly PresetCollectionInterface $presetCollection, ) {} - public function encode(ValueInterface $value): string + public function modifiersToPath(string|array $value): string { - if ($value instanceof PresetValue) { - $value = new Value($this->presetCollection->get($value->presetName)); + if (is_string($value)) { + $value = $this->doExpand(value: $value); } - return $this->codec->encode($value); + return $this->codec->modifiersToPath(value: $value); } - public function decode(ValueInterface $value): array + public function pathToModifiers(string $value): array { - if ($value instanceof PresetValue) { - return $this->presetCollection->get($value->presetName); + return $this->codec->pathToModifiers(value: $value); + } + + public function expandModifiers(array|string $value): array + { + if (is_string($value)) { + $value = $this->doExpand(value: $value); + } + + return $this->codec->expandModifiers(value: $value); + } + + /** + * @return array + */ + private function doExpand(string $value): array + { + $presetAlias = $value; + $presetValue = true; + $assigner = $this->config[Config::MODIFIER_ASSIGNER]; + $assigner = empty($assigner) ? ':' : $assigner; + + if (str_contains($presetAlias, $assigner)) { + [$presetAlias, $presetValue] = explode($assigner, $presetAlias, 2); + } + + $preset = $this->presetCollection->get(presetAlias: $presetAlias); + $modifiers[] = $preset->modifiers; + + if (null !== $preset->descriptor) { + $presetValue = $preset->descriptor->validateModifierValue( + value: $presetValue, + default: $preset->defaultDescriptorValue, + ); + + $modifiers[] = $preset->descriptor->expandModifier( + modifierCollection: $this->modifierCollection, + value: $presetValue, + ); } - return $this->codec->decode($value); + return array_merge(...$modifiers); } } diff --git a/src/Modifier/Codec/RuntimeCachedCodec.php b/src/Modifier/Codec/RuntimeCachedCodec.php index 09a0ca4..911a80b 100644 --- a/src/Modifier/Codec/RuntimeCachedCodec.php +++ b/src/Modifier/Codec/RuntimeCachedCodec.php @@ -5,17 +5,22 @@ namespace SixtyEightPublishers\ImageStorage\Modifier\Codec; use JsonException; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\ValueInterface; use function json_encode; -use function md5; final class RuntimeCachedCodec implements CodecInterface { - /** @var array */ - private array $encodeCache = []; - - /** @var array> */ - private array $decodeCache = []; + /** + * @var array{ + * modifiersToPath: array, + * pathToModifiers: array>, + * expandModifiers: array>, + * } + */ + private array $cache = [ + 'modifiersToPath' => [], + 'pathToModifiers' => [], + 'expandModifiers' => [], + ]; public function __construct( private readonly CodecInterface $codec, @@ -24,28 +29,25 @@ public function __construct( /** * @throws JsonException */ - public function encode(ValueInterface $value): string + public function modifiersToPath(string|array $value): string { - $key = $this->createCacheKey($value); + $key = json_encode($value, JSON_THROW_ON_ERROR); - return $this->encodeCache[$key] ?? ($this->encodeCache[$key] = $this->codec->encode($value)); + return $this->cache['modifiersToPath'][$key] ??= $this->codec->modifiersToPath($value); } - /** - * @throws JsonException - */ - public function decode(ValueInterface $value): array + public function pathToModifiers(string $value): array { - $key = $this->createCacheKey($value); - - return $this->decodeCache[$key] ?? ($this->decodeCache[$key] = $this->codec->decode($value)); + return $this->cache['pathToModifiers'][$value] ??= $this->codec->pathToModifiers($value); } /** * @throws JsonException */ - private function createCacheKey(ValueInterface $value): string + public function expandModifiers(array|string $value): array { - return md5(json_encode($value->getValue(), JSON_THROW_ON_ERROR)); + $key = json_encode($value, JSON_THROW_ON_ERROR); + + return $this->cache['expandModifiers'][$key] ??= $this->codec->expandModifiers($value); } } diff --git a/src/Modifier/Codec/Value/PresetValue.php b/src/Modifier/Codec/Value/PresetValue.php deleted file mode 100644 index bc68a86..0000000 --- a/src/Modifier/Codec/Value/PresetValue.php +++ /dev/null @@ -1,17 +0,0 @@ -presetName; - } -} diff --git a/src/Modifier/Codec/Value/Value.php b/src/Modifier/Codec/Value/Value.php deleted file mode 100644 index c65b94f..0000000 --- a/src/Modifier/Codec/Value/Value.php +++ /dev/null @@ -1,23 +0,0 @@ - $value - */ - public function __construct( - private readonly array|string $value, - ) {} - - /** - * @return string|array - */ - public function getValue(): array|string - { - return $this->value; - } -} diff --git a/src/Modifier/Codec/Value/ValueInterface.php b/src/Modifier/Codec/Value/ValueInterface.php deleted file mode 100644 index 2ef4f49..0000000 --- a/src/Modifier/Codec/Value/ValueInterface.php +++ /dev/null @@ -1,10 +0,0 @@ - $preset) { - if (!is_array($preset)) { - throw new InvalidArgumentException(sprintf( - 'The argument passed into the method %s() must be an array of arrays (a preset name => an array of modifier aliases).', - __METHOD__, - )); - } - $this->presetCollection->add((string) $name, $preset); } } @@ -97,6 +89,11 @@ public function getModifierCollection(): ModifierCollectionInterface return $this->modifierCollection; } + public function getPresetCollection(): PresetCollectionInterface + { + return $this->presetCollection; + } + public function getCodec(): CodecInterface { return $this->codec; @@ -104,8 +101,9 @@ public function getCodec(): CodecInterface public function modifyImage(Image $image, PathInfoInterface $info, string|array $modifiers, bool $stripMeta = false): ModifyResult { - if (!is_array($modifiers)) { - $modifiers = $this->getCodec()->decode(new PresetValue($modifiers)); + if (is_string($modifiers)) { + $codec = $this->getCodec(); + $modifiers = $codec->expandModifiers(value: $modifiers); } if (empty($modifiers)) { diff --git a/src/Modifier/Facade/ModifierFacadeFactory.php b/src/Modifier/Facade/ModifierFacadeFactory.php index aa9c13c..70a0764 100644 --- a/src/Modifier/Facade/ModifierFacadeFactory.php +++ b/src/Modifier/Facade/ModifierFacadeFactory.php @@ -24,12 +24,22 @@ public function create(ConfigInterface $config): ModifierFacadeInterface $modifierCollection = $this->modifierCollectionFactory->create(); $codec = new RuntimeCachedCodec( - new PresetCodec( - new Codec($config, $modifierCollection), - $presetCollection, + codec: new PresetCodec( + codec: new Codec( + config: $config, + modifierCollection: $modifierCollection, + ), + config: $config, + modifierCollection: $modifierCollection, + presetCollection: $presetCollection, ), ); - return new ModifierFacade($config, $codec, $presetCollection, $modifierCollection); + return new ModifierFacade( + config: $config, + codec: $codec, + presetCollection: $presetCollection, + modifierCollection: $modifierCollection, + ); } } diff --git a/src/Modifier/Facade/ModifierFacadeInterface.php b/src/Modifier/Facade/ModifierFacadeInterface.php index f4cc58a..453729b 100644 --- a/src/Modifier/Facade/ModifierFacadeInterface.php +++ b/src/Modifier/Facade/ModifierFacadeInterface.php @@ -10,6 +10,8 @@ use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\ModifierInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Validator\ValidatorInterface; interface ModifierFacadeInterface @@ -20,7 +22,7 @@ interface ModifierFacadeInterface public function setModifiers(array $modifiers): void; /** - * @param array> $presets + * @param array $presets */ public function setPresets(array $presets): void; @@ -36,6 +38,8 @@ public function setValidators(array $validators): void; public function getModifierCollection(): ModifierCollectionInterface; + public function getPresetCollection(): PresetCollectionInterface; + public function getCodec(): CodecInterface; /** diff --git a/src/Modifier/Preset/Preset.php b/src/Modifier/Preset/Preset.php new file mode 100644 index 0000000..3a1c127 --- /dev/null +++ b/src/Modifier/Preset/Preset.php @@ -0,0 +1,19 @@ + $modifiers + */ + public function __construct( + public readonly array $modifiers, + public readonly ?DescriptorInterface $descriptor, + public readonly mixed $defaultDescriptorValue, + ) {} +} diff --git a/src/Modifier/Preset/PresetCollection.php b/src/Modifier/Preset/PresetCollection.php index 22980cc..2bfb649 100644 --- a/src/Modifier/Preset/PresetCollection.php +++ b/src/Modifier/Preset/PresetCollection.php @@ -8,12 +8,12 @@ final class PresetCollection implements PresetCollectionInterface { - /** @var array> */ + /** @var array */ private array $presets = []; - public function add(string $presetAlias, array $parameters): void + public function add(string $presetAlias, Preset $preset): void { - $this->presets[$presetAlias] = $parameters; + $this->presets[$presetAlias] = $preset; } public function has(string $presetAlias): bool @@ -24,7 +24,7 @@ public function has(string $presetAlias): bool /** * @throws InvalidArgumentException */ - public function get(string $presetAlias): array + public function get(string $presetAlias): Preset { if (!$this->has($presetAlias)) { throw new InvalidArgumentException(sprintf( diff --git a/src/Modifier/Preset/PresetCollectionInterface.php b/src/Modifier/Preset/PresetCollectionInterface.php index c5b6ecc..4ba3208 100644 --- a/src/Modifier/Preset/PresetCollectionInterface.php +++ b/src/Modifier/Preset/PresetCollectionInterface.php @@ -6,15 +6,9 @@ interface PresetCollectionInterface { - /** - * @param array $parameters - */ - public function add(string $presetAlias, array $parameters): void; + public function add(string $presetAlias, Preset $preset): void; public function has(string $presetAlias): bool; - /** - * @return array - */ - public function get(string $presetAlias): array; + public function get(string $presetAlias): Preset; } diff --git a/src/PathInfo.php b/src/PathInfo.php index 954cb5e..e1afc50 100644 --- a/src/PathInfo.php +++ b/src/PathInfo.php @@ -8,9 +8,6 @@ use SixtyEightPublishers\FileStorage\PathInfo as BasePathInfo; use SixtyEightPublishers\ImageStorage\Helper\SupportedType; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; -use function is_string; use function sprintf; final class PathInfo extends BasePathInfo implements PathInfoInterface @@ -68,7 +65,9 @@ public function withModifiers(string|array|null $modifiers): static public function withEncodedModifiers(string $modifiers): static { - return $this->withModifiers($this->codec->decode(new Value($modifiers))); + return $this->withModifiers( + modifiers: $this->codec->pathToModifiers(value: $modifiers), + ); } public function getPath(): string @@ -82,7 +81,7 @@ public function getPath(): string : sprintf('%s/%s', $namespace, $this->getName()); } - $modifier = $this->codec->encode(is_string($modifiers) ? new PresetValue($modifiers) : new Value($modifiers)); + $modifier = $this->codec->modifiersToPath(value: $modifiers); $extension = $this->getExtension() ?? SupportedType::getDefaultExtension(); return $namespace === '' diff --git a/src/PathInfoInterface.php b/src/PathInfoInterface.php index 4f8b228..6d8fdd6 100644 --- a/src/PathInfoInterface.php +++ b/src/PathInfoInterface.php @@ -19,7 +19,7 @@ public function getModifiers(): string|array|null; public function withModifiers(string|array|null $modifiers): static; /** - * Creates new object with encoded modifiers, the modifier will be decoded into an array. + * Creates a new object with encoded modifiers, the modifier will be decoded into an array. */ public function withEncodedModifiers(string $modifiers): static; } diff --git a/src/Responsive/Descriptor/ArgsFacade.php b/src/Responsive/Descriptor/ArgsFacade.php index 92fd5f8..ca76883 100644 --- a/src/Responsive/Descriptor/ArgsFacade.php +++ b/src/Responsive/Descriptor/ArgsFacade.php @@ -6,7 +6,6 @@ use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface; use function is_array; @@ -26,7 +25,9 @@ public function __construct( $modifiers = $this->pathInfo->getModifiers(); if (null !== $modifiers) { - $this->defaultModifiers = is_array($modifiers) ? $modifiers : $this->modifierFacade->getCodec()->decode(new PresetValue($modifiers)); + $this->defaultModifiers = is_array($modifiers) + ? $modifiers + : $this->modifierFacade->getCodec()->expandModifiers(value: $modifiers); } } diff --git a/src/Responsive/Descriptor/DescriptorInterface.php b/src/Responsive/Descriptor/DescriptorInterface.php index bc44cac..6500d98 100644 --- a/src/Responsive/Descriptor/DescriptorInterface.php +++ b/src/Responsive/Descriptor/DescriptorInterface.php @@ -4,10 +4,41 @@ namespace SixtyEightPublishers\ImageStorage\Responsive\Descriptor; +use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; +use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use Stringable; interface DescriptorInterface extends Stringable { + /** + * @param string|numeric|bool $value + * + * @return string|numeric|bool + * @throws InvalidArgumentException + */ + public function validateModifierValue( + mixed $value, + mixed $default, + ): mixed; + + /** + * @param string|numeric|bool $value + * + * @return array + * @throws InvalidArgumentException + */ + public function expandModifier( + ModifierCollectionInterface $modifierCollection, + mixed $value, + ): array; + + /** + * @return iterable> + */ + public function iterateModifiers( + ModifierCollectionInterface $modifierCollection, + ): iterable; + public function createSrcSet(ArgsFacade $args): SrcSet; } diff --git a/src/Responsive/Descriptor/WDescriptor.php b/src/Responsive/Descriptor/WDescriptor.php index b0ad80a..4c85aee 100644 --- a/src/Responsive/Descriptor/WDescriptor.php +++ b/src/Responsive/Descriptor/WDescriptor.php @@ -5,14 +5,17 @@ namespace SixtyEightPublishers\ImageStorage\Responsive\Descriptor; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; +use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Width; use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use function array_map; use function array_unique; use function array_values; use function implode; +use function is_numeric; use function range; use function sprintf; +use function var_export; final class WDescriptor implements DescriptorInterface { @@ -56,6 +59,67 @@ public static function fromRange(int $min, int $max, int $step = 100): self return new self(...array_values(array_unique($range))); } + public function validateModifierValue( + mixed $value, + mixed $default, + ): int { + if (true === $value) { + if (!is_numeric($default) && [] !== $this->widths) { + return $this->widths[0]; + } + + $value = $default; + } + + if (is_numeric($value) && in_array((int) $value, $this->widths, true)) { + return (int) $value; + } + + throw new InvalidArgumentException( + message: sprintf( + 'Invalid preset value "%s" passed for descriptor %s', + var_export($value, true), + $this, + ), + ); + } + + public function expandModifier( + ModifierCollectionInterface $modifierCollection, + mixed $value, + ): array { + $wAlias = $modifierCollection + ->getByName(Width::class) + ->getAlias(); + + if (is_numeric($value) && in_array((int) $value, $this->widths, true)) { + return [ + $wAlias => (int) $value, + ]; + } + + throw new InvalidArgumentException( + message: sprintf( + 'Invalid preset value "%s" passed for descriptor %s', + var_export($value, true), + $this, + ), + ); + } + + public function iterateModifiers(ModifierCollectionInterface $modifierCollection): iterable + { + $wAlias = $modifierCollection + ->getByName(Width::class) + ->getAlias(); + + foreach ($this->widths as $width) { + yield [ + $wAlias => $width, + ]; + } + } + public function __toString(): string { return sprintf('W(%s)', implode(',', $this->widths)); diff --git a/src/Responsive/Descriptor/XDescriptor.php b/src/Responsive/Descriptor/XDescriptor.php index 40a1fde..60d732c 100644 --- a/src/Responsive/Descriptor/XDescriptor.php +++ b/src/Responsive/Descriptor/XDescriptor.php @@ -4,6 +4,8 @@ namespace SixtyEightPublishers\ImageStorage\Responsive\Descriptor; +use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; +use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\PixelDensity; use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; use function array_map; @@ -37,6 +39,67 @@ public static function default(): self return new self(1, 2, 3); } + public function validateModifierValue( + mixed $value, + mixed $default, + ): float { + if (true === $value) { + if (!is_numeric($default) && [] !== $this->pixelDensities) { + return $this->pixelDensities[0]; + } + + $value = $default; + } + + if (is_numeric($value) && in_array((float) $value, $this->pixelDensities, true)) { + return (float) $value; + } + + throw new InvalidArgumentException( + message: sprintf( + 'Invalid preset value "%s" passed for descriptor %s', + var_export($value, true), + $this, + ), + ); + } + + public function expandModifier( + ModifierCollectionInterface $modifierCollection, + mixed $value, + ): array { + $pdAlias = $modifierCollection + ->getByName(PixelDensity::class) + ->getAlias(); + + if (is_numeric($value) && in_array((float) $value, $this->pixelDensities, true)) { + return [ + $pdAlias => (float) $value, + ]; + } + + throw new InvalidArgumentException( + message: sprintf( + 'Invalid preset value "%s" passed for descriptor %s', + var_export($value, true), + $this, + ), + ); + } + + public function iterateModifiers(ModifierCollectionInterface $modifierCollection): iterable + { + $pdAlias = $modifierCollection + ->getByName(PixelDensity::class) + ->getAlias(); + + foreach ($this->pixelDensities as $pixelDensity) { + yield [ + $pdAlias => $pixelDensity, + ]; + } + } + public function __toString(): string { return sprintf('X(%s)', implode(',', $this->pixelDensities)); diff --git a/src/Responsive/SrcSetGenerator.php b/src/Responsive/SrcSetGenerator.php index 7a8b325..77ed950 100644 --- a/src/Responsive/SrcSetGenerator.php +++ b/src/Responsive/SrcSetGenerator.php @@ -9,7 +9,6 @@ use SixtyEightPublishers\ImageStorage\PathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\ArgsFacade; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; -use function array_key_exists; final class SrcSetGenerator { @@ -25,7 +24,7 @@ public function generate(DescriptorInterface $descriptor, PathInfoInterface $pat { $key = $descriptor . '::' . ($absolute ? 'abs' : 'rel') . '::' . (empty($pathInfo->getModifiers()) ? $pathInfo->withModifiers(['original' => true]) : $pathInfo); - if (array_key_exists($key, $this->results)) { + if (isset($this->results[$key])) { return $this->results[$key]; } diff --git a/src/Security/KnownModifiers.php b/src/Security/KnownModifiers.php new file mode 100644 index 0000000..abc7a89 --- /dev/null +++ b/src/Security/KnownModifiers.php @@ -0,0 +1,20 @@ + $list + */ + public function __construct( + public readonly array $list, + ) {} + + public function isKnown(string $modifiers): bool + { + return isset($this->list[$modifiers]); + } +} diff --git a/src/Security/SignatureStrategy.php b/src/Security/SignatureStrategy.php index c65b889..ed3a644 100644 --- a/src/Security/SignatureStrategy.php +++ b/src/Security/SignatureStrategy.php @@ -6,6 +6,8 @@ use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\ImageStorage\Config\Config; +use function array_pop; +use function explode; use function hash_equals; use function hash_hmac; use function is_string; @@ -15,9 +17,32 @@ final class SignatureStrategy implements SignatureStrategyInterface { public function __construct( private readonly ConfigInterface $config, + private readonly KnownModifiers $knownModifiers, ) {} - public function createToken(string $path): string + public function createToken(string $path): ?string + { + if ($this->isKnown(path: $path)) { + return null; + } + + return $this->doCreateToken(path: $path); + } + + public function verifyToken(string $token, string $path): bool + { + if ($this->isKnown($path)) { + return true; + } + + if ('' === $token) { + return false; + } + + return hash_equals($token, $this->doCreateToken(path: $path)); + } + + private function doCreateToken(string $path): string { $algo = $this->config[Config::SIGNATURE_ALGORITHM]; $key = $this->config[Config::SIGNATURE_KEY]; @@ -29,8 +54,22 @@ public function createToken(string $path): string ); } - public function verifyToken(string $token, string $path): bool + private function isKnown(string $path): bool { - return hash_equals($token, $this->createToken($path)); + if (!$this->config[Config::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS]) { + return false; + } + + $parts = explode('/', $path); + array_pop($parts); # filename + $modifiers = array_pop($parts); + + if (null === $modifiers) { + return false; + } + + return $this->knownModifiers->isKnown( + modifiers: $modifiers, + ); } } diff --git a/src/Security/SignatureStrategyInterface.php b/src/Security/SignatureStrategyInterface.php index 399852f..b0d4848 100644 --- a/src/Security/SignatureStrategyInterface.php +++ b/src/Security/SignatureStrategyInterface.php @@ -6,7 +6,7 @@ interface SignatureStrategyInterface { - public function createToken(string $path): string; + public function createToken(string $path): ?string; public function verifyToken(string $token, string $path): bool; } diff --git a/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php b/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php index 10e73fc..b64d7ca 100644 --- a/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php +++ b/tests/Bridge/Nette/DI/ImageStorageExtensionTest.php @@ -123,6 +123,7 @@ public function testExtensionShouldBeIntegratedWithMinimalConfiguration(): void Config::SIGNATURE_PARAMETER_NAME => '_s', Config::SIGNATURE_KEY => null, Config::SIGNATURE_ALGORITHM => 'sha256', + Config::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS => false, Config::ALLOWED_PIXEL_DENSITY => [], Config::ALLOWED_RESOLUTIONS => [], Config::ALLOWED_QUALITIES => [], @@ -194,6 +195,7 @@ public function testExtensionShouldBeIntegratedWithExternalImageServer(): void Config::SIGNATURE_PARAMETER_NAME => '_s', Config::SIGNATURE_KEY => null, Config::SIGNATURE_ALGORITHM => 'sha256', + Config::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS => false, Config::ALLOWED_PIXEL_DENSITY => [], Config::ALLOWED_RESOLUTIONS => [], Config::ALLOWED_QUALITIES => [], @@ -220,6 +222,7 @@ public function testExtensionShouldBeIntegratedWithSignatureStrategy(): void Config::SIGNATURE_PARAMETER_NAME => '_s', Config::SIGNATURE_KEY => 'abc', Config::SIGNATURE_ALGORITHM => 'sha256', + Config::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS => false, Config::ALLOWED_PIXEL_DENSITY => [], Config::ALLOWED_RESOLUTIONS => [], Config::ALLOWED_QUALITIES => [], @@ -257,17 +260,29 @@ public function testExtensionShouldBeIntegratedWithCustomModifiersAndApplicators TestValidator::class, ], presets: [ - 'small' => [ - 'w' => 100, - 'ar' => '2x1', - ], - 'huge' => [ - 'w' => 1000, - 'ar' => '16x9', - ], - 'rotated' => [ - 'o' => 180, - ], + 'small' => new Modifier\Preset\Preset( + modifiers: [ + 'w' => 100, + 'ar' => '2x1', + ], + descriptor: null, + defaultDescriptorValue: null, + ), + 'huge' => new Modifier\Preset\Preset( + modifiers: [ + 'w' => 1000, + 'ar' => '16x9', + ], + descriptor: null, + defaultDescriptorValue: null, + ), + 'rotated' => new Modifier\Preset\Preset( + modifiers: [ + 'o' => 180, + ], + descriptor: null, + defaultDescriptorValue: null, + ), ], ); } @@ -500,7 +515,7 @@ static function () use ($modifierFacade, $applicatorTypes, $validatorTypes, $pre call_user_func(Closure::bind( static function () use ($presetCollection, $presets): void { - Assert::same($presets, $presetCollection->presets); + Assert::equal($presets, $presetCollection->presets); }, null, PresetCollection::class, diff --git a/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon b/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon index 73fed5d..32c7657 100644 --- a/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon +++ b/tests/Bridge/Nette/DI/config/ImageStorage/config.withModifiersAndApplicatorsAndValidatorsAndPresets.neon @@ -25,10 +25,13 @@ image_storage: - SixtyEightPublishers\ImageStorage\Tests\Fixtures\TestValidator presets: small: - w: 100 - ar: 2x1 + modifiers: + w: 100 + ar: 2x1 huge: - w: 1000 - ar: 16x9 + modifiers: + w: 1000 + ar: 16x9 rotated: - o: 180 + modifiers: + o: 180 diff --git a/tests/ImageServer/LocalImageServerTest.phpt b/tests/ImageServer/LocalImageServerTest.phpt index 0e75ef3..ef8d19b 100644 --- a/tests/ImageServer/LocalImageServerTest.phpt +++ b/tests/ImageServer/LocalImageServerTest.phpt @@ -32,7 +32,7 @@ final class LocalImageServerTest extends TestCase $config = $this->createConfig('', true); $response = (object) ['test' => true]; - $server = new LocalImageServer($imageStorage, $this->expectErrorResponse($response, 'Missing signature in request.', 403)); + $server = new LocalImageServer($imageStorage, $this->expectErrorResponse($response, 'Request contains invalid signature.', 403)); $imageStorage->shouldReceive('getConfig') ->withNoArgs() @@ -43,6 +43,11 @@ final class LocalImageServerTest extends TestCase ->withNoArgs() ->andReturn($signatureStrategy); + $signatureStrategy->shouldReceive('verifyToken') + ->once() + ->with('', 'path/w:100/image.png') + ->andReturn(false); + Assert::same($response, $server->getImageResponse($this->createRequest('/path/w:100/image.png', null))); } diff --git a/tests/LinkGenerator/LinkGeneratorTest.phpt b/tests/LinkGenerator/LinkGeneratorTest.phpt index f6f668e..15e6115 100644 --- a/tests/LinkGenerator/LinkGeneratorTest.phpt +++ b/tests/LinkGenerator/LinkGeneratorTest.phpt @@ -11,6 +11,8 @@ use SixtyEightPublishers\ImageStorage\Config\Config; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGenerator; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; use SixtyEightPublishers\ImageStorage\PathInfoInterface as ImagePathInfoInterface; use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; use SixtyEightPublishers\ImageStorage\Responsive\SrcSet; @@ -186,6 +188,148 @@ final class LinkGeneratorTest extends TestCase Assert::same($srcSet, $linkGenerator->srcSet($pathInfo, $descriptor, false)); } + public function testSrcSetShouldBeCreatedWithDescriptorResolvedFromPreset(): void + { + $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); + $srcSetGeneratorFactory = Mockery::mock(SrcSetGeneratorFactoryInterface::class); + $srcSetGenerator = Mockery::mock(SrcSetGenerator::class); + $pathInfo = Mockery::mock(ImagePathInfoInterface::class); + $descriptor = Mockery::mock(DescriptorInterface::class); + $presetCollection = Mockery::mock(PresetCollectionInterface::class); + $preset = new Preset(['w' => 100], $descriptor, 100); + $linkGenerator = new LinkGenerator(new Config([]), $modifierFacade, $srcSetGeneratorFactory); + $srcSet = new SrcSet( + descriptor: 'test', + links: [ + 1 => 'srcset', + ], + value: 'srcset', + ); + + $pathInfo->shouldReceive('getModifiers') + ->once() + ->withNoArgs() + ->andReturn('preset'); + + $modifierFacade->shouldReceive('getPresetCollection') + ->once() + ->withNoArgs() + ->andReturn($presetCollection); + + $presetCollection->shouldReceive('get') + ->once() + ->with('preset') + ->andReturn($preset); + + $srcSetGeneratorFactory->shouldReceive('create') + ->once() + ->with($linkGenerator, $modifierFacade) + ->andReturn($srcSetGenerator); + + $srcSetGenerator->shouldReceive('generate') + ->once() + ->with($descriptor, $pathInfo, true) + ->andReturn($srcSet); + + Assert::same($srcSet, $linkGenerator->srcSet($pathInfo)); + } + + public function testSrcSetShouldBeCreatedWithDescriptorResolvedFromPresetWithCustomValue(): void + { + $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); + $srcSetGeneratorFactory = Mockery::mock(SrcSetGeneratorFactoryInterface::class); + $srcSetGenerator = Mockery::mock(SrcSetGenerator::class); + $pathInfo = Mockery::mock(ImagePathInfoInterface::class); + $descriptor = Mockery::mock(DescriptorInterface::class); + $presetCollection = Mockery::mock(PresetCollectionInterface::class); + $preset = new Preset(['w' => 100], $descriptor, 100); + $linkGenerator = new LinkGenerator(new Config([]), $modifierFacade, $srcSetGeneratorFactory); + $srcSet = new SrcSet( + descriptor: 'test', + links: [ + 1 => 'srcset', + ], + value: 'srcset', + ); + + $pathInfo->shouldReceive('getModifiers') + ->once() + ->withNoArgs() + ->andReturn('preset:200'); + + $modifierFacade->shouldReceive('getPresetCollection') + ->once() + ->withNoArgs() + ->andReturn($presetCollection); + + $presetCollection->shouldReceive('get') + ->once() + ->with('preset') + ->andReturn($preset); + + $srcSetGeneratorFactory->shouldReceive('create') + ->once() + ->with($linkGenerator, $modifierFacade) + ->andReturn($srcSetGenerator); + + $srcSetGenerator->shouldReceive('generate') + ->once() + ->with($descriptor, $pathInfo, true) + ->andReturn($srcSet); + + Assert::same($srcSet, $linkGenerator->srcSet($pathInfo)); + } + + public function testExceptionShouldBeThrownWhenDescriptorCannotBeResolvedFromPresetWithoutDescriptor(): void + { + $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); + $srcSetGeneratorFactory = Mockery::mock(SrcSetGeneratorFactoryInterface::class); + $pathInfo = Mockery::mock(ImagePathInfoInterface::class); + $presetCollection = Mockery::mock(PresetCollectionInterface::class); + $preset = new Preset(['w' => 100], null, null); + $linkGenerator = new LinkGenerator(new Config([]), $modifierFacade, $srcSetGeneratorFactory); + + $pathInfo->shouldReceive('getModifiers') + ->once() + ->withNoArgs() + ->andReturn('preset'); + + $modifierFacade->shouldReceive('getPresetCollection') + ->once() + ->withNoArgs() + ->andReturn($presetCollection); + + $presetCollection->shouldReceive('get') + ->once() + ->with('preset') + ->andReturn($preset); + + Assert::exception( + static fn () => $linkGenerator->srcSet($pathInfo), + InvalidArgumentException::class, + '#Unable to resolve descriptor for path info .+\. Descriptor must be provided to the method .+::srcSet\(\) manually\.#', + ); + } + + public function testExceptionShouldBeThrownWhenDescriptorCannotBeResolvedFromArrayModifiers(): void + { + $modifierFacade = Mockery::mock(ModifierFacadeInterface::class); + $srcSetGeneratorFactory = Mockery::mock(SrcSetGeneratorFactoryInterface::class); + $pathInfo = Mockery::mock(ImagePathInfoInterface::class); + $linkGenerator = new LinkGenerator(new Config([]), $modifierFacade, $srcSetGeneratorFactory); + + $pathInfo->shouldReceive('getModifiers') + ->once() + ->withNoArgs() + ->andReturn(['w' => 100, 'h' => 200]); + + Assert::exception( + static fn () => $linkGenerator->srcSet($pathInfo), + InvalidArgumentException::class, + '#Unable to resolve descriptor for path info .+\. Descriptor must be provided to the method .+::srcSet\(\) manually\.#', + ); + } + public function tearDown(): void { Mockery::close(); @@ -202,7 +346,7 @@ final class LinkGeneratorTest extends TestCase ->andReturn($version); $pathInfo->shouldReceive('getModifiers') - ->once() + ->atLeast()->once() ->withNoArgs() ->andReturn(['w' => 100, 'h' => 200]); diff --git a/tests/Modifier/Codec/CodecTest.phpt b/tests/Modifier/Codec/CodecTest.phpt index 5b509d3..488fce5 100644 --- a/tests/Modifier/Codec/CodecTest.phpt +++ b/tests/Modifier/Codec/CodecTest.phpt @@ -10,7 +10,6 @@ use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\ImageStorage\Config\Config; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\Modifier\Codec\Codec; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\ModifierInterface; use SixtyEightPublishers\ImageStorage\Modifier\ParsableModifierInterface; @@ -29,7 +28,7 @@ final class CodecTest extends TestCase $codec = new Codec($config, $modifierCollection); Assert::exception( - static fn () => $codec->encode(new Value('test')), + static fn () => $codec->modifiersToPath('test'), InvalidArgumentException::class, 'Can not decode value of type string, the value must be array.', ); @@ -42,7 +41,7 @@ final class CodecTest extends TestCase $codec = new Codec($config, $modifierCollection); Assert::exception( - static fn () => $codec->encode(new Value([])), + static fn () => $codec->modifiersToPath([]), InvalidArgumentException::class, 'Value can not be an empty array.', ); @@ -69,26 +68,13 @@ final class CodecTest extends TestCase ->andReturn($modifier); } - Assert::same('ar:16x9,flag_a,pd:2.5,w:100', $codec->encode(new Value([ + Assert::same('ar:16x9,flag_a,pd:2.5,w:100', $codec->modifiersToPath([ 'w' => 100, 'pd' => 2.5, 'ar' => '16x9', 'flag_a' => true, 'flag_b' => false, - ]))); - } - - public function testExceptionShouldBeThrownIfNonStringValueIsDecoded(): void - { - $config = Mockery::mock(ConfigInterface::class); - $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); - $codec = new Codec($config, $modifierCollection); - - Assert::exception( - static fn () => $codec->decode(new Value([])), - InvalidArgumentException::class, - 'Can not decode value of type array, the value must be string or Stringable object.', - ); + ])); } public function testExceptionShouldBeThrownIfEmptyStringValueIsDecoded(): void @@ -98,7 +84,7 @@ final class CodecTest extends TestCase $codec = new Codec($config, $modifierCollection); Assert::exception( - static fn () => $codec->decode(new Value('')), + static fn () => $codec->pathToModifiers(''), InvalidArgumentException::class, 'Value can not be an empty string.', ); @@ -111,7 +97,7 @@ final class CodecTest extends TestCase $codec = new Codec($config, $modifierCollection); Assert::exception( - static fn () => $codec->decode(new Value('w:100:200,ar:16x9')), + static fn () => $codec->pathToModifiers('w:100:200,ar:16x9'), InvalidArgumentException::class, 'An invalid path "w:100:200,ar:16x9" passed, the modifier "w:100:200" has an invalid format.', ); @@ -135,7 +121,7 @@ final class CodecTest extends TestCase ->andReturn('w'); Assert::exception( - static fn () => $codec->decode(new Value('w')), + static fn () => $codec->pathToModifiers('w'), InvalidArgumentException::class, 'An invalid path "w" passed, the modifier "w" must have a value.', ); @@ -159,7 +145,7 @@ final class CodecTest extends TestCase ->andReturn('flag_a'); Assert::exception( - static fn () => $codec->decode(new Value('flag_a:value')), + static fn () => $codec->pathToModifiers('flag_a:value'), InvalidArgumentException::class, 'An invalid path "flag_a:value" passed, the modifier "flag_a" can not have a value.', ); @@ -197,7 +183,7 @@ final class CodecTest extends TestCase 'flag_a' => true, 'pd' => '2.5', 'w' => '100', - ], $codec->decode(new Value('ar:16x9,flag_a,pd:2.5,w:100'))); + ], $codec->pathToModifiers('ar:16x9,flag_a,pd:2.5,w:100')); } protected function tearDown(): void diff --git a/tests/Modifier/Codec/PresetCodecTest.phpt b/tests/Modifier/Codec/PresetCodecTest.phpt index b708e40..1cffeee 100644 --- a/tests/Modifier/Codec/PresetCodecTest.phpt +++ b/tests/Modifier/Codec/PresetCodecTest.phpt @@ -5,11 +5,14 @@ declare(strict_types=1); namespace SixtyEightPublishers\ImageStorage\Tests\Modifier\Codec; use Mockery; +use SixtyEightPublishers\FileStorage\Config\ConfigInterface; +use SixtyEightPublishers\ImageStorage\Config\Config; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; use SixtyEightPublishers\ImageStorage\Modifier\Codec\PresetCodec; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; +use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; +use SixtyEightPublishers\ImageStorage\Responsive\Descriptor\DescriptorInterface; use Tester\Assert; use Tester\TestCase; @@ -20,72 +23,160 @@ final class PresetCodecTest extends TestCase public function testSimpleValueShouldBeEncoded(): void { $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); $presetCollection = Mockery::mock(PresetCollectionInterface::class); - $presetCodec = new PresetCodec($innerCodec, $presetCollection); - $value = new Value(['w' => 100, 'h' => 200]); + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); + $value = ['w' => 100, 'h' => 200]; - $innerCodec->shouldReceive('encode') + $innerCodec->shouldReceive('modifiersToPath') ->once() ->with($value) ->andReturn('w:100,h:200'); - Assert::same('w:100,h:200', $presetCodec->encode($value)); + Assert::same('w:100,h:200', $presetCodec->modifiersToPath($value)); } public function testPresetValueShouldBeEncoded(): void { $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); $presetCollection = Mockery::mock(PresetCollectionInterface::class); - $presetCodec = new PresetCodec($innerCodec, $presetCollection); - $value = new PresetValue('preset'); - $preset = ['w' => 100, 'h' => 200]; + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); + $preset = new Preset(['w' => 100, 'h' => 200], null, null); + + $config->shouldReceive('offsetGet') + ->with(Config::MODIFIER_ASSIGNER) + ->andReturn(':'); $presetCollection->shouldReceive('get') ->once() ->with('preset') ->andReturn($preset); - $innerCodec->shouldReceive('encode') + $innerCodec->shouldReceive('modifiersToPath') ->once() - ->with(Mockery::type(Value::class)) - ->andReturnUsing(static function (Value $value) use ($preset): string { - Assert::same($preset, $value->getValue()); - - return 'w:100,h:200'; - }); + ->with(['w' => 100, 'h' => 200]) + ->andReturn('w:100,h:200'); - Assert::same('w:100,h:200', $presetCodec->encode($value)); + Assert::same('w:100,h:200', $presetCodec->modifiersToPath('preset')); } public function testSimpleValueShouldBeDecoded(): void { $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); $presetCollection = Mockery::mock(PresetCollectionInterface::class); - $presetCodec = new PresetCodec($innerCodec, $presetCollection); - $value = new Value('w:100,h:200'); + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); - $innerCodec->shouldReceive('decode') + $innerCodec->shouldReceive('pathToModifiers') ->once() - ->with($value) + ->with('w:100,h:200') + ->andReturn(['w' => 100, 'h' => 200]); + + Assert::same(['w' => 100, 'h' => 200], $presetCodec->pathToModifiers('w:100,h:200')); + } + + public function testPresetValueShouldBeExpanded(): void + { + $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); + $presetCollection = Mockery::mock(PresetCollectionInterface::class); + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); + $preset = new Preset(['w' => 100, 'h' => 200], null, null); + + $config->shouldReceive('offsetGet') + ->with(Config::MODIFIER_ASSIGNER) + ->andReturn(':'); + + $presetCollection->shouldReceive('get') + ->once() + ->with('preset') + ->andReturn($preset); + + $innerCodec->shouldReceive('expandModifiers') + ->once() + ->with(['w' => 100, 'h' => 200]) ->andReturn(['w' => 100, 'h' => 200]); - Assert::same(['w' => 100, 'h' => 200], $presetCodec->decode($value)); + Assert::same(['w' => 100, 'h' => 200], $presetCodec->expandModifiers('preset')); + } + + public function testPresetValueWithDescriptorShouldBeExpanded(): void + { + $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); + $presetCollection = Mockery::mock(PresetCollectionInterface::class); + $descriptor = Mockery::mock(DescriptorInterface::class); + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); + $preset = new Preset(['ar' => '16x9'], $descriptor, 100); + + $config->shouldReceive('offsetGet') + ->with(Config::MODIFIER_ASSIGNER) + ->andReturn(':'); + + $presetCollection->shouldReceive('get') + ->once() + ->with('preset') + ->andReturn($preset); + + $descriptor->shouldReceive('validateModifierValue') + ->once() + ->with(true, 100) + ->andReturn(100); + + $descriptor->shouldReceive('expandModifier') + ->once() + ->with($modifierCollection, 100) + ->andReturn(['w' => 100]); + + $innerCodec->shouldReceive('expandModifiers') + ->once() + ->with(['ar' => '16x9', 'w' => 100]) + ->andReturn(['ar' => '16x9', 'w' => 100]); + + Assert::same(['ar' => '16x9', 'w' => 100], $presetCodec->expandModifiers('preset')); } - public function testPresetValueShouldBeDecoded(): void + public function testPresetValueWithDescriptorAndCustomValueShouldBeExpanded(): void { $innerCodec = Mockery::mock(CodecInterface::class); + $config = Mockery::mock(ConfigInterface::class); + $modifierCollection = Mockery::mock(ModifierCollectionInterface::class); $presetCollection = Mockery::mock(PresetCollectionInterface::class); - $presetCodec = new PresetCodec($innerCodec, $presetCollection); - $value = new PresetValue('preset'); - $preset = ['w' => 100, 'h' => 200]; + $descriptor = Mockery::mock(DescriptorInterface::class); + $presetCodec = new PresetCodec($innerCodec, $config, $modifierCollection, $presetCollection); + $preset = new Preset(['ar' => '16x9'], $descriptor, 100); + + $config->shouldReceive('offsetGet') + ->with(Config::MODIFIER_ASSIGNER) + ->andReturn(':'); $presetCollection->shouldReceive('get') ->once() ->with('preset') ->andReturn($preset); - Assert::same(['w' => 100, 'h' => 200], $presetCodec->decode($value)); + $descriptor->shouldReceive('validateModifierValue') + ->once() + ->with('200', 100) + ->andReturn(200); + + $descriptor->shouldReceive('expandModifier') + ->once() + ->with($modifierCollection, 200) + ->andReturn(['w' => 200]); + + $innerCodec->shouldReceive('expandModifiers') + ->once() + ->with(['ar' => '16x9', 'w' => 200]) + ->andReturn(['ar' => '16x9', 'w' => 200]); + + Assert::same(['ar' => '16x9', 'w' => 200], $presetCodec->expandModifiers('preset:200')); } protected function tearDown(): void diff --git a/tests/Modifier/Codec/RuntimeCachedCodecTest.phpt b/tests/Modifier/Codec/RuntimeCachedCodecTest.phpt index a91ed27..c0389e2 100644 --- a/tests/Modifier/Codec/RuntimeCachedCodecTest.phpt +++ b/tests/Modifier/Codec/RuntimeCachedCodecTest.phpt @@ -7,7 +7,6 @@ namespace SixtyEightPublishers\ImageStorage\Tests\Modifier\Codec; use Mockery; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; use SixtyEightPublishers\ImageStorage\Modifier\Codec\RuntimeCachedCodec; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; use Tester\Assert; use Tester\TestCase; @@ -19,60 +18,60 @@ final class RuntimeCachedCodecTest extends TestCase { $innerCodec = Mockery::mock(CodecInterface::class); $runtimeCachedCodec = new RuntimeCachedCodec($innerCodec); - $value = new Value(['w' => 100, 'h' => 200]); + $value = ['w' => 100, 'h' => 200]; - $innerCodec->shouldReceive('encode') + $innerCodec->shouldReceive('modifiersToPath') ->once() ->with($value) ->andReturn('w:100,h:200'); - Assert::same('w:100,h:200', $runtimeCachedCodec->encode($value)); - Assert::same('w:100,h:200', $runtimeCachedCodec->encode($value)); + Assert::same('w:100,h:200', $runtimeCachedCodec->modifiersToPath($value)); + Assert::same('w:100,h:200', $runtimeCachedCodec->modifiersToPath($value)); } public function testStringValueShouldBeEncodedAndCached(): void { $innerCodec = Mockery::mock(CodecInterface::class); $runtimeCachedCodec = new RuntimeCachedCodec($innerCodec); - $value = new Value('preset'); + $value = 'preset'; - $innerCodec->shouldReceive('encode') + $innerCodec->shouldReceive('modifiersToPath') ->once() ->with($value) ->andReturn('w:100,h:200'); - Assert::same('w:100,h:200', $runtimeCachedCodec->encode($value)); - Assert::same('w:100,h:200', $runtimeCachedCodec->encode($value)); + Assert::same('w:100,h:200', $runtimeCachedCodec->modifiersToPath($value)); + Assert::same('w:100,h:200', $runtimeCachedCodec->modifiersToPath($value)); } - public function testArrayValueShouldBeDecodedAndCached(): void + public function testStringValueShouldBeDecodedAndCached2(): void { $innerCodec = Mockery::mock(CodecInterface::class); $runtimeCachedCodec = new RuntimeCachedCodec($innerCodec); - $value = new Value(['w' => 100, 'h' => 200]); + $value = 'w:100,h:200'; - $innerCodec->shouldReceive('decode') + $innerCodec->shouldReceive('pathToModifiers') ->once() ->with($value) ->andReturn(['w' => 100, 'h' => 200]); - Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->decode($value)); - Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->decode($value)); + Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->pathToModifiers($value)); + Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->pathToModifiers($value)); } public function testStringValueShouldBeDecodedAndCached(): void { $innerCodec = Mockery::mock(CodecInterface::class); $runtimeCachedCodec = new RuntimeCachedCodec($innerCodec); - $value = new Value('w:100,h:100'); + $value = 'w:100,h:100'; - $innerCodec->shouldReceive('decode') + $innerCodec->shouldReceive('pathToModifiers') ->once() ->with($value) ->andReturn(['w' => 100, 'h' => 200]); - Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->decode($value)); - Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->decode($value)); + Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->pathToModifiers($value)); + Assert::same(['w' => 100, 'h' => 200], $runtimeCachedCodec->pathToModifiers($value)); } protected function tearDown(): void diff --git a/tests/Modifier/Facade/ModifierFacadeTest.phpt b/tests/Modifier/Facade/ModifierFacadeTest.phpt index 800094d..821e006 100644 --- a/tests/Modifier/Facade/ModifierFacadeTest.phpt +++ b/tests/Modifier/Facade/ModifierFacadeTest.phpt @@ -12,16 +12,17 @@ use SixtyEightPublishers\FileStorage\PathInfoInterface; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\Modifier\Applicator\ModifierApplicatorInterface; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierValues; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacade; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifyResult; use SixtyEightPublishers\ImageStorage\Modifier\ModifierInterface; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Validator\ValidatorInterface; use Tester\Assert; use Tester\TestCase; +use TypeError; use function call_user_func; require __DIR__ . '/../../bootstrap.php'; @@ -71,8 +72,7 @@ final class ModifierFacadeTest extends TestCase Assert::exception( static fn () => $facade->setPresets(['test']), - InvalidArgumentException::class, - 'The argument passed into the method SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacade::setPresets() must be an array of arrays (a preset name => an array of modifier aliases).', + TypeError::class, ); } @@ -80,16 +80,16 @@ final class ModifierFacadeTest extends TestCase { $presetCollection = Mockery::mock(PresetCollectionInterface::class); $presets = [ - 'a' => ['w' => 100], - 'b' => ['w' => 150, 'f' => 'stretch'], - 'c' => ['w' => 150, 'ar' => '16x9'], + 'a' => new Preset(['w' => 100], null, null), + 'b' => new Preset(['w' => 150, 'f' => 'stretch'], null, null), + 'c' => new Preset(['w' => 150, 'ar' => '16x9'], null, null), ]; $addedPresets = []; $presetCollection->shouldReceive('add') ->times(3) - ->with(Mockery::type('string'), Mockery::type('array')) - ->andReturnUsing(static function (string $name, array $preset) use (&$addedPresets) { + ->with(Mockery::type('string'), Mockery::type(Preset::class)) + ->andReturnUsing(static function (string $name, Preset $preset) use (&$addedPresets) { $addedPresets[$name] = $preset; return null; @@ -291,14 +291,10 @@ final class ModifierFacadeTest extends TestCase $modifiers = ['w' => 100, 'h' => 200]; $preset = 'preset'; - $codec->shouldReceive('decode') + $codec->shouldReceive('expandModifiers') ->once() - ->with(Mockery::type(PresetValue::class)) - ->andReturnUsing(static function (PresetValue $value) use ($preset, $modifiers): array { - Assert::same($preset, $value->presetName); - - return $modifiers; - }); + ->with($preset) + ->andReturn($modifiers); $modifierCollection->shouldReceive('parseValues') ->once() @@ -344,14 +340,10 @@ final class ModifierFacadeTest extends TestCase $modifiers = ['w' => 100, 'h' => 200]; $preset = 'preset'; - $codec->shouldReceive('decode') + $codec->shouldReceive('expandModifiers') ->once() - ->with(Mockery::type(PresetValue::class)) - ->andReturnUsing(static function (PresetValue $value) use ($preset, $modifiers): array { - Assert::same($preset, $value->presetName); - - return $modifiers; - }); + ->with($preset) + ->andReturn($modifiers); $modifierCollection->shouldReceive('parseValues') ->once() diff --git a/tests/Modifier/Preset/PresetCollectionTest.phpt b/tests/Modifier/Preset/PresetCollectionTest.phpt index a129e96..07fb46c 100644 --- a/tests/Modifier/Preset/PresetCollectionTest.phpt +++ b/tests/Modifier/Preset/PresetCollectionTest.phpt @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SixtyEightPublishers\ImageStorage\Tests\Modifier\Preset; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; +use SixtyEightPublishers\ImageStorage\Modifier\Preset\Preset; use SixtyEightPublishers\ImageStorage\Modifier\Preset\PresetCollection; use Tester\Assert; use Tester\TestCase; @@ -17,15 +18,15 @@ final class PresetCollectionTest extends TestCase { $collection = new PresetCollection(); - $collection->add('a', ['w' => 15]); - $collection->add('b', ['w' => 15, 'pd' => 2.0]); + $collection->add('a', new Preset(['w' => 15], null, null)); + $collection->add('b', new Preset(['w' => 15, 'pd' => 2.0], null, null)); Assert::true($collection->has('a')); Assert::true($collection->has('b')); Assert::false($collection->has('c')); - Assert::same(['w' => 15], $collection->get('a')); - Assert::same(['w' => 15, 'pd' => 2.0], $collection->get('b')); + Assert::same(['w' => 15], $collection->get('a')->modifiers); + Assert::same(['w' => 15, 'pd' => 2.0], $collection->get('b')->modifiers); Assert::exception( static fn () => $collection->get('c'), diff --git a/tests/PathInfoTest.phpt b/tests/PathInfoTest.phpt index 1ff92ac..c744e5e 100644 --- a/tests/PathInfoTest.phpt +++ b/tests/PathInfoTest.phpt @@ -7,8 +7,6 @@ namespace SixtyEightPublishers\ImageStorage\Tests; use Mockery; use SixtyEightPublishers\FileStorage\Exception\PathInfoException; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\Value; use SixtyEightPublishers\ImageStorage\PathInfo; use Tester\Assert; use Tester\TestCase; @@ -112,17 +110,13 @@ final class PathInfoTest extends TestCase { $codec = Mockery::mock(CodecInterface::class); - $codec->shouldReceive('decode') + $codec->shouldReceive('pathToModifiers') ->once() - ->with(Mockery::type(Value::class)) - ->andReturnUsing(static function (Value $value): array { - Assert::same('w:15,h:15', $value->getValue()); - - return [ - 'w' => 15, - 'h' => 15, - ]; - }); + ->with('w:15,h:15') + ->andReturn([ + 'w' => 15, + 'h' => 15, + ]); $info1 = new PathInfo($codec, 'var/www', 'image', 'png', null); $info2 = $info1->withEncodedModifiers('w:15,h:15'); @@ -151,23 +145,15 @@ final class PathInfoTest extends TestCase $codecPreset = Mockery::mock(CodecInterface::class); $codecArray = Mockery::mock(CodecInterface::class); - $codecPreset->shouldReceive('encode') + $codecPreset->shouldReceive('modifiersToPath') ->times(4) - ->with(Mockery::type(PresetValue::class)) - ->andReturnUsing(static function (PresetValue $value): string { - Assert::same('preset', $value->presetName); - - return 'w:15,h:15'; - }); + ->with('preset') + ->andReturn('w:15,h:15'); - $codecArray->shouldReceive('encode') + $codecArray->shouldReceive('modifiersToPath') ->times(4) - ->with(Mockery::type(Value::class)) - ->andReturnUsing(static function (Value $value): string { - Assert::same(['h' => 15, 'w' => 15], $value->getValue()); - - return 'w:15,h:15'; - }); + ->with(['h' => 15, 'w' => 15]) + ->andReturn('w:15,h:15'); $infoPreset1 = new PathInfo($codecPreset, 'var/www', 'image', null, 'preset'); $infoPreset2 = new PathInfo($codecPreset, 'var/www', 'image', 'png', 'preset'); diff --git a/tests/Responsive/Descriptor/ArgsFacadeTest.phpt b/tests/Responsive/Descriptor/ArgsFacadeTest.phpt index a1cce49..c131c18 100644 --- a/tests/Responsive/Descriptor/ArgsFacadeTest.phpt +++ b/tests/Responsive/Descriptor/ArgsFacadeTest.phpt @@ -8,7 +8,6 @@ use Mockery; use SixtyEightPublishers\ImageStorage\Exception\InvalidArgumentException; use SixtyEightPublishers\ImageStorage\LinkGenerator\LinkGeneratorInterface; use SixtyEightPublishers\ImageStorage\Modifier\Codec\CodecInterface; -use SixtyEightPublishers\ImageStorage\Modifier\Codec\Value\PresetValue; use SixtyEightPublishers\ImageStorage\Modifier\Collection\ModifierCollectionInterface; use SixtyEightPublishers\ImageStorage\Modifier\Facade\ModifierFacadeInterface; use SixtyEightPublishers\ImageStorage\Modifier\Width; @@ -70,14 +69,10 @@ final class ArgsFacadeTest extends TestCase ->withNoArgs() ->andReturn($codec); - $codec->shouldReceive('decode') + $codec->shouldReceive('expandModifiers') ->once() - ->with(Mockery::type(PresetValue::class)) - ->andReturnUsing(static function (PresetValue $value): array { - Assert::same('preset', $value->presetName); - - return ['w' => 150]; - }); + ->with('preset') + ->andReturn(['w' => 150]); $facade = new ArgsFacade($linkGenerator, $modifierFacade, $pathInfo, true); diff --git a/tests/Security/SignatureStrategyTest.phpt b/tests/Security/SignatureStrategyTest.phpt index e0dcfa2..9e15c12 100644 --- a/tests/Security/SignatureStrategyTest.phpt +++ b/tests/Security/SignatureStrategyTest.phpt @@ -7,6 +7,7 @@ namespace SixtyEightPublishers\ImageStorage\Tests\Security; use Mockery; use SixtyEightPublishers\FileStorage\Config\ConfigInterface; use SixtyEightPublishers\ImageStorage\Config\Config; +use SixtyEightPublishers\ImageStorage\Security\KnownModifiers; use SixtyEightPublishers\ImageStorage\Security\SignatureStrategy; use Tester\Assert; use Tester\TestCase; @@ -19,7 +20,7 @@ final class SignatureStrategyTest extends TestCase { public function testTokenShouldBeCreatedAndVerifiedWithEmptyConfig(): void { - $strategy = new SignatureStrategy($this->createConfig(null, null)); + $strategy = new SignatureStrategy($this->createConfig(null, null, false), new KnownModifiers([])); $token = $strategy->createToken('var/www/file.png'); Assert::true(hash_equals( @@ -32,7 +33,7 @@ final class SignatureStrategyTest extends TestCase public function testTokenShouldBeCreatedAndVerifiedWithKeyOption(): void { - $strategy = new SignatureStrategy($this->createConfig(null, 'my_secret')); + $strategy = new SignatureStrategy($this->createConfig(null, 'my_secret', false), new KnownModifiers([])); $token = $strategy->createToken('var/www/file.png'); Assert::true(hash_equals( @@ -45,7 +46,7 @@ final class SignatureStrategyTest extends TestCase public function testTokenShouldBeCreatedAndVerifiedWithAlgorithmOption(): void { - $strategy = new SignatureStrategy($this->createConfig('md5', null)); + $strategy = new SignatureStrategy($this->createConfig('md5', null, false), new KnownModifiers([])); $token = $strategy->createToken('var/www/file.png'); Assert::true(hash_equals( @@ -58,7 +59,7 @@ final class SignatureStrategyTest extends TestCase public function testTokenShouldBeCreatedAndVerifiedWithPathThatStartsWithSlash(): void { - $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret')); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', false), new KnownModifiers([])); $token = $strategy->createToken('/var/www/file.png'); Assert::true(hash_equals( @@ -71,17 +72,61 @@ final class SignatureStrategyTest extends TestCase public function testInvalidTokenShouldNotBeVerified(): void { - $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret')); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', false), new KnownModifiers([])); Assert::false($strategy->verifyToken('invalid_token', 'var/www/file.png')); } + public function testTokenShouldNotBeCreatedForKnownModifiers(): void + { + $knownModifiers = new KnownModifiers(['w:100,h:200' => true]); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', true), $knownModifiers); + + $token = $strategy->createToken('var/www/w:100,h:200/file.png'); + + Assert::null($token); + } + + public function testTokenShouldBeCreatedForUnknownModifiersEvenWhenDisabledOnKnown(): void + { + $knownModifiers = new KnownModifiers(['w:100,h:200' => true]); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', true), $knownModifiers); + + $token = $strategy->createToken('var/www/w:150,h:200/file.png'); + + Assert::notNull($token); + Assert::true(hash_equals( + $token, + hash_hmac('sha256', 'var/www/w:150,h:200/file.png', 'my_secret'), + )); + } + + public function testKnownModifiersShouldBeVerifiedWithoutToken(): void + { + $knownModifiers = new KnownModifiers(['w:100,h:200' => true]); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', true), $knownModifiers); + + Assert::true($strategy->verifyToken('', 'var/www/w:100,h:200/file.png')); + Assert::true($strategy->verifyToken('invalid_token', 'var/www/w:100,h:200/file.png')); + } + + public function testTokenShouldBeCreatedForKnownModifiersWhenNotDisabled(): void + { + $knownModifiers = new KnownModifiers(['w:100,h:200' => true]); + $strategy = new SignatureStrategy($this->createConfig('sha256', 'my_secret', false), $knownModifiers); + + $token = $strategy->createToken('var/www/w:100,h:200/file.png'); + + Assert::notNull($token); + Assert::true($strategy->verifyToken($token, 'var/www/w:100,h:200/file.png')); + } + protected function tearDown(): void { Mockery::close(); } - private function createConfig(?string $algo, ?string $key): ConfigInterface + private function createConfig(?string $algo, ?string $key, bool $disableOnKnown): ConfigInterface { $config = Mockery::mock(ConfigInterface::class); @@ -93,6 +138,10 @@ final class SignatureStrategyTest extends TestCase ->with(Config::SIGNATURE_KEY) ->andReturn($key); + $config->shouldReceive('offsetGet') + ->with(Config::DISABLE_SIGNATURE_ON_KNOWN_MODIFIERS) + ->andReturn($disableOnKnown); + return $config; } }