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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/Bridge/Nette/DI/Config/PresetConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace SixtyEightPublishers\ImageStorage\Bridge\Nette\DI\Config;

final class PresetConfig
{
/** @var array<string, numeric|string|bool> */
public array $modifiers = [];

/** @var array<int> */
public array $w = [];

/** @var list<int|float> */
public array $x = [];

public int|null $defaultW = null;

public int|float|null $defaultX = null;
}
2 changes: 1 addition & 1 deletion src/Bridge/Nette/DI/Config/StorageConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class StorageConfig
/** @var array<string, string> */
public array $no_image_patterns;

/** @var array<string, array<string, scalar>> */
/** @var array<string, PresetConfig> */
public array $presets;

/** @var array<Statement> */
Expand Down
140 changes: 137 additions & 3 deletions src/Bridge/Nette/DI/ImageStorageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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([]),

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -506,4 +552,92 @@ private function registerImageServerPresenter(): void

$this->imageServerPresenterRegistered = true;
}

/**
* @return array<string, true>
*/
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,
);
Comment thread
tg666 marked this conversation as resolved.

$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,
);
}
}
2 changes: 2 additions & 0 deletions src/Config/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 => [],
Expand Down
2 changes: 1 addition & 1 deletion src/FileInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion src/FileInfoInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
7 changes: 1 addition & 6 deletions src/ImageServer/LocalImageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
2 changes: 1 addition & 1 deletion src/ImageStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
54 changes: 46 additions & 8 deletions src/LinkGenerator/LinkGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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]);
}

Expand All @@ -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);
}
Expand All @@ -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__,
),
);
}
}
Loading