diff --git a/composer.json b/composer.json index 1078155..c18a221 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "codeinc/assets-middleware", - "version": "2.0.5", + "version": "2.1.0", "description": "A PSR-15 middleware to server static assets (CSS, JS, images, etc.)", "homepage": "https://github.com/CodeIncHQ/AssetsMiddleware", "type": "library", @@ -15,7 +15,8 @@ "codeinc/psr7-responses": "^2", "matthiasmullie/minify": "^1.3", "codeinc/media-types": "^1.0", - "enshrined/svg-sanitize": "^0.8.2" + "enshrined/svg-sanitize": "^0.8.2", + "codeinc/collection-interface": "^1.1" }, "require-dev": { "phpunit/phpunit": "^7", diff --git a/src/Assets/AssetInterface.php b/src/Assets/AssetInterface.php new file mode 100644 index 0000000..3b8e28b --- /dev/null +++ b/src/Assets/AssetInterface.php @@ -0,0 +1,75 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Assets; +use Psr\Http\Message\StreamInterface; + + +/** + * Interface AssetInterface + * + * @package CodeInc\AssetsMiddleware + * @author Joan Fabrégat + */ +interface AssetInterface +{ + /** + * Returns the asset's filename. + * + * @return string + */ + public function getFilename():string; + + /** + * Returns the asset's size or NULL if unknown. + * + * @return int|null + */ + public function getSize():?int; + + /** + * Returns the last modification time or NULL if unknown. + * + * @return \DateTime|null + */ + public function getMTime():?\DateTime; + + /** + * Verifies if the assets must be downloaded as an attachment. + * + * @return bool + */ + public function asAttachment():bool; + + /** + * Returns the assets media type or NULL if unknown. + * + * @return string + */ + public function getMediaType():string; + + /** + * Returns a stream to the assets interface. + * + * @return StreamInterface + */ + public function getContent():StreamInterface; +} \ No newline at end of file diff --git a/src/Assets/LocalAsset.php b/src/Assets/LocalAsset.php new file mode 100644 index 0000000..a6d46d1 --- /dev/null +++ b/src/Assets/LocalAsset.php @@ -0,0 +1,104 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Assets; +use CodeInc\AssetsMiddleware\Exceptions\AssetReadException; +use function GuzzleHttp\Psr7\stream_for; +use Psr\Http\Message\StreamInterface; + + +/** + * Class LocalAsset + * + * @package CodeInc\AssetsMiddleware + * @author Joan Fabrégat + */ +class LocalAsset extends StreamAsset +{ + /** + * @var string + */ + private $path; + + /** + * StreamAsset constructor. + * + * @param string $path + * @param null|string $filename + * @param bool $asAttachment + * @param null|string $mediaType + * @param int|null $size + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException + * @throws AssetReadException + */ + public function __construct(string $path, ?string $filename = null, bool $asAttachment = false, + ?string $mediaType = null, ?int $size = null) + { + $this->path = $path; + parent::__construct( + $this->getPathStream($path), + $filename ?? basename($path), + $this->readMTime($path), + $asAttachment, + $mediaType, + $size + ); + } + + /** + * Returns the local asset's path. + * + * @return string + */ + public function getPath():string + { + return $this->path; + } + + /** + * Returns the last modification time for the given path. + * + * @param string $path + * @return \DateTime|null + */ + private function readMTime(string $path):?\DateTime + { + if (($mTime = filemtime($path)) !== false) { + return new \DateTime('@'.$mTime); + } + return null; + } + + /** + * Returns the stream for the given path. + * + * @param string $path + * @return StreamInterface + * @throws AssetReadException + */ + private function getPathStream(string $path):StreamInterface + { + if (($f = fopen($path, 'r')) === false) { + throw new AssetReadException($path); + } + return stream_for($f); + } +} \ No newline at end of file diff --git a/src/Assets/StreamAsset.php b/src/Assets/StreamAsset.php new file mode 100644 index 0000000..17cffa1 --- /dev/null +++ b/src/Assets/StreamAsset.php @@ -0,0 +1,145 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Assets; +use CodeInc\MediaTypes\MediaTypes; +use Psr\Http\Message\StreamInterface; + + +/** + * Class LocalAsset + * + * @package CodeInc\AssetsMiddleware + * @author Joan Fabrégat + */ +class StreamAsset implements AssetInterface +{ + /** + * @var null|string + */ + private $filename; + + /** + * @var int|null + */ + private $size; + + /** + * @var bool + */ + private $asAttachment; + + /** + * @var string + */ + private $mediaType; + + /** + * @var \GuzzleHttp\Psr7\Stream + */ + private $content; + + /** + * @var \DateTime + */ + private $mTime; + + /** + * StreamAsset constructor. + * + * @param StreamInterface $stream + * @param null|string $filename + * @param \DateTime|null $mTime + * @param bool $asAttachment + * @param null|string $mediaType + * @param int|null $size + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException + */ + public function __construct(StreamInterface $stream, string $filename, ?\DateTime $mTime = null, + bool $asAttachment = false, ?string $mediaType = null, ?int $size = null) + { + $this->filename = $filename; + $this->asAttachment = $asAttachment; + $this->mTime = $mTime; + $this->mediaType = $mediaType ?? MediaTypes::getFilenameMediaType($filename) ?? 'application/octet-stream'; + $this->size = $size ?? $stream->getSize(); + $this->content = $stream; + } + + /** + * Returns the asset's filename. + * + * @return string + */ + public function getFilename():string + { + return $this->filename; + } + + /** + * Returns the asset's size or NULL if unknown. + * + * @return int|null + */ + public function getSize():?int + { + return $this->size; + } + + /** + * Verifies if the assets must be downloaded as an attachment. + * + * @return bool + */ + public function asAttachment():bool + { + return $this->asAttachment; + } + + /** + * Returns the assets media type or NULL if unknown. + * + * @return string + */ + public function getMediaType():string + { + return $this->mediaType; + } + + /** + * Returns a stream to the assets interface. + * + * @return StreamInterface + */ + public function getContent():StreamInterface + { + return $this->content; + } + + /** + * @inheritdoc + * @return \DateTime|null + */ + public function getMTime():?\DateTime + { + return $this->mTime; + } +} \ No newline at end of file diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index 342c307..1d61d0f 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -21,17 +21,16 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use CodeInc\AssetsMiddleware\Exceptions\InvalidAssetMediaTypeException; -use CodeInc\AssetsMiddleware\Exceptions\InvalidAssetPathException; -use CodeInc\AssetsMiddleware\Exceptions\EmptyDirectoryKeyException; -use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; use CodeInc\AssetsMiddleware\Exceptions\ResponseErrorException; +use CodeInc\AssetsMiddleware\Resolvers\AssetResolverInterface; use CodeInc\AssetsMiddleware\Responses\AssetResponse; use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; -use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; +use CodeInc\AssetsMiddleware\Responses\AssetMinifiedResponse; use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; -use CodeInc\MediaTypes\MediaTypes; use Micheh\Cache\CacheUtil; +use function PHPSTORM_META\elementType; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -49,9 +48,9 @@ class AssetsMiddleware implements MiddlewareInterface { /** - * @var array + * @var AssetResolverInterface */ - private $assetsDirectories = []; + private $resolver; /** * Base assets URI path. @@ -72,7 +71,7 @@ class AssetsMiddleware implements MiddlewareInterface * * @var bool */ - private $minimizeAssets; + private $minifyAssets; /** * Limits the allowed assets media types. @@ -84,44 +83,20 @@ class AssetsMiddleware implements MiddlewareInterface /** * AssetsMiddleware constructor. * + * @param AssetResolverInterface $resolver * @param string $assetsUriPrefix Base assets URI path * @param bool $cacheAssets Allows the assets to the cached in the web browser - * @param bool $minimizeAssets Minimizes the assets before sending them (@see AssetCompressedResponse) + * @param bool $minifyAssets Minimizes the assets before sending them (@see AssetCompressedResponse) */ - public function __construct(string $assetsUriPrefix, bool $cacheAssets = true, - bool $minimizeAssets = false) + public function __construct(AssetResolverInterface $resolver, string $assetsUriPrefix, + bool $cacheAssets = true, bool $minifyAssets = false) { + $this->resolver = $resolver; $this->assetsUriPrefix = $assetsUriPrefix; $this->cacheAssets = $cacheAssets; - $this->minimizeAssets = $minimizeAssets; + $this->minifyAssets = $minifyAssets; } - /** - * Adds an assets directory - * - * @param string $directoryPath - * @param string|null $directoryKey - */ - public function addAssetsDirectory(string $directoryPath, string $directoryKey = null):void - { - if (!is_dir($directoryPath) || ($directoryPath = realpath($directoryPath)) === false) { - throw new NotADirectoryException($directoryPath); - } - if ($directoryKey !== null && empty($directoryKey)) { - throw new EmptyDirectoryKeyException($directoryPath); - } - $this->assetsDirectories[$directoryKey ?? hash('sha1', $directoryPath)] = $directoryPath; - } - - /** - * @inheritdoc - * @return iterable - */ - protected function getAssetsDirectories():iterable - { - return $this->assetsDirectories; - } - /** * Sets the allowed media types for the assets. The comparison supports shell patterns with operators * like *, ?, etc. @@ -135,6 +110,14 @@ public function setAllowMediaTypes(iterable $allowMediaTypes):void : $allowMediaTypes; } + /** + * @return null|string[] + */ + public function getAllowedMediaTypes():?array + { + return $this->allowedMediaTypes; + } + /** * @inheritdoc * @param ServerRequestInterface $request @@ -143,85 +126,54 @@ public function setAllowMediaTypes(iterable $allowMediaTypes):void */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface { - // if the requests points toward an assets directory - if (preg_match('#^'.preg_quote($this->assetsUriPrefix, '#').'([^/]+)/(.+)$#i', - $request->getUri()->getPath(), $matches)) { - - // searching for the corresponding assets directory - foreach ($this->getAssetsDirectories() as $directoryKey => $directoryPath) { - - // if a match is found - if ($matches[1] == $directoryKey) { - if (($realDirectoryPath = realpath($directoryPath)) === false) { - throw new NotADirectoryException($directoryPath); - } + $reqUriPath = $request->getUri()->getPath(); + if (preg_match('/^'.preg_quote($this->assetsUriPrefix, '/').'.+$/ui', $reqUriPath) + && ($asset = $this->resolver->getAsset($reqUriPath)) !== null) + { + try { + // checking the asset's media type + if (!$this->isMediaTypeAllowed($asset)) { + throw new InvalidAssetMediaTypeException($asset); + } - // validating the assets location - $assetPath = realpath($directoryPath.DIRECTORY_SEPARATOR.$matches[2]); - if ($assetPath && substr($assetPath, 0, strlen($realDirectoryPath)) == $realDirectoryPath) - { - return $this->buildAssetResponse($assetPath, $request); + // building the response + $response = $this->minifyAssets + ? new AssetMinifiedResponse($asset) + : new AssetResponse($asset); + + // enabling cache + if ($this->cacheAssets && ($mTime = $asset->getMTime()) !== null) { + $cache = new CacheUtil(); + $response = $cache->withCache($response, true, 3600); + $response = $cache->withETag($response, hash('sha1', (string)$mTime->getTimestamp())); + $response = $cache->withLastModified($response, $mTime); + if ($cache->isNotModified($request, $response)) { + $response = new NotModifiedAssetResponse($asset); } + return $response; } + return $response; + } + catch (\Throwable $exception) { + throw new ResponseErrorException($asset, 0, $exception); } } return $handler->handle($request); } - /** - * Builds and returns the asset's PSR-7 response. - * - * @param string $assetPath - * @param ServerRequestInterface $request - * @return AssetResponseInterface - */ - private function buildAssetResponse(string $assetPath, ServerRequestInterface $request):AssetResponseInterface - { - try { - // reading the assets media type - $assetMediaType = MediaTypes::getFilenameMediaType($assetPath); - - // checking the asset's media type - if (!$this->isMediaTypeAllowed($assetMediaType)) { - throw new InvalidAssetMediaTypeException($assetPath, $assetMediaType); - } - - // building the response - $response = $this->minimizeAssets - ? new MinifiedAssetResponse($assetPath, $assetMediaType) : - new AssetResponse($assetPath, $assetMediaType); - - // enabling cache - if ($this->cacheAssets) { - $assetMTime = filemtime($assetPath); - $cache = new CacheUtil(); - $response = $cache->withCache($response, true, 3600); - $response = $cache->withETag($response, hash('sha1', (string)$assetMTime)); - $response = $cache->withLastModified($response, $assetMTime); - if ($cache->isNotModified($request, $response)) { - $response = new NotModifiedAssetResponse($assetPath); - } - return $response; - } - return $response; - } - catch (\Throwable $exception) { - throw new ResponseErrorException($assetPath, 0, $exception); - } - } /** * Verifies if the assets media type is supported. * - * @param string $assetMediaType + * @param AssetInterface $asset * @return bool */ - protected function isMediaTypeAllowed(string $assetMediaType):bool + protected function isMediaTypeAllowed(AssetInterface $asset):bool { - if (is_array($this->allowedMediaTypes) && !empty($this->allowedMediaTypes)) { - foreach ($this->allowedMediaTypes as $mediaType) { - if (strcasecmp($assetMediaType, $mediaType) === 0 || fnmatch($mediaType, $assetMediaType)) { + if (is_array($this->allowedMediaTypes)) { + foreach ($this->allowedMediaTypes as $allowedMediaType) { + if (fnmatch($allowedMediaType, $asset->getMediaType())) { return true; } } @@ -231,22 +183,12 @@ protected function isMediaTypeAllowed(string $assetMediaType):bool } /** - * Returns the public URI for a given asset. The asset must be within a registered assets directory. + * Returns the assets resolver in use. * - * @param string $assetPath - * @return string + * @return AssetResolverInterface */ - public function getAssetUri(string $assetPath):?string + public function getResolver():AssetResolverInterface { - if (($realAssetPath = realpath($assetPath)) === false) { - throw new InvalidAssetPathException($assetPath); - } - foreach ($this->getAssetsDirectories() as $directoryKey => $directoryPath) { - if (substr($assetPath, 0, strlen($directoryPath)) == $directoryPath) { - return $this->assetsUriPrefix.urlencode($directoryKey).'/' - .str_replace('\\', '/', substr($assetPath, strlen($directoryPath) + 1)); - } - } - return null; + return $this->resolver; } } \ No newline at end of file diff --git a/src/Exceptions/InvalidAssetPathException.php b/src/Exceptions/AssetReadException.php similarity index 74% rename from src/Exceptions/InvalidAssetPathException.php rename to src/Exceptions/AssetReadException.php index a32e743..72c4e72 100644 --- a/src/Exceptions/InvalidAssetPathException.php +++ b/src/Exceptions/AssetReadException.php @@ -15,7 +15,7 @@ // +---------------------------------------------------------------------+ // // Author: Joan Fabrégat -// Date: 28/09/2018 +// Date: 09/10/2018 // Project: AssetsMiddleware // declare(strict_types=1); @@ -24,30 +24,30 @@ /** - * Class InvalidAssetPathException + * Class AssetReadException * * @package CodeInc\AssetsMiddleware\Exceptions * @author Joan Fabrégat */ -class InvalidAssetPathException extends \RuntimeException implements AssetsMiddlewareException +class AssetReadException extends \RuntimeException implements AssetsMiddlewareException { /** * @var string */ - private $assetPath; + private $path; /** - * InvalidAssetPathException constructor. + * AssetReadException constructor. * - * @param string $assetPath + * @param string $path * @param int $code * @param Throwable|null $previous */ - public function __construct(string $assetPath, int $code = 0, Throwable $previous = null) + public function __construct(string $path, int $code = 0, Throwable $previous = null) { - $this->assetPath = $assetPath; + $this->path = $path; parent::__construct( - sprintf("The asset path '%s' is not valid.", $assetPath), + sprintf("Unable to open the file '%s' for reading.", $path), $code, $previous ); @@ -56,8 +56,8 @@ public function __construct(string $assetPath, int $code = 0, Throwable $previou /** * @return string */ - public function getAssetPath():string + public function getPath():string { - return $this->assetPath; + return $this->path; } } \ No newline at end of file diff --git a/src/Exceptions/InvalidAssetMediaTypeException.php b/src/Exceptions/InvalidAssetMediaTypeException.php index e4843cb..780d893 100644 --- a/src/Exceptions/InvalidAssetMediaTypeException.php +++ b/src/Exceptions/InvalidAssetMediaTypeException.php @@ -20,6 +20,7 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Exceptions; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use Throwable; @@ -32,47 +33,33 @@ class InvalidAssetMediaTypeException extends \RuntimeException implements AssetsMiddlewareException { /** - * @var string + * @var AssetInterface */ - private $assetPath; - - /** - * @var string - */ - private $mediaType; + private $asset; /** * InvalidAssetMediaTypeException constructor. * - * @param string $assetPath - * @param string $mediaType + * @param AssetInterface $asset * @param int $code * @param Throwable|null $previous */ - public function __construct(string $assetPath, string $mediaType, int $code = 0, Throwable $previous = null) + public function __construct(AssetInterface $asset, int $code = 0, Throwable $previous = null) { - $this->assetPath = $assetPath; - $this->mediaType = $mediaType; + $this->asset = $asset; parent::__construct( - sprintf("The media type '%s' of the asset '%s' is not allowed.", $mediaType, $assetPath), + sprintf("The media type '%s' is not allowed for the asset '%s'.", + $asset->getMediaType(), $asset->getFilename()), $code, $previous ); } /** - * @return string - */ - public function getAssetPath():string - { - return $this->assetPath; - } - - /** - * @return string + * @return AssetInterface */ - public function getMediaType():string + public function getAsset():AssetInterface { - return $this->mediaType; + return $this->asset; } } \ No newline at end of file diff --git a/src/Exceptions/NotADirectoryException.php b/src/Exceptions/NotADirectoryException.php index c7302de..029b450 100644 --- a/src/Exceptions/NotADirectoryException.php +++ b/src/Exceptions/NotADirectoryException.php @@ -47,7 +47,7 @@ public function __construct(string $path, int $code = 0, Throwable $previous = n { $this->path = $path; parent::__construct( - sprintf("The assets path '%s' is not a directory or does not exist.", $path), + sprintf("The path '%s' is not a directory or does not exist.", $path), $code, $previous ); diff --git a/src/Exceptions/NotAnAssetException.php b/src/Exceptions/NotAnAssetException.php new file mode 100644 index 0000000..a534a20 --- /dev/null +++ b/src/Exceptions/NotAnAssetException.php @@ -0,0 +1,63 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class NotAnAssetException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class NotAnAssetException extends \LogicException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $path; + + /** + * NotAnAssetException constructor. + * + * @param string $path + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $path, int $code = 0, Throwable $previous = null) + { + $this->path = $path; + parent::__construct( + sprintf("The asset '%s' does not exist.", $path), + $code, + $previous + ); + } + + /** + * @return string + */ + public function getPath():string + { + return $this->path; + } +} \ No newline at end of file diff --git a/src/Exceptions/NotAnAssetResolverException.php b/src/Exceptions/NotAnAssetResolverException.php new file mode 100644 index 0000000..fc3e7d1 --- /dev/null +++ b/src/Exceptions/NotAnAssetResolverException.php @@ -0,0 +1,64 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class NotAnAssetResolverException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class NotAnAssetResolverException extends \LogicException implements AssetsMiddlewareException +{ + /** + * @var mixed + */ + private $item; + + /** + * NotAnAssetResolverException constructor. + * + * @param mixed $item + * @param int $code + * @param Throwable|null $previous + */ + public function __construct($item, int $code = 0, Throwable $previous = null) + { + $this->item = $item; + parent::__construct( + sprintf("The item '%s' is not a resolver. All resolvers must implement '%s'.", + is_object($item) ? get_class($item) : (string)$item), + $code, + $previous + ); + } + + /** + * @return mixed + */ + public function getItem() + { + return $this->item; + } +} \ No newline at end of file diff --git a/src/Exceptions/ResponseErrorException.php b/src/Exceptions/ResponseErrorException.php index d30ee3e..fec47df 100644 --- a/src/Exceptions/ResponseErrorException.php +++ b/src/Exceptions/ResponseErrorException.php @@ -20,6 +20,7 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Exceptions; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use Throwable; @@ -32,32 +33,33 @@ class ResponseErrorException extends \RuntimeException implements AssetsMiddlewareException { /** - * @var string + * @var AssetInterface */ - private $assetPath; + private $asset; /** * ResponseErrorException constructor. * - * @param string $assetPath + * @param AssetInterface $asset * @param int $code * @param Throwable|null $previous */ - public function __construct(string $assetPath, int $code = 0, Throwable $previous = null) + public function __construct(AssetInterface $asset, int $code = 0, Throwable $previous = null) { - $this->assetPath = $assetPath; + $this->asset = $asset; parent::__construct( - sprintf("Error while building the PSR-7 response for the asset '%s'.", $assetPath), + sprintf("Error while building the PSR-7 response for the asset '%s'.", + $asset->getFilename()), $code, $previous ); } /** - * @return string + * @return AssetInterface */ - public function getAssetPath():string + public function getAsset():AssetInterface { - return $this->assetPath; + return $this->asset; } } \ No newline at end of file diff --git a/src/Resolvers/AssetResolverAggregator.php b/src/Resolvers/AssetResolverAggregator.php new file mode 100644 index 0000000..e3a0957 --- /dev/null +++ b/src/Resolvers/AssetResolverAggregator.php @@ -0,0 +1,168 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Resolvers; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; +use CodeInc\AssetsMiddleware\Exceptions\NotAnAssetResolverException; +use CodeInc\CollectionInterface\CountableCollectionInterface; + + +/** + * Class ResolverAggregator + * + * @package CodeInc\AssetsMiddleware\Resolvers + * @author Joan Fabrégat + */ +class AssetResolverAggregator implements AssetResolverInterface, CountableCollectionInterface +{ + /** + * @var AssetResolverInterface[] + */ + private $resolvers = []; + + /** + * @var int + */ + private $iteratorPosition = 0; + + /** + * AssetResolverAggregator constructor. + * + * @param iterable|AssetResolverInterface[]|null $resolvers + * @throws NotAnAssetResolverException + */ + public function __construct(?iterable $resolvers = null) + { + if ($resolvers) { + $this->addResolvers($resolvers); + } + } + + /** + * Adds multiple resolvers. + * + * @param iterable|AssetResolverInterface[] $resolvers + * @throws NotAnAssetResolverException + */ + public function addResolvers(iterable $resolvers):void + { + foreach ($resolvers as $resolver) { + if (!$resolver instanceof AssetResolverInterface) { + throw new NotAnAssetResolverException($resolver); + } + $this->addResolver($resolver); + } + } + + /** + * Adds a resolver. + * + * @param AssetResolverInterface $resolver + */ + public function addResolver(AssetResolverInterface $resolver):void + { + $this->resolvers[] = $resolver; + } + + /** + * Returns the asset corresponding to a given route. + * + * @param string $assetUri + * @return AssetInterface|null + */ + public function getAsset(string $assetUri):?AssetInterface + { + foreach ($this->resolvers as $resolver) { + if (($asset = $resolver->getAsset($assetUri)) !== null) { + return $asset; + } + } + return null; + } + + /** + * Returns the URI of an asset. + * + * @param string $assetPath + * @return null|string + */ + public function getAssetUri(string $assetPath):?string + { + foreach ($this->resolvers as $resolver) { + if (($uri = $resolver->getAssetUri($assetPath)) !== null) { + return $uri; + } + } + return null; + } + + /** + * @inheritdoc + */ + public function rewind():void + { + $this->iteratorPosition = 0; + } + + /** + * @inheritdoc + */ + public function next():void + { + $this->iteratorPosition++; + } + + /** + * @inheritdoc + * @return bool + */ + public function valid():bool + { + return array_key_exists($this->iteratorPosition, $this->resolvers); + } + + /** + * @inheritdoc + * @return AssetResolverInterface + */ + public function current():AssetResolverInterface + { + return $this->resolvers[$this->iteratorPosition]; + } + + /** + * @inheritdoc + * @return int + */ + public function key():int + { + return $this->iteratorPosition; + } + + /** + * @inheritdoc + * @return int + */ + public function count():int + { + return count($this->resolvers); + } +} \ No newline at end of file diff --git a/src/Resolvers/AssetResolverInterface.php b/src/Resolvers/AssetResolverInterface.php new file mode 100644 index 0000000..c825816 --- /dev/null +++ b/src/Resolvers/AssetResolverInterface.php @@ -0,0 +1,49 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Resolvers; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; + + +/** + * Interface AssetResolverInterface + * + * @package CodeInc\AssetsMiddleware\Resolvers + * @author Joan Fabrégat + */ +interface AssetResolverInterface +{ + /** + * Returns the asset corresponding to a given route. + * + * @param string $assetUri + * @return AssetInterface|null + */ + public function getAsset(string $assetUri):?AssetInterface; + + /** + * Returns the URI of an asset. + * + * @param string $assetPath + * @return null|string + */ + public function getAssetUri(string $assetPath):?string; +} \ No newline at end of file diff --git a/src/Resolvers/AssetsDirectoryResolver.php b/src/Resolvers/AssetsDirectoryResolver.php new file mode 100644 index 0000000..56e54e7 --- /dev/null +++ b/src/Resolvers/AssetsDirectoryResolver.php @@ -0,0 +1,109 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Resolvers; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; +use CodeInc\AssetsMiddleware\Assets\LocalAsset; +use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; + + +/** + * Class AssetsDirectoryResolver + * + * @package CodeInc\AssetsMiddleware\Resolvers + * @author Joan Fabrégat + */ +class AssetsDirectoryResolver implements AssetResolverInterface +{ + /** + * @var string + */ + private $dirPath; + + /** + * @var string + */ + private $uriPrefix; + + /** + * AssetsDirectoryResolver constructor. + * + * @param string $dirPath + * @param string $uriPrefix + * @throws NotADirectoryException + */ + public function __construct(string $dirPath, string $uriPrefix) + { + if (($realDirPath = realpath($dirPath)) === false || !is_dir($realDirPath)) { + throw new NotADirectoryException($dirPath); + } + $this->dirPath = $realDirPath; + $this->uriPrefix = $uriPrefix; + } + + /** + * @return string + */ + public function getDirPath():string + { + return $this->dirPath; + } + + /** + * @return string + */ + public function getUriPrefix():string + { + return $this->uriPrefix; + } + + /** + * @inheritdoc + * @param string $assetUri + * @return AssetInterface|null + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException + */ + public function getAsset(string $assetUri):?AssetInterface + { + if (preg_match('#^'.preg_quote($this->uriPrefix, '#').'(.+)#ui', $assetUri, $matches)) { + $assetPath = $this->dirPath.DIRECTORY_SEPARATOR.$matches[1]; + if (($realAssetPath = realpath($assetPath)) !== false + && substr($realAssetPath, 0, strlen($this->dirPath)) == $this->dirPath) { + return new LocalAsset($realAssetPath); + } + } + return null; + } + + /** + * @inheritdoc + * @param string $assetPath + * @return null|string + */ + public function getAssetUri(string $assetPath):?string + { + if (($realAssetPath = realpath($assetPath)) !== false + && preg_match('#^'.preg_quote($this->dirPath, '#').'(.+)#ui', $realAssetPath, $matches)) { + return $this->uriPrefix.$matches[1]; + } + return null; + } +} \ No newline at end of file diff --git a/src/Resolvers/StaticAssetsResolver.php b/src/Resolvers/StaticAssetsResolver.php new file mode 100644 index 0000000..e953a92 --- /dev/null +++ b/src/Resolvers/StaticAssetsResolver.php @@ -0,0 +1,130 @@ + +// Date: 09/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Resolvers; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; +use CodeInc\AssetsMiddleware\Assets\LocalAsset; +use CodeInc\AssetsMiddleware\Exceptions\NotAnAssetException; + + +/** + * Class StaticAssetsResolver + * + * @package CodeInc\AssetsMiddleware\Resolvers + * @author Joan Fabrégat + */ +class StaticAssetsResolver implements AssetResolverInterface, \IteratorAggregate, \Countable +{ + /** + * @var string[] + */ + private $assets = []; + + /** + * StaticAssetsResolver constructor. + * + * @param iterable|null $assets + * @throws NotAnAssetException + */ + public function __construct(?iterable $assets = null) + { + if ($assets) { + $this->addAssets($assets); + } + } + + /** + * Adds multiple assets. + * + * @param iterable $assets + * @throws NotAnAssetException + */ + public function addAssets(iterable $assets):void + { + foreach ($assets as $uri => $path) { + $this->addAsset($uri, $path); + } + } + + /** + * Adds an asset. + * + * @param string $assetUri + * @param string $assetPath + * @throws NotAnAssetException + */ + public function addAsset(string $assetUri, string $assetPath):void + { + if (($realAssetPath = realpath($assetPath)) === false) { + throw new NotAnAssetException($assetPath); + } + $this->assets[$assetUri] = $realAssetPath; + } + + /** + * @inheritdoc + * @param string $assetUri + * @return AssetInterface|null + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException + */ + public function getAsset(string $assetUri):?AssetInterface + { + if (array_key_exists($assetUri, $this->assets)) { + return new LocalAsset($this->assets[$assetUri]); + } + return null; + } + + /** + * @inheritdoc + * @param string $assetPath + * @return null|string + */ + public function getAssetUri(string $assetPath):?string + { + if (($realAssetPath = realpath($assetPath)) !== false) { + foreach ($this->assets as $uri => $path) { + if ($path == $realAssetPath) { + return $uri; + } + } + } + return null; + } + + /** + * @inheritdoc + * @return \ArrayIterator + */ + public function getIterator():\ArrayIterator + { + return new \ArrayIterator($this->assets); + } + + /** + * @inheritdoc + * @return int + */ + public function count():int + { + return count($this->assets); + } +} \ No newline at end of file diff --git a/src/Responses/MinifiedAssetResponse.php b/src/Responses/AssetMinifiedResponse.php similarity index 55% rename from src/Responses/MinifiedAssetResponse.php rename to src/Responses/AssetMinifiedResponse.php index 6835514..44cc741 100644 --- a/src/Responses/MinifiedAssetResponse.php +++ b/src/Responses/AssetMinifiedResponse.php @@ -21,99 +21,78 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Responses; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use CodeInc\Psr7Responses\FileResponse; use enshrined\svgSanitize\Sanitizer; use function GuzzleHttp\Psr7\stream_for; use MatthiasMullie\Minify; use Psr\Http\Message\StreamInterface; -use RuntimeException; /** - * Class MinifiedAssetResponse + * Class AssetMinifiedResponse * * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ -class MinifiedAssetResponse extends FileResponse implements AssetResponseInterface +class AssetMinifiedResponse extends FileResponse implements AssetResponseInterface { /** - * @var string + * @var AssetInterface */ - private $assetPath; + private $asset; /** - * @var string - */ - private $mediaType; - - /** - * MinifiedAssetResponse constructor. + * AssetMinifiedResponse constructor. * - * @param string $assetPath - * @param string $mediaType + * @param AssetInterface $asset * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - public function __construct(string $assetPath, string $mediaType) + public function __construct(AssetInterface $asset) { - $this->assetPath = $assetPath; - $this->mediaType = $mediaType; + $this->asset = $asset; parent::__construct( - basename($assetPath), - $this->buildStream($assetPath), + $asset->getFilename(), + $this->getAssetMinifiedContent(), 200, '', - $mediaType, - null, - false + $asset->getMediaType(), + $asset->getSize(), + $asset->asAttachment() ); } /** - * @param string $filePath * @return StreamInterface */ - private function buildStream(string $filePath):StreamInterface + private function getAssetMinifiedContent():StreamInterface { - switch ($this->mediaType) { + switch ($this->asset->getMediaType()) { case 'text/css': - $css = new Minify\CSS($filePath); + $css = new Minify\CSS(); + $css->add($this->asset->getContent()->__toString()); $css->setImportExtensions([]); return stream_for($css->minify()); case 'text/javascript': case 'application/javascript': - return stream_for((new Minify\JS($filePath))->minify()); + $js = new Minify\JS(); + $js->add($this->asset->getContent()->__toString()); + return stream_for($js->minify()); case 'image/svg+xml': - $svgContent = file_get_contents($filePath); - if ($svgContent === false) { - throw new RuntimeException( - sprintf("Unable to read the SCF assets file '%s'", $filePath) - ); - } $sanitizer = new Sanitizer(); $sanitizer->minify(true); - return stream_for($sanitizer->sanitize($svgContent)); - - default: - $f = fopen($filePath, 'r'); - if ($f === false) { - throw new RuntimeException( - sprintf("Unable to open the assets file '%s'", $filePath) - ); - } - return stream_for($f); + return stream_for($sanitizer->sanitize($this->asset->getContent()->__toString())); } + return $this->asset->getContent(); } /** - * Returns the asset's path. - * - * @return string + * @return AssetInterface */ - public function getAssetPath():string + public function getAsset():AssetInterface { - return $this->assetPath; + return $this->asset; } } \ No newline at end of file diff --git a/src/Responses/AssetResponse.php b/src/Responses/AssetResponse.php index 981c501..3ee40e7 100644 --- a/src/Responses/AssetResponse.php +++ b/src/Responses/AssetResponse.php @@ -21,8 +21,8 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Responses; -use CodeInc\Psr7Responses\LocalFileResponse; - +use CodeInc\AssetsMiddleware\Assets\AssetInterface; +use CodeInc\Psr7Responses\FileResponse; /** * Class AssetResponse @@ -30,41 +30,39 @@ * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ -class AssetResponse extends LocalFileResponse implements AssetResponseInterface +class AssetResponse extends FileResponse implements AssetResponseInterface { /** - * @var string + * @var AssetInterface */ - private $assetPath; + private $asset; /** * AssetResponse constructor. * - * @param string $assetPath - * @param string $contentType + * @param AssetInterface $asset * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - public function __construct(string $assetPath, string $contentType) + public function __construct(AssetInterface $asset) { - $this->assetPath = $assetPath; + $this->asset = $asset; parent::__construct( - $assetPath, + $asset->getFilename(), + $asset->getContent(), 200, '', - null, - $contentType, - null, - false + $asset->getMediaType(), + $asset->getSize(), + $asset->asAttachment() ); } - /** * @inheritdoc - * @return string + * @return AssetInterface */ - public function getAssetPath():string + public function getAsset():AssetInterface { - return $this->assetPath; + return $this->asset; } } \ No newline at end of file diff --git a/src/Responses/AssetResponseInterface.php b/src/Responses/AssetResponseInterface.php index 71f8347..943b85f 100644 --- a/src/Responses/AssetResponseInterface.php +++ b/src/Responses/AssetResponseInterface.php @@ -20,6 +20,7 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Responses; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use Psr\Http\Message\ResponseInterface; @@ -32,9 +33,9 @@ interface AssetResponseInterface extends ResponseInterface { /** - * Returns the asset's path. + * Returns the asset. * - * @return string + * @return AssetInterface */ - public function getAssetPath():string; + public function getAsset():AssetInterface; } \ No newline at end of file diff --git a/src/Responses/NotModifiedAssetResponse.php b/src/Responses/NotModifiedAssetResponse.php index 127d9d9..b2d760d 100644 --- a/src/Responses/NotModifiedAssetResponse.php +++ b/src/Responses/NotModifiedAssetResponse.php @@ -21,6 +21,7 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Responses; +use CodeInc\AssetsMiddleware\Assets\AssetInterface; use CodeInc\Psr7Responses\NotModifiedResponse; @@ -33,27 +34,27 @@ class NotModifiedAssetResponse extends NotModifiedResponse implements AssetResponseInterface { /** - * @var string + * @var AssetInterface */ - private $assetPath; + private $asset; /** * AssetNotModifiedResponse constructor. * - * @param string $assetPath + * @param AssetInterface $asset */ - public function __construct(string $assetPath) + public function __construct(AssetInterface $asset) { - $this->assetPath = $assetPath; + $this->asset = $asset; parent::__construct(); } /** * @inheritdoc - * @return string + * @return AssetInterface */ - public function getAssetPath():string + public function getAsset():AssetInterface { - return $this->assetPath; + return $this->asset; } } \ No newline at end of file