From 3a5a19ccfe8bf47c836c473ace32d2396b82f4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Mon, 17 Sep 2018 11:37:42 +0200 Subject: [PATCH 01/11] dev v2 --- composer.json | 4 +- src/Assets/AssetCompressedResponse.php | 56 ++---- src/Assets/AssetNotModifiedResponse.php | 20 +- src/Assets/AssetResponse.php | 26 +-- src/Assets/AssetResponseInterface.php | 11 +- src/AssetsMiddleware.php | 234 +++++++++++++++++------- tests/AssetsMiddlewareTest.php | 62 ++----- 7 files changed, 224 insertions(+), 189 deletions(-) diff --git a/composer.json b/composer.json index 85301cb..4f27f10 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "license": "MIT", "require": { "php": ">=7.1", + "ext-gettext": "*", "psr/http-message": "^1.0", "psr/http-server-middleware": "^1.0", "psr/http-server-handler": "^1.0", @@ -14,7 +15,8 @@ "codeinc/psr7-responses": "^1.2", "matthiasmullie/minify": "^1.3", "codeinc/media-types": "^1.0", - "enshrined/svg-sanitize": "^0.8.2" + "enshrined/svg-sanitize": "^0.8.2", + "zendframework/zend-validator": "^2.10" }, "require-dev": { "phpunit/phpunit": "^7", diff --git a/src/Assets/AssetCompressedResponse.php b/src/Assets/AssetCompressedResponse.php index 054045c..66627ae 100644 --- a/src/Assets/AssetCompressedResponse.php +++ b/src/Assets/AssetCompressedResponse.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Assets; use CodeInc\MediaTypes\MediaTypes; -use CodeInc\Psr7Responses\StreamResponse; +use CodeInc\Psr7Responses\FileResponse; use enshrined\svgSanitize\Sanitizer; use function GuzzleHttp\Psr7\stream_for; use MatthiasMullie\Minify; @@ -36,61 +36,34 @@ * @package CodeInc\AssetsMiddleware\Assets * @author Joan Fabrégat */ -class AssetCompressedResponse extends StreamResponse implements AssetResponseInterface +class AssetCompressedResponse extends FileResponse implements AssetResponseInterface { /** * @var string */ - private $assetName; + private $assetPath; /** * AssetCompressedResponse constructor. * - * @param string $filePath - * @param string $assetName - * @param null|string $fileName - * @param null|string $mimeType - * @param bool $asAttachment - * @param int $status - * @param array $headers - * @param string $version - * @param null|string $reason + * @param string $assetPath * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - public function __construct(string $filePath, string $assetName, ?string $fileName = null, ?string $mimeType = null, - bool $asAttachment = false, int $status = 200, array $headers = [], - string $version = '1.1', ?string $reason = null) + public function __construct(string $assetPath) { - $this->assetName = $assetName; - - if (!$fileName) { - $fileName = basename($assetName); - } - if (!$mimeType) { - $mimeType = MediaTypes::getFilenameMediaType($fileName); - } - - parent::__construct( - $this->buildStream($mimeType, $filePath), - $mimeType, - null, - $fileName, - $asAttachment, - $status, - $headers, - $version, - $reason - ); + $this->assetPath = $assetPath; + parent::__construct($this->buildStream($assetPath), basename($assetPath), + null, false); } /** - * @param string $mimeType * @param string $filePath * @return StreamInterface + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - private function buildStream(string $mimeType, string $filePath):StreamInterface + private function buildStream(string $filePath):StreamInterface { - switch ($mimeType) { + switch (MediaTypes::getFilenameMediaType($filePath)) { case 'text/css': $css = new Minify\CSS($filePath); $css->setImportExtensions([]); @@ -123,11 +96,12 @@ private function buildStream(string $mimeType, string $filePath):StreamInterface } /** - * @inheritdoc + * Returns the asset's path. + * * @return string */ - public function getAssetName():string + public function getAssetPath():string { - return $this->assetName; + return $this->assetPath; } } \ No newline at end of file diff --git a/src/Assets/AssetNotModifiedResponse.php b/src/Assets/AssetNotModifiedResponse.php index f9b5d6c..c647fcc 100644 --- a/src/Assets/AssetNotModifiedResponse.php +++ b/src/Assets/AssetNotModifiedResponse.php @@ -35,31 +35,25 @@ class AssetNotModifiedResponse extends Response implements AssetResponseInterfac /** * @var string */ - private $assetName; + private $assetPath; /** * AssetNotModifiedResponse constructor. * - * @param string $assetName - * @param int $status - * @param array $headers - * @param null $body - * @param string $version - * @param null|string $reason + * @param string $assetPath */ - public function __construct(string $assetName, int $status = 304, array $headers = [], $body = null, - string $version = '1.1', ?string $reason = null) + public function __construct(string $assetPath) { - $this->assetName = $assetName; - parent::__construct($status, $headers, $body, $version, $reason); + $this->assetPath = $assetPath; + parent::__construct(304); } /** * @inheritdoc * @return string */ - public function getAssetName():string + public function getAssetPath():string { - return $this->assetName; + return $this->assetPath; } } \ No newline at end of file diff --git a/src/Assets/AssetResponse.php b/src/Assets/AssetResponse.php index f910a6a..623fad4 100644 --- a/src/Assets/AssetResponse.php +++ b/src/Assets/AssetResponse.php @@ -35,37 +35,27 @@ class AssetResponse extends FileResponse implements AssetResponseInterface /** * @var string */ - private $assetName; + private $assetPath; /** * AssetResponse constructor. * - * @param string $filePath - * @param string $assetName - * @param null|string $fileName - * @param null|string $mimeType - * @param bool $asAttachment - * @param int $status - * @param array $headers - * @param string $version - * @param null|string $reason + * @param string $assetPath * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ - public function __construct(string $filePath, string $assetName, ?string $fileName = null, - ?string $mimeType = null, bool $asAttachment = false, int $status = 200, array $headers = [], - string $version = '1.1', ?string $reason = null) + public function __construct(string $assetPath) { - $this->assetName = $assetName; - parent::__construct($filePath, $fileName, $mimeType, $asAttachment, $status, $headers, $version, $reason); + $this->assetPath = $assetPath; + parent::__construct($assetPath, basename($assetPath), null, false); } + /** * @inheritdoc * @return string */ - public function getAssetName():string + public function getAssetPath():string { - return $this->assetName; + return $this->assetPath; } } \ No newline at end of file diff --git a/src/Assets/AssetResponseInterface.php b/src/Assets/AssetResponseInterface.php index 668f066..9d75683 100644 --- a/src/Assets/AssetResponseInterface.php +++ b/src/Assets/AssetResponseInterface.php @@ -3,20 +3,19 @@ // +---------------------------------------------------------------------+ // | CODE INC. SOURCE CODE | // +---------------------------------------------------------------------+ -// | Copyright (c) 2017 - Code Inc. SAS - All Rights Reserved. | +// | Copyright (c) 2018 - Code Inc. SAS - All Rights Reserved. | // | Visit https://www.codeinc.fr for more information about licensing. | // +---------------------------------------------------------------------+ // | NOTICE: All information contained herein is, and remains the | // | property of Code Inc. SAS. The intellectual and technical concepts | // | contained herein are proprietary to Code Inc. SAS are protected by | // | trade secret or copyright law. Dissemination of this information or | -// | reproduction of this material is strictly forbidden unless prior | +// | reproduction of this material is strictly forbidden unless prior | // | written permission is obtained from Code Inc. SAS. | // +---------------------------------------------------------------------+ // // Author: Joan Fabrégat -// Date: 03/05/2018 -// Time: 17:15 +// Date: 14/09/2018 // Project: AssetsMiddleware // declare(strict_types=1); @@ -33,9 +32,9 @@ interface AssetResponseInterface extends ResponseInterface { /** - * Returns the name of the asset. + * Returns the asset's path. * * @return string */ - public function getAssetName():string; + public function getAssetPath():string; } \ No newline at end of file diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index 58fb5f2..8de7aab 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -3,14 +3,14 @@ // +---------------------------------------------------------------------+ // | CODE INC. SOURCE CODE | // +---------------------------------------------------------------------+ -// | Copyright (c) 2017 - Code Inc. SAS - All Rights Reserved. | +// | Copyright (c) 2018 - Code Inc. SAS - All Rights Reserved. | // | Visit https://www.codeinc.fr for more information about licensing. | // +---------------------------------------------------------------------+ // | NOTICE: All information contained herein is, and remains the | // | property of Code Inc. SAS. The intellectual and technical concepts | // | contained herein are proprietary to Code Inc. SAS are protected by | // | trade secret or copyright law. Dissemination of this information or | -// | reproduction of this material is strictly forbidden unless prior | +// | reproduction of this material is strictly forbidden unless prior | // | written permission is obtained from Code Inc. SAS. | // +---------------------------------------------------------------------+ // @@ -22,6 +22,7 @@ declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; use CodeInc\AssetsMiddleware\Assets\AssetCompressedResponse; +use CodeInc\AssetsMiddleware\Assets\AssetResponseInterface; use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; use CodeInc\AssetsMiddleware\Assets\AssetResponse; use CodeInc\AssetsMiddleware\Test\AssetsMiddlewareTest; @@ -30,6 +31,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Zend\Validator\File\Exists; /** @@ -40,74 +42,72 @@ * @license MIT * @link https://github.com/CodeIncHQ/AssetsMiddleware * @see AssetsMiddlewareTest + * @version 2 */ class AssetsMiddleware implements MiddlewareInterface { /** - * @var string + * Stack of local assets directories. + * + * @see AssetsMiddleware::registerAssetsDirectory() + * @var string[] */ - private $assetsLocalPath; + private $assetsDirectories = []; /** + * Base assets URI path. + * * @var string */ - private $assetsUriPath; + private $assetsUri; /** + * Allows the assets to the cached in the web browser. + * * @var bool */ private $allowAssetsCache; /** + * Allows the assets to be minimized. + * * @var bool */ - private $allowAssetsCompression; + private $allowAssetsMinimization; /** * AssetsMiddleware constructor. * - * @param string $assetsLocalPath - * @param string $assetsUriPath - * @param bool $allowAssetsCache Allows assets cache through HTTP headers - * @param bool $allowAssetsCompression Compresses CSS, JS and SVG files - * @throws AssetsMiddlewareException + * @param string $assetsUri Base assets URI path + * @param bool $allowAssetsCache Allows the assets to the cached in the web browser + * @param bool $allowAssetsMinimization Allows the assets to be minimized */ - public function __construct(string $assetsLocalPath, string $assetsUriPath, - bool $allowAssetsCache = true, bool $allowAssetsCompression = false) + public function __construct(string $assetsUri, bool $allowAssetsCache = true, + bool $allowAssetsMinimization = false) { - if (!is_dir($assetsLocalPath) || ($assetsLocalPath = realpath($assetsLocalPath)) === null) { - throw new AssetsMiddlewareException( - sprintf("%s is not a directory and can not be used as assets source", $assetsLocalPath), - $this - ); - } - $this->assetsLocalPath = $assetsLocalPath; - $this->assetsUriPath = $assetsUriPath; + $this->assetsUri = $assetsUri; $this->allowAssetsCache = $allowAssetsCache; - $this->allowAssetsCompression = $allowAssetsCompression; + $this->allowAssetsMinimization = $allowAssetsMinimization; } /** * @inheritdoc * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler - * @return ResponseInterface - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException + * @return ResponseInterface|AssetResponseInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface { // if the response points toward a valid asset - if (($assetName = $this->getAssetName($request)) !== null) { - $assetPath = $this->getAssetPath($assetName); - if (file_exists($assetPath)) { - + if ($assetPath = $this->getAssetPath($request)) + { + try { // builds the response - if (!$this->allowAssetsCompression) { - $response = new AssetResponse($assetPath, $assetName); + if (!$this->allowAssetsMinimization) { + $response = new AssetResponse($assetPath); } else { - $response = new AssetCompressedResponse($assetPath, $assetName); + $response = new AssetCompressedResponse($assetPath); } // enables the cache @@ -118,12 +118,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $response = $cache->withETag($response, hash('sha1', (string)$assetMTime)); $response = $cache->withLastModified($response, $assetMTime); if ($cache->isNotModified($request, $response)) { - return new AssetNotModifiedResponse($assetName); + return new AssetNotModifiedResponse($assetPath); } } return $response; } + catch (\Throwable $exception) { + throw new \RuntimeException(sprintf("Error while building the PSR-7 response " + ."for the asset request '%s'", $request->getUri()->getPath())); + } } // returns the handler response @@ -131,74 +135,178 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } /** - * @return string + * Returns the asset path corresponding to a request. + * + * @param ServerRequestInterface $request + * @return null|string */ - public function getAssetsLocalPath():string + protected function getAssetPath(ServerRequestInterface $request):?string { - return $this->assetsLocalPath; + // passing the request uri path + if ($parsedUriPath = $this->parseAssetUriPath($request)) { + + // checking the assets parent directory + if ($directoryPath = $this->getAssetsDirectoryPath($parsedUriPath['directoryHash'])) { + + // checking the asset existence + $assetPath = $directoryPath.DIRECTORY_SEPARATOR.$parsedUriPath['assetPath']; + if ((new Exists($directoryPath))->isValid($assetPath)) { + return $assetPath; + } + } + } + return null; } /** - * @return string + * Parsed a request URI path and if the URL is an assets, returns the asset's parent directory hash and path in an + * associative array. Returns NULL if the requests does not points toward an asset. + * + * @param ServerRequestInterface $request + * @return array|null */ - public function getAssetsUriPath():string + protected function parseAssetUriPath(ServerRequestInterface $request):?array { - return $this->assetsUriPath; + if (preg_match('#^'.preg_quote($this->assetsUri, '#').'([a-f0-9]{32})/(.+)$#i', + $request->getUri()->getPath(), $matches)) { + return [ + 'directoryHash' => $matches[1], + 'assetPath' => $matches[2] + ]; + } + return null; } /** - * Enables the assets cache (enabled by default). + * @param string $directoryHash + * @return null|string */ - public function enableAssetsCache():void + protected function getAssetsDirectoryPath(string $directoryHash):?string { - $this->allowAssetsCache = false; + if (($directoryPath = array_search($directoryHash, $this->assetsDirectories)) !== false) { + return $directoryPath; + } + return null; } /** - * Disables the assets cache (enabled by default). + * Registers multiple paths to web assets directories. + * + * Attention : all files in the directories will be publicly available. + * + * @uses AssetsMiddleware::registerAssetsDirectory() + * @param iterable $directories */ - public function disableAssetsCache():void + public function registerAssetsDirectories(iterable $directories):void { - $this->allowAssetsCache = false; + foreach ($directories as $directory) { + $this->registerAssetsDirectory((string)$directory); + } } /** - * Returns an asset's name from a request or null if the request does'nt points toward an asset. + * Registers a path to a web assets directory. * - * @param ServerRequestInterface $request - * @return null|string + * Attention : all files in the directory will be publicly available. + * + * The method returns the base URI of the where the web assets within the directory will be available. This URI + * can then be resolved using getAssetUri() and getAssetsDirUri(). + * + * @param string $assetsDirectory + * @return string */ - public function getAssetName(ServerRequestInterface $request):?string + public function registerAssetsDirectory(string $assetsDirectory):string { - if (preg_match('#^'.preg_quote($this->assetsUriPath, '#').'([\\w\\-_./]+)$#ui', - $request->getUri()->getPath(), $matches)) { - return $matches[1]; + if (!is_dir($assetsDirectory) || ($assetsDirectory = realpath($assetsDirectory)) === false) { + throw new \RuntimeException(sprintf(_("The web assets path '%s' is not a directory"), + $assetsDirectory)); } - return null; + if (!is_readable($assetsDirectory)) { + throw new \RuntimeException(sprintf(_("The web assets directory '%s' is not readable"), + $assetsDirectory)); + } + if (isset($this->assetsDirectories[$assetsDirectory])) { + throw new \LogicException(sprintf(_("The web assets directory '%s' is already registered"), + $assetsDirectory)); + } + + $this->assetsDirectories[$assetsDirectory] = md5($assetsDirectory); + + return $this->getAssetsDirectoryUri($assetsDirectory); } /** - * Returns an asset's path. + * Returns the base URI path corresponding to a registered web asset's directory. * - * @param string $assetName + * @param string $assetsDir * @return string */ - public function getAssetPath(string $assetName):string + public function getAssetsDirectoryUri(string $assetsDir):string { - if (substr($assetName, 0, strlen(DIRECTORY_SEPARATOR)) == DIRECTORY_SEPARATOR) { - $assetName = substr($assetName, strlen(DIRECTORY_SEPARATOR)); + if (!isset($this->assetsDirectories[$assetsDir])) { + throw new \RuntimeException(sprintf(_("The assets directory '%s' is no registered"), + $assetsDir)); } - return $this->assetsLocalPath.DIRECTORY_SEPARATOR.$assetName; + return $this->assetsUri.$this->assetsDirectories[$assetsDir].'/'; } /** - * Returns an assets URI path. + * Returns the public URI for a given asset. The asset must be within a registered assets directory. * - * @param string $asset + * @param string $assetPath * @return string */ - public function getAssetUriPath(string $asset):string + public function getAssetUri(string $assetPath):string + { + if (($assetPath = realpath($assetPath)) === false) { + throw new \LogicException(sprintf(_("Unable to read the real path of the asset '%s'"), + $assetPath)); + } + + foreach ($this->assetsDirectories as $assetsDirectory => $directoryUuid) { + if (substr($assetPath, 0, strlen($assetsDirectory)) == $assetsDirectory) { + return $this->getAssetsDirectoryUri($assetsDirectory).substr($assetPath, strlen($assetsDirectory)); + } + } + + // is the asset is not in any directory, throwing an exception + throw new \LogicException(sprintf(_("The asset '%s' is not within any registered assets directory"), + $assetPath)); + } + + /** + * Returns the array of registered assets directories paths. + * + * @return string[] + */ + public function getAssetsDirectories():array { - return $this->assetsUriPath.$asset; + return array_keys($this->assetsDirectories); + } + + /** + * Returns the assets base URI. + * + * @return string + */ + public function getAssetsUri():string + { + return $this->assetsUri; + } + + /** + * Enables the assets cache (enabled by default). + */ + public function enableAssetsCache():void + { + $this->allowAssetsCache = false; + } + + /** + * Disables the assets cache (enabled by default). + */ + public function disableAssetsCache():void + { + $this->allowAssetsCache = false; } } \ No newline at end of file diff --git a/tests/AssetsMiddlewareTest.php b/tests/AssetsMiddlewareTest.php index a283dd8..933d69f 100644 --- a/tests/AssetsMiddlewareTest.php +++ b/tests/AssetsMiddlewareTest.php @@ -21,8 +21,8 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Test; -use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; use CodeInc\AssetsMiddleware\Assets\AssetResponseInterface; +use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; use CodeInc\AssetsMiddleware\AssetsMiddleware; use CodeInc\MiddlewareTestKit\FakeRequestHandler; use CodeInc\MiddlewareTestKit\FakeServerRequest; @@ -46,16 +46,11 @@ final class AssetsMiddlewareTest extends TestCase ]; /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testAssets():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/v2/' - ); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets', true); + $middleware->registerAssetsDirectory('/assets/v2/'); foreach (self::ASSETS as $path => $type) { self::assertFileExists($path); @@ -69,7 +64,6 @@ public function testAssets():void self::assertInstanceOf(ResponseInterface::class, $response); self::assertInstanceOf(AssetResponseInterface::class, $response); - self::assertEquals($response->getAssetName(), basename($path)); self::assertEquals($type, $response->getHeaderLine('Content-Type')); self::assertNotEmpty($response->getHeaderLine('Cache-Control')); self::assertNotEmpty($response->getHeaderLine('ETag')); @@ -79,17 +73,11 @@ public function testAssets():void } /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testUncachedAssets():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/v2/' - ); - $middleware->disableAssetsCache(); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets', false); + $middleware->registerAssetsDirectory('/assets/v2/'); foreach (self::ASSETS as $path => $type) { self::assertFileExists($path); @@ -110,16 +98,11 @@ public function testUncachedAssets():void } /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testNotFoundAsset():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/v2/' - ); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets'); + $middleware->registerAssetsDirectory('/assets/v2/'); $response = $middleware->process( FakeServerRequest::getSecureServerRequestWithPath('/assets/v2/a-not-found-asset.bin'), new FakeRequestHandler() @@ -129,16 +112,11 @@ public function testNotFoundAsset():void } /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testNonAssetRequest():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/v2/' - ); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets'); + $middleware->registerAssetsDirectory('/assets/v2/'); $response = $middleware->process( FakeServerRequest::getSecureServerRequestWithPath('/a-page.html'), new FakeRequestHandler() @@ -148,16 +126,11 @@ public function testNonAssetRequest():void } /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testDateCacheAsset():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/' - ); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets'); + $middleware->registerAssetsDirectory('/assets/'); $request = FakeServerRequest::getSecureServerRequestWithPath('/assets/image.svg') ->withHeader('If-Modified-Since', date('D, d M Y H:i:s \G\M\T')); @@ -167,20 +140,15 @@ public function testDateCacheAsset():void self::assertInstanceOf(ResponseInterface::class, $response); self::assertInstanceOf(AssetNotModifiedResponse::class, $response); - self::assertEquals($response->getAssetName(), 'image.svg'); + self::assertEquals(basename($response->getAssetPath()), 'image.svg'); } /** - * @throws \CodeInc\AssetsMiddleware\AssetsMiddlewareException - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException - * @throws \CodeInc\Psr7Responses\ResponseException */ public function testEtagCacheAsset():void { - $middleware = new AssetsMiddleware( - __DIR__ . '/Assets', - '/assets/' - ); + $middleware = new AssetsMiddleware(__DIR__ . '/Assets'); + $middleware->registerAssetsDirectory('/assets/'); $request = FakeServerRequest::getSecureServerRequestWithPath('/assets/image.svg') ->withHeader('If-None-Match', '"9fda03907099301a7e94f69f6502b3f3805bf1c3"'); @@ -190,6 +158,6 @@ public function testEtagCacheAsset():void self::assertInstanceOf(ResponseInterface::class, $response); self::assertInstanceOf(AssetNotModifiedResponse::class, $response); - self::assertEquals($response->getAssetName(), 'image.svg'); + self::assertEquals(basename($response->getAssetPath()), 'image.svg'); } } \ No newline at end of file From 4d34935f59e3e88fd13a914ac287b7bc80e1ee89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Mon, 17 Sep 2018 11:37:51 +0200 Subject: [PATCH 02/11] v2.0.0-beta.1 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f27f10..4f18656 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "codeinc/assets-middleware", - "version": "1.2.3", + "version": "2.0.0-beta.1", "description": "A PSR-15 middleware to server static assets (CSS, JS, images, etc.)", "homepage": "https://github.com/CodeIncHQ/AssetsMiddleware", "type": "library", From 04834067e742c1c386ff0afddb59cbaef1f4d830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Tue, 25 Sep 2018 11:56:07 +0200 Subject: [PATCH 03/11] code simplification --- src/AbstractAssetsMiddleware.php | 159 ++++++++++ src/AssetsMiddleware.php | 278 ++---------------- src/AssetsMiddlewareException.php | 41 +-- src/{Assets => Responses}/AssetResponse.php | 4 +- .../AssetResponseInterface.php | 4 +- .../MinifiedAssetResponse.php} | 10 +- .../NotModifiedAssetResponse.php} | 6 +- tests/AssetsMiddlewareTest.php | 11 +- 8 files changed, 218 insertions(+), 295 deletions(-) create mode 100644 src/AbstractAssetsMiddleware.php rename src/{Assets => Responses}/AssetResponse.php (95%) rename src/{Assets => Responses}/AssetResponseInterface.php (93%) rename src/{Assets/AssetCompressedResponse.php => Responses/MinifiedAssetResponse.php} (93%) rename src/{Assets/AssetNotModifiedResponse.php => Responses/NotModifiedAssetResponse.php} (91%) diff --git a/src/AbstractAssetsMiddleware.php b/src/AbstractAssetsMiddleware.php new file mode 100644 index 0000000..6b2a083 --- /dev/null +++ b/src/AbstractAssetsMiddleware.php @@ -0,0 +1,159 @@ + +// Date: 05/03/2018 +// Time: 17:15 +// Project: AssetsMiddleware +// +declare(strict_types = 1); +namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; +use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; +use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; +use CodeInc\AssetsMiddleware\Responses\AssetResponse; +use Micheh\Cache\CacheUtil; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Zend\Validator\File\Exists; + + +/** + * Class AbstractAssetsMiddleware + * + * @package CodeInc\AssetsMiddleware + * @author Joan Fabrégat + * @license MIT + * @link https://github.com/CodeIncHQ/AssetsMiddleware + */ +abstract class AbstractAssetsMiddleware implements MiddlewareInterface +{ + /** + * Base assets URI path. + * + * @var string + */ + private $assetsUriPrefix; + + /** + * Allows the assets to the cached in the web browser. + * + * @var bool + */ + private $cacheAssets; + + /** + * Allows the assets to be minimized. + * + * @var bool + */ + private $minimizeAssets; + + /** + * AssetsMiddleware constructor. + * + * @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) + */ + public function __construct(string $assetsUriPrefix, bool $cacheAssets = true, + bool $minimizeAssets = false) + { + $this->assetsUriPrefix = $assetsUriPrefix; + $this->cacheAssets = $cacheAssets; + $this->minimizeAssets = $minimizeAssets; + } + + /** + * Returns the assets directories. Keys must be directories identifiers (often hashes of the path) + * and values directories paths. + * + * @return iterable + */ + abstract protected function getAssetsDirectories():iterable; + + /** + * @inheritdoc + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return AssetResponseInterface + * @throws AssetsMiddlewareException + */ + 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) { + + // validating the assets location + $assetPath = $directoryPath.DIRECTORY_SEPARATOR.$matches[2]; + if ((new Exists($directoryPath))->isValid($assetPath)) { + + try { + // building the response + $response = $this->minimizeAssets + ? new MinifiedAssetResponse($assetPath) : + new AssetResponse($assetPath); + + // 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); + } + } + } + catch (\Throwable $exception) { + throw AssetsMiddlewareException::responseError($assetPath, $exception); + } + + return $response; + } + } + } + } + + return $handler->handle($request); + } + + /** + * Returns the public URI for a given asset. The asset must be within a registered assets directory. + * + * @param string $assetPath + * @return string + */ + public function getAssetUri(string $assetPath):?string + { + foreach ($this->getAssetsDirectories() as $directoryKey => $directoryPath) { + if (substr($assetPath, 0, strlen($directoryPath)) == $directoryPath) { + return $this->assetsUriPrefix.urlencode($directoryKey).'/'.substr($assetPath, strlen($directoryPath)); + } + } + return null; + } +} \ No newline at end of file diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index 8de7aab..6243df7 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -21,17 +21,7 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; -use CodeInc\AssetsMiddleware\Assets\AssetCompressedResponse; -use CodeInc\AssetsMiddleware\Assets\AssetResponseInterface; -use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; -use CodeInc\AssetsMiddleware\Assets\AssetResponse; use CodeInc\AssetsMiddleware\Test\AssetsMiddlewareTest; -use Micheh\Cache\CacheUtil; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Zend\Validator\File\Exists; /** @@ -44,269 +34,37 @@ * @see AssetsMiddlewareTest * @version 2 */ -class AssetsMiddleware implements MiddlewareInterface +class AssetsMiddleware extends AbstractAssetsMiddleware { /** - * Stack of local assets directories. - * - * @see AssetsMiddleware::registerAssetsDirectory() - * @var string[] + * @var array */ private $assetsDirectories = []; /** - * Base assets URI path. - * - * @var string - */ - private $assetsUri; - - /** - * Allows the assets to the cached in the web browser. - * - * @var bool - */ - private $allowAssetsCache; - - /** - * Allows the assets to be minimized. - * - * @var bool - */ - private $allowAssetsMinimization; - - /** - * AssetsMiddleware constructor. + * Adds an assets directory * - * @param string $assetsUri Base assets URI path - * @param bool $allowAssetsCache Allows the assets to the cached in the web browser - * @param bool $allowAssetsMinimization Allows the assets to be minimized + * @param string $directoryPath + * @param string|null $directoryKey + * @throws AssetsMiddlewareException */ - public function __construct(string $assetsUri, bool $allowAssetsCache = true, - bool $allowAssetsMinimization = false) + public function addAssetsDirectory(string $directoryPath, string $directoryKey = null):void { - $this->assetsUri = $assetsUri; - $this->allowAssetsCache = $allowAssetsCache; - $this->allowAssetsMinimization = $allowAssetsMinimization; - } - - /** - * @inheritdoc - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return ResponseInterface|AssetResponseInterface - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface - { - // if the response points toward a valid asset - if ($assetPath = $this->getAssetPath($request)) - { - try { - // builds the response - if (!$this->allowAssetsMinimization) { - $response = new AssetResponse($assetPath); - } - else { - $response = new AssetCompressedResponse($assetPath); - } - - // enables the cache - if ($this->allowAssetsCache) { - $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)) { - return new AssetNotModifiedResponse($assetPath); - } - } - - return $response; - } - catch (\Throwable $exception) { - throw new \RuntimeException(sprintf("Error while building the PSR-7 response " - ."for the asset request '%s'", $request->getUri()->getPath())); - } + if (!is_dir($directoryPath) || ($directoryPath = realpath($directoryPath)) === false) { + throw AssetsMiddlewareException::notADirectory($directoryPath); } - - // returns the handler response - return $handler->handle($request); - } - - /** - * Returns the asset path corresponding to a request. - * - * @param ServerRequestInterface $request - * @return null|string - */ - protected function getAssetPath(ServerRequestInterface $request):?string - { - // passing the request uri path - if ($parsedUriPath = $this->parseAssetUriPath($request)) { - - // checking the assets parent directory - if ($directoryPath = $this->getAssetsDirectoryPath($parsedUriPath['directoryHash'])) { - - // checking the asset existence - $assetPath = $directoryPath.DIRECTORY_SEPARATOR.$parsedUriPath['assetPath']; - if ((new Exists($directoryPath))->isValid($assetPath)) { - return $assetPath; - } - } - } - return null; - } - - /** - * Parsed a request URI path and if the URL is an assets, returns the asset's parent directory hash and path in an - * associative array. Returns NULL if the requests does not points toward an asset. - * - * @param ServerRequestInterface $request - * @return array|null - */ - protected function parseAssetUriPath(ServerRequestInterface $request):?array - { - if (preg_match('#^'.preg_quote($this->assetsUri, '#').'([a-f0-9]{32})/(.+)$#i', - $request->getUri()->getPath(), $matches)) { - return [ - 'directoryHash' => $matches[1], - 'assetPath' => $matches[2] - ]; - } - return null; - } - - /** - * @param string $directoryHash - * @return null|string - */ - protected function getAssetsDirectoryPath(string $directoryHash):?string - { - if (($directoryPath = array_search($directoryHash, $this->assetsDirectories)) !== false) { - return $directoryPath; - } - return null; - } - - /** - * Registers multiple paths to web assets directories. - * - * Attention : all files in the directories will be publicly available. - * - * @uses AssetsMiddleware::registerAssetsDirectory() - * @param iterable $directories - */ - public function registerAssetsDirectories(iterable $directories):void - { - foreach ($directories as $directory) { - $this->registerAssetsDirectory((string)$directory); + if ($directoryKey !== null && empty($directoryKey)) { + throw AssetsMiddlewareException::emptyDirectoryKey($directoryPath); } + $this->assetsDirectories[$directoryKey ?? md5($directoryPath)] = $directoryPath; } /** - * Registers a path to a web assets directory. - * - * Attention : all files in the directory will be publicly available. - * - * The method returns the base URI of the where the web assets within the directory will be available. This URI - * can then be resolved using getAssetUri() and getAssetsDirUri(). - * - * @param string $assetsDirectory - * @return string - */ - public function registerAssetsDirectory(string $assetsDirectory):string - { - if (!is_dir($assetsDirectory) || ($assetsDirectory = realpath($assetsDirectory)) === false) { - throw new \RuntimeException(sprintf(_("The web assets path '%s' is not a directory"), - $assetsDirectory)); - } - if (!is_readable($assetsDirectory)) { - throw new \RuntimeException(sprintf(_("The web assets directory '%s' is not readable"), - $assetsDirectory)); - } - if (isset($this->assetsDirectories[$assetsDirectory])) { - throw new \LogicException(sprintf(_("The web assets directory '%s' is already registered"), - $assetsDirectory)); - } - - $this->assetsDirectories[$assetsDirectory] = md5($assetsDirectory); - - return $this->getAssetsDirectoryUri($assetsDirectory); - } - - /** - * Returns the base URI path corresponding to a registered web asset's directory. - * - * @param string $assetsDir - * @return string - */ - public function getAssetsDirectoryUri(string $assetsDir):string - { - if (!isset($this->assetsDirectories[$assetsDir])) { - throw new \RuntimeException(sprintf(_("The assets directory '%s' is no registered"), - $assetsDir)); - } - return $this->assetsUri.$this->assetsDirectories[$assetsDir].'/'; - } - - /** - * Returns the public URI for a given asset. The asset must be within a registered assets directory. - * - * @param string $assetPath - * @return string - */ - public function getAssetUri(string $assetPath):string - { - if (($assetPath = realpath($assetPath)) === false) { - throw new \LogicException(sprintf(_("Unable to read the real path of the asset '%s'"), - $assetPath)); - } - - foreach ($this->assetsDirectories as $assetsDirectory => $directoryUuid) { - if (substr($assetPath, 0, strlen($assetsDirectory)) == $assetsDirectory) { - return $this->getAssetsDirectoryUri($assetsDirectory).substr($assetPath, strlen($assetsDirectory)); - } - } - - // is the asset is not in any directory, throwing an exception - throw new \LogicException(sprintf(_("The asset '%s' is not within any registered assets directory"), - $assetPath)); - } - - /** - * Returns the array of registered assets directories paths. - * - * @return string[] - */ - public function getAssetsDirectories():array - { - return array_keys($this->assetsDirectories); - } - - /** - * Returns the assets base URI. - * - * @return string - */ - public function getAssetsUri():string - { - return $this->assetsUri; - } - - /** - * Enables the assets cache (enabled by default). - */ - public function enableAssetsCache():void - { - $this->allowAssetsCache = false; - } - - /** - * Disables the assets cache (enabled by default). + * @inheritdoc + * @return iterable */ - public function disableAssetsCache():void - { - $this->allowAssetsCache = false; - } + protected function getAssetsDirectories():iterable + { + return $this->assetsDirectories; + } } \ No newline at end of file diff --git a/src/AssetsMiddlewareException.php b/src/AssetsMiddlewareException.php index 600f40f..749c736 100644 --- a/src/AssetsMiddlewareException.php +++ b/src/AssetsMiddlewareException.php @@ -21,8 +21,6 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware; -use RuntimeException; -use Throwable; /** @@ -31,33 +29,40 @@ * @package CodeInc\AssetsMiddleware * @author Joan Fabrégat */ -class AssetsMiddlewareException extends RuntimeException +class AssetsMiddlewareException extends \Exception { + public const CODE_RESPONSE_ERROR = 1; + public const CODE_NOT_A_DIRECTORY = 2; + public const CODE_EMPTY_DIRECTORY_KEY = 3; + /** - * @var AssetsMiddleware + * @param string $assetPath + * @param \Throwable $error + * @return AssetsMiddlewareException */ - private $assetsMiddleware; + public static function responseError(string $assetPath, \Throwable $error):self + { + return new self(sprintf("Error while building the PSR-7 response for the asset '%s'.", $assetPath), + self::CODE_RESPONSE_ERROR, $error); + } /** - * AssetsMiddlewareException constructor. - * - * @param string $message - * @param AssetsMiddleware $assetsMiddleware - * @param int $code - * @param Throwable|null $previous + * @param string $directoryPath + * @return AssetsMiddlewareException */ - public function __construct(string $message, AssetsMiddleware $assetsMiddleware, - int $code = 0, Throwable $previous = null) + public static function notADirectory(string $directoryPath):self { - $this->assetsMiddleware = $assetsMiddleware; - parent::__construct($message, $code, $previous); + return new self(sprintf("The path '%s' is not a directory or does not exist.", $directoryPath), + self::CODE_NOT_A_DIRECTORY); } /** - * @return AssetsMiddleware + * @param string $directoryPath + * @return AssetsMiddlewareException */ - public function getAssetsMiddleware():AssetsMiddleware + public static function emptyDirectoryKey(string $directoryPath):self { - return $this->assetsMiddleware; + return new self(sprintf("The key of the directory '%s' can not empty.", $directoryPath), + self::CODE_EMPTY_DIRECTORY_KEY); } } \ No newline at end of file diff --git a/src/Assets/AssetResponse.php b/src/Responses/AssetResponse.php similarity index 95% rename from src/Assets/AssetResponse.php rename to src/Responses/AssetResponse.php index 623fad4..ed6b34a 100644 --- a/src/Assets/AssetResponse.php +++ b/src/Responses/AssetResponse.php @@ -20,14 +20,14 @@ // Project: AssetsMiddleware // declare(strict_types=1); -namespace CodeInc\AssetsMiddleware\Assets; +namespace CodeInc\AssetsMiddleware\Responses; use CodeInc\Psr7Responses\FileResponse; /** * Class AssetResponse * - * @package CodeInc\AssetsMiddleware\Assets + * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ class AssetResponse extends FileResponse implements AssetResponseInterface diff --git a/src/Assets/AssetResponseInterface.php b/src/Responses/AssetResponseInterface.php similarity index 93% rename from src/Assets/AssetResponseInterface.php rename to src/Responses/AssetResponseInterface.php index 9d75683..71f8347 100644 --- a/src/Assets/AssetResponseInterface.php +++ b/src/Responses/AssetResponseInterface.php @@ -19,14 +19,14 @@ // Project: AssetsMiddleware // declare(strict_types=1); -namespace CodeInc\AssetsMiddleware\Assets; +namespace CodeInc\AssetsMiddleware\Responses; use Psr\Http\Message\ResponseInterface; /** * Interface AssetResponseInterface * - * @package CodeInc\AssetsMiddleware\Assets + * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ interface AssetResponseInterface extends ResponseInterface diff --git a/src/Assets/AssetCompressedResponse.php b/src/Responses/MinifiedAssetResponse.php similarity index 93% rename from src/Assets/AssetCompressedResponse.php rename to src/Responses/MinifiedAssetResponse.php index 66627ae..1f66a24 100644 --- a/src/Assets/AssetCompressedResponse.php +++ b/src/Responses/MinifiedAssetResponse.php @@ -20,7 +20,7 @@ // Project: AssetsMiddleware // declare(strict_types=1); -namespace CodeInc\AssetsMiddleware\Assets; +namespace CodeInc\AssetsMiddleware\Responses; use CodeInc\MediaTypes\MediaTypes; use CodeInc\Psr7Responses\FileResponse; use enshrined\svgSanitize\Sanitizer; @@ -31,12 +31,12 @@ /** - * Class AssetCompressedResponse + * Class MinifiedAssetResponse * - * @package CodeInc\AssetsMiddleware\Assets + * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ -class AssetCompressedResponse extends FileResponse implements AssetResponseInterface +class MinifiedAssetResponse extends FileResponse implements AssetResponseInterface { /** * @var string @@ -44,7 +44,7 @@ class AssetCompressedResponse extends FileResponse implements AssetResponseInter private $assetPath; /** - * AssetCompressedResponse constructor. + * MinifiedAssetResponse constructor. * * @param string $assetPath * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException diff --git a/src/Assets/AssetNotModifiedResponse.php b/src/Responses/NotModifiedAssetResponse.php similarity index 91% rename from src/Assets/AssetNotModifiedResponse.php rename to src/Responses/NotModifiedAssetResponse.php index c647fcc..7f495c7 100644 --- a/src/Assets/AssetNotModifiedResponse.php +++ b/src/Responses/NotModifiedAssetResponse.php @@ -20,17 +20,17 @@ // Project: AssetsMiddleware // declare(strict_types=1); -namespace CodeInc\AssetsMiddleware\Assets; +namespace CodeInc\AssetsMiddleware\Responses; use GuzzleHttp\Psr7\Response; /** * Class AssetNotModifiedResponse * - * @package CodeInc\AssetsMiddleware\Assets + * @package CodeInc\AssetsMiddleware\Responses * @author Joan Fabrégat */ -class AssetNotModifiedResponse extends Response implements AssetResponseInterface +class NotModifiedAssetResponse extends Response implements AssetResponseInterface { /** * @var string diff --git a/tests/AssetsMiddlewareTest.php b/tests/AssetsMiddlewareTest.php index 933d69f..b4bdaed 100644 --- a/tests/AssetsMiddlewareTest.php +++ b/tests/AssetsMiddlewareTest.php @@ -21,8 +21,8 @@ // declare(strict_types=1); namespace CodeInc\AssetsMiddleware\Test; -use CodeInc\AssetsMiddleware\Assets\AssetResponseInterface; -use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; +use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; +use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; use CodeInc\AssetsMiddleware\AssetsMiddleware; use CodeInc\MiddlewareTestKit\FakeRequestHandler; use CodeInc\MiddlewareTestKit\FakeServerRequest; @@ -46,11 +46,12 @@ final class AssetsMiddlewareTest extends TestCase ]; /** + * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ public function testAssets():void { $middleware = new AssetsMiddleware(__DIR__ . '/Assets', true); - $middleware->registerAssetsDirectory('/assets/v2/'); + $middleware->addAssetsDirectory('/assets/v2/'); foreach (self::ASSETS as $path => $type) { self::assertFileExists($path); @@ -139,7 +140,7 @@ public function testDateCacheAsset():void $response = $middleware->process($request, new FakeRequestHandler()); self::assertInstanceOf(ResponseInterface::class, $response); - self::assertInstanceOf(AssetNotModifiedResponse::class, $response); + self::assertInstanceOf(NotModifiedAssetResponse::class, $response); self::assertEquals(basename($response->getAssetPath()), 'image.svg'); } @@ -157,7 +158,7 @@ public function testEtagCacheAsset():void $response = $middleware->process($request, new FakeRequestHandler()); self::assertInstanceOf(ResponseInterface::class, $response); - self::assertInstanceOf(AssetNotModifiedResponse::class, $response); + self::assertInstanceOf(NotModifiedAssetResponse::class, $response); self::assertEquals(basename($response->getAssetPath()), 'image.svg'); } } \ No newline at end of file From ebcf4e8cea956fa65a8866d4b703cbd01988f287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Fri, 28 Sep 2018 10:55:49 +0200 Subject: [PATCH 04/11] code improvement --- src/AbstractAssetsMiddleware.php | 4 +- src/AssetsMiddleware.php | 21 +++++- src/AssetsMiddlewareException.php | 68 ------------------- src/Exceptions/AssetsMiddlewareException.php | 33 +++++++++ src/Exceptions/EmptyDirectoryKeyException.php | 63 +++++++++++++++++ src/Exceptions/InvalidAssetPathException.php | 63 +++++++++++++++++ src/Exceptions/NotADirectoryException.php | 63 +++++++++++++++++ src/Exceptions/ResponseErrorException.php | 63 +++++++++++++++++ 8 files changed, 305 insertions(+), 73 deletions(-) delete mode 100644 src/AssetsMiddlewareException.php create mode 100644 src/Exceptions/AssetsMiddlewareException.php create mode 100644 src/Exceptions/EmptyDirectoryKeyException.php create mode 100644 src/Exceptions/InvalidAssetPathException.php create mode 100644 src/Exceptions/NotADirectoryException.php create mode 100644 src/Exceptions/ResponseErrorException.php diff --git a/src/AbstractAssetsMiddleware.php b/src/AbstractAssetsMiddleware.php index 6b2a083..35ef909 100644 --- a/src/AbstractAssetsMiddleware.php +++ b/src/AbstractAssetsMiddleware.php @@ -21,6 +21,7 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Exceptions\ResponseErrorException; use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; @@ -92,7 +93,6 @@ abstract protected function getAssetsDirectories():iterable; * @param ServerRequestInterface $request * @param RequestHandlerInterface $handler * @return AssetResponseInterface - * @throws AssetsMiddlewareException */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface { @@ -129,7 +129,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } } catch (\Throwable $exception) { - throw AssetsMiddlewareException::responseError($assetPath, $exception); + throw new ResponseErrorException($assetPath, 0, $exception); } return $response; diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index 6243df7..c20ebcc 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -21,6 +21,9 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Exceptions\InvalidAssetPathException; +use CodeInc\AssetsMiddleware\Exceptions\EmptyDirectoryKeyException; +use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; use CodeInc\AssetsMiddleware\Test\AssetsMiddlewareTest; @@ -46,15 +49,14 @@ class AssetsMiddleware extends AbstractAssetsMiddleware * * @param string $directoryPath * @param string|null $directoryKey - * @throws AssetsMiddlewareException */ public function addAssetsDirectory(string $directoryPath, string $directoryKey = null):void { if (!is_dir($directoryPath) || ($directoryPath = realpath($directoryPath)) === false) { - throw AssetsMiddlewareException::notADirectory($directoryPath); + throw new NotADirectoryException($directoryPath); } if ($directoryKey !== null && empty($directoryKey)) { - throw AssetsMiddlewareException::emptyDirectoryKey($directoryPath); + throw new EmptyDirectoryKeyException($directoryPath); } $this->assetsDirectories[$directoryKey ?? md5($directoryPath)] = $directoryPath; } @@ -67,4 +69,17 @@ protected function getAssetsDirectories():iterable { return $this->assetsDirectories; } + + /** + * @inheritdoc + * @param string $assetPath + * @return null|string + */ + public function getAssetUri(string $assetPath):?string + { + if (($realAssetPath = realpath($assetPath)) === false) { + throw new InvalidAssetPathException($assetPath); + } + return parent::getAssetUri($realAssetPath); + } } \ No newline at end of file diff --git a/src/AssetsMiddlewareException.php b/src/AssetsMiddlewareException.php deleted file mode 100644 index 749c736..0000000 --- a/src/AssetsMiddlewareException.php +++ /dev/null @@ -1,68 +0,0 @@ - -// Date: 03/05/2018 -// Time: 16:15 -// Project: AssetsMiddleware -// -declare(strict_types=1); -namespace CodeInc\AssetsMiddleware; - - -/** - * Class AssetsMiddlewareException - * - * @package CodeInc\AssetsMiddleware - * @author Joan Fabrégat - */ -class AssetsMiddlewareException extends \Exception -{ - public const CODE_RESPONSE_ERROR = 1; - public const CODE_NOT_A_DIRECTORY = 2; - public const CODE_EMPTY_DIRECTORY_KEY = 3; - - /** - * @param string $assetPath - * @param \Throwable $error - * @return AssetsMiddlewareException - */ - public static function responseError(string $assetPath, \Throwable $error):self - { - return new self(sprintf("Error while building the PSR-7 response for the asset '%s'.", $assetPath), - self::CODE_RESPONSE_ERROR, $error); - } - - /** - * @param string $directoryPath - * @return AssetsMiddlewareException - */ - public static function notADirectory(string $directoryPath):self - { - return new self(sprintf("The path '%s' is not a directory or does not exist.", $directoryPath), - self::CODE_NOT_A_DIRECTORY); - } - - /** - * @param string $directoryPath - * @return AssetsMiddlewareException - */ - public static function emptyDirectoryKey(string $directoryPath):self - { - return new self(sprintf("The key of the directory '%s' can not empty.", $directoryPath), - self::CODE_EMPTY_DIRECTORY_KEY); - } -} \ No newline at end of file diff --git a/src/Exceptions/AssetsMiddlewareException.php b/src/Exceptions/AssetsMiddlewareException.php new file mode 100644 index 0000000..9bc759e --- /dev/null +++ b/src/Exceptions/AssetsMiddlewareException.php @@ -0,0 +1,33 @@ + +// Date: 28/09/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; + +/** + * Interface AssetsMiddlewareException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +interface AssetsMiddlewareException +{ + +} \ No newline at end of file diff --git a/src/Exceptions/EmptyDirectoryKeyException.php b/src/Exceptions/EmptyDirectoryKeyException.php new file mode 100644 index 0000000..22a5e32 --- /dev/null +++ b/src/Exceptions/EmptyDirectoryKeyException.php @@ -0,0 +1,63 @@ + +// Date: 28/09/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class EmptyDirectoryKeyException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class EmptyDirectoryKeyException extends \LogicException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $directoryPath; + + /** + * EmptyDirectoryKeyException constructor. + * + * @param string $directoryPath + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $directoryPath, int $code = 0, Throwable $previous = null) + { + $this->directoryPath = $directoryPath; + parent::__construct( + sprintf("The key of the directory '%s' can not empty.", $directoryPath), + $code, + $previous + ); + } + + /** + * @return string + */ + public function getDirectoryPath():string + { + return $this->directoryPath; + } +} \ No newline at end of file diff --git a/src/Exceptions/InvalidAssetPathException.php b/src/Exceptions/InvalidAssetPathException.php new file mode 100644 index 0000000..a32e743 --- /dev/null +++ b/src/Exceptions/InvalidAssetPathException.php @@ -0,0 +1,63 @@ + +// Date: 28/09/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class InvalidAssetPathException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class InvalidAssetPathException extends \RuntimeException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $assetPath; + + /** + * InvalidAssetPathException constructor. + * + * @param string $assetPath + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $assetPath, int $code = 0, Throwable $previous = null) + { + $this->assetPath = $assetPath; + parent::__construct( + sprintf("The asset path '%s' is not valid.", $assetPath), + $code, + $previous + ); + } + + /** + * @return string + */ + public function getAssetPath():string + { + return $this->assetPath; + } +} \ No newline at end of file diff --git a/src/Exceptions/NotADirectoryException.php b/src/Exceptions/NotADirectoryException.php new file mode 100644 index 0000000..029b450 --- /dev/null +++ b/src/Exceptions/NotADirectoryException.php @@ -0,0 +1,63 @@ + +// Date: 28/09/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class NotADirectoryException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class NotADirectoryException extends \LogicException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $path; + + /** + * NotADirectoryException 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 path '%s' is not a directory or 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/ResponseErrorException.php b/src/Exceptions/ResponseErrorException.php new file mode 100644 index 0000000..d30ee3e --- /dev/null +++ b/src/Exceptions/ResponseErrorException.php @@ -0,0 +1,63 @@ + +// Date: 28/09/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class ResponseErrorException + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class ResponseErrorException extends \RuntimeException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $assetPath; + + /** + * ResponseErrorException constructor. + * + * @param string $assetPath + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $assetPath, int $code = 0, Throwable $previous = null) + { + $this->assetPath = $assetPath; + parent::__construct( + sprintf("Error while building the PSR-7 response for the asset '%s'.", $assetPath), + $code, + $previous + ); + } + + /** + * @return string + */ + public function getAssetPath():string + { + return $this->assetPath; + } +} \ No newline at end of file From e5e5b086fe86928065fa573cd735763c630b0bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Fri, 28 Sep 2018 11:13:28 +0200 Subject: [PATCH 05/11] bug fix --- src/AbstractAssetsMiddleware.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AbstractAssetsMiddleware.php b/src/AbstractAssetsMiddleware.php index 35ef909..01f3c29 100644 --- a/src/AbstractAssetsMiddleware.php +++ b/src/AbstractAssetsMiddleware.php @@ -151,7 +151,8 @@ public function getAssetUri(string $assetPath):?string { foreach ($this->getAssetsDirectories() as $directoryKey => $directoryPath) { if (substr($assetPath, 0, strlen($directoryPath)) == $directoryPath) { - return $this->assetsUriPrefix.urlencode($directoryKey).'/'.substr($assetPath, strlen($directoryPath)); + return $this->assetsUriPrefix.urlencode($directoryKey) + .str_replace('\\', '/', substr($assetPath, strlen($directoryPath))); } } return null; From 4144631b0bf8dd65120e8925cb3f525a1cbe1ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Fri, 28 Sep 2018 17:59:22 +0200 Subject: [PATCH 06/11] fix --- composer.json | 3 +-- src/AbstractAssetsMiddleware.php | 15 +++++++++------ src/Exceptions/NotADirectoryException.php | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 4f18656..75ee40d 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ "codeinc/psr7-responses": "^1.2", "matthiasmullie/minify": "^1.3", "codeinc/media-types": "^1.0", - "enshrined/svg-sanitize": "^0.8.2", - "zendframework/zend-validator": "^2.10" + "enshrined/svg-sanitize": "^0.8.2" }, "require-dev": { "phpunit/phpunit": "^7", diff --git a/src/AbstractAssetsMiddleware.php b/src/AbstractAssetsMiddleware.php index 01f3c29..e01835d 100644 --- a/src/AbstractAssetsMiddleware.php +++ b/src/AbstractAssetsMiddleware.php @@ -21,6 +21,7 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; use CodeInc\AssetsMiddleware\Exceptions\ResponseErrorException; use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; @@ -31,7 +32,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Zend\Validator\File\Exists; /** @@ -105,11 +105,14 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface // if a match is found if ($matches[1] == $directoryKey) { + if (($realDirectoryPath = realpath($directoryPath)) === false) { + throw new NotADirectoryException($directoryPath); + } // validating the assets location - $assetPath = $directoryPath.DIRECTORY_SEPARATOR.$matches[2]; - if ((new Exists($directoryPath))->isValid($assetPath)) { - + $assetPath = realpath($directoryPath.DIRECTORY_SEPARATOR.$matches[2]); + if ($assetPath && substr($assetPath, 0, strlen($realDirectoryPath)) == $realDirectoryPath) + { try { // building the response $response = $this->minimizeAssets @@ -126,13 +129,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if ($cache->isNotModified($request, $response)) { $response = new NotModifiedAssetResponse($assetPath); } + return $response; } + return $response; } catch (\Throwable $exception) { throw new ResponseErrorException($assetPath, 0, $exception); } - - return $response; } } } diff --git a/src/Exceptions/NotADirectoryException.php b/src/Exceptions/NotADirectoryException.php index 029b450..c7302de 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 path '%s' is not a directory or does not exist.", $path), + sprintf("The assets path '%s' is not a directory or does not exist.", $path), $code, $previous ); From b32763e54415eed31eefe301bf1c5ba81958cfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Thu, 4 Oct 2018 09:38:08 +0200 Subject: [PATCH 07/11] merges AbstractAssetsMiddleware ans AssetsMiddleware --- src/AbstractAssetsMiddleware.php | 163 ------------------------------- src/AssetsMiddleware.php | 159 +++++++++++++++++++++++++++--- 2 files changed, 147 insertions(+), 175 deletions(-) delete mode 100644 src/AbstractAssetsMiddleware.php diff --git a/src/AbstractAssetsMiddleware.php b/src/AbstractAssetsMiddleware.php deleted file mode 100644 index e01835d..0000000 --- a/src/AbstractAssetsMiddleware.php +++ /dev/null @@ -1,163 +0,0 @@ - -// Date: 05/03/2018 -// Time: 17:15 -// Project: AssetsMiddleware -// -declare(strict_types = 1); -namespace CodeInc\AssetsMiddleware; -use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; -use CodeInc\AssetsMiddleware\Exceptions\ResponseErrorException; -use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; -use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; -use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; -use CodeInc\AssetsMiddleware\Responses\AssetResponse; -use Micheh\Cache\CacheUtil; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; - - -/** - * Class AbstractAssetsMiddleware - * - * @package CodeInc\AssetsMiddleware - * @author Joan Fabrégat - * @license MIT - * @link https://github.com/CodeIncHQ/AssetsMiddleware - */ -abstract class AbstractAssetsMiddleware implements MiddlewareInterface -{ - /** - * Base assets URI path. - * - * @var string - */ - private $assetsUriPrefix; - - /** - * Allows the assets to the cached in the web browser. - * - * @var bool - */ - private $cacheAssets; - - /** - * Allows the assets to be minimized. - * - * @var bool - */ - private $minimizeAssets; - - /** - * AssetsMiddleware constructor. - * - * @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) - */ - public function __construct(string $assetsUriPrefix, bool $cacheAssets = true, - bool $minimizeAssets = false) - { - $this->assetsUriPrefix = $assetsUriPrefix; - $this->cacheAssets = $cacheAssets; - $this->minimizeAssets = $minimizeAssets; - } - - /** - * Returns the assets directories. Keys must be directories identifiers (often hashes of the path) - * and values directories paths. - * - * @return iterable - */ - abstract protected function getAssetsDirectories():iterable; - - /** - * @inheritdoc - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - * @return AssetResponseInterface - */ - 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); - } - - // validating the assets location - $assetPath = realpath($directoryPath.DIRECTORY_SEPARATOR.$matches[2]); - if ($assetPath && substr($assetPath, 0, strlen($realDirectoryPath)) == $realDirectoryPath) - { - try { - // building the response - $response = $this->minimizeAssets - ? new MinifiedAssetResponse($assetPath) : - new AssetResponse($assetPath); - - // 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); - } - } - } - } - } - - return $handler->handle($request); - } - - /** - * Returns the public URI for a given asset. The asset must be within a registered assets directory. - * - * @param string $assetPath - * @return string - */ - public function getAssetUri(string $assetPath):?string - { - foreach ($this->getAssetsDirectories() as $directoryKey => $directoryPath) { - if (substr($assetPath, 0, strlen($directoryPath)) == $directoryPath) { - return $this->assetsUriPrefix.urlencode($directoryKey) - .str_replace('\\', '/', substr($assetPath, strlen($directoryPath))); - } - } - return null; - } -} \ No newline at end of file diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index c20ebcc..c86ba76 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -21,10 +21,20 @@ // declare(strict_types = 1); namespace CodeInc\AssetsMiddleware; +use CodeInc\AssetsMiddleware\Exceptions\InvalidAssetMediaTypeException; use CodeInc\AssetsMiddleware\Exceptions\InvalidAssetPathException; use CodeInc\AssetsMiddleware\Exceptions\EmptyDirectoryKeyException; use CodeInc\AssetsMiddleware\Exceptions\NotADirectoryException; -use CodeInc\AssetsMiddleware\Test\AssetsMiddlewareTest; +use CodeInc\AssetsMiddleware\Exceptions\ResponseErrorException; +use CodeInc\AssetsMiddleware\Responses\AssetResponse; +use CodeInc\AssetsMiddleware\Responses\AssetResponseInterface; +use CodeInc\AssetsMiddleware\Responses\MinifiedAssetResponse; +use CodeInc\AssetsMiddleware\Responses\NotModifiedAssetResponse; +use CodeInc\MediaTypes\MediaTypes; +use Micheh\Cache\CacheUtil; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; /** @@ -34,16 +44,50 @@ * @author Joan Fabrégat * @license MIT * @link https://github.com/CodeIncHQ/AssetsMiddleware - * @see AssetsMiddlewareTest - * @version 2 */ -class AssetsMiddleware extends AbstractAssetsMiddleware +class AssetsMiddleware { /** * @var array */ private $assetsDirectories = []; + /** + * Base assets URI path. + * + * @var string + */ + private $assetsUriPrefix; + + /** + * Allows the assets to the cached in the web browser. + * + * @var bool + */ + private $cacheAssets; + + /** + * Allows the assets to be minimized. + * + * @var bool + */ + private $minimizeAssets; + + /** + * AssetsMiddleware constructor. + * + * @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) + */ + public function __construct(string $assetsUriPrefix, bool $cacheAssets = true, + bool $minimizeAssets = false) + { + $this->assetsUriPrefix = $assetsUriPrefix; + $this->cacheAssets = $cacheAssets; + $this->minimizeAssets = $minimizeAssets; + } + /** * Adds an assets directory * @@ -70,16 +114,107 @@ protected function getAssetsDirectories():iterable return $this->assetsDirectories; } + /** + * Sets the allowed media types for the assets. The comparison supports shell patterns with operators + * like *, ?, etc. + * + * @param iterable $allowMediaTypes + */ + public function setAllowMediaTypes(iterable $allowMediaTypes):void + { + $this->allowedMediaTypes = ($allowMediaTypes instanceof \Traversable) + ? iterator_to_array($allowMediaTypes) + : $allowMediaTypes; + } + /** * @inheritdoc + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return AssetResponseInterface + */ + 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); + } + + // validating the assets location + $assetPath = realpath($directoryPath.DIRECTORY_SEPARATOR.$matches[2]); + if ($assetPath && substr($assetPath, 0, strlen($realDirectoryPath)) == $realDirectoryPath) + { + return $this->buildAssetResponse($assetPath, $request); + } + } + } + } + + return $handler->handle($request); + } + + /** + * Builds and returns the asset's PSR-7 response. + * * @param string $assetPath - * @return null|string + * @param ServerRequestInterface $request + * @return AssetResponseInterface */ - public function getAssetUri(string $assetPath):?string - { - if (($realAssetPath = realpath($assetPath)) === false) { - throw new InvalidAssetPathException($assetPath); - } - return parent::getAssetUri($realAssetPath); - } + private function buildAssetResponse(string $assetPath, ServerRequestInterface $request):AssetResponseInterface + { + try { + // reading the assets media type + $assetMediaType = MediaTypes::getFilenameMediaType($assetPath); + + // 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); + } + } + + /** + * Returns the public URI for a given asset. The asset must be within a registered assets directory. + * + * @param string $assetPath + * @return string + */ + public function getAssetUri(string $assetPath):?string + { + 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))); + } + } + return null; + } } \ No newline at end of file From 964257d0eaa8f5aea57f819f7918863b2674dd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Thu, 4 Oct 2018 09:38:29 +0200 Subject: [PATCH 08/11] adds support for asset's media type limitation --- src/AssetsMiddleware.php | 31 ++++++++ .../InvalidAssetMediaTypeException.php | 78 +++++++++++++++++++ src/Responses/AssetResponse.php | 5 +- src/Responses/MinifiedAssetResponse.php | 15 ++-- 4 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 src/Exceptions/InvalidAssetMediaTypeException.php diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index c86ba76..dfe52d3 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -73,6 +73,13 @@ class AssetsMiddleware */ private $minimizeAssets; + /** + * Limits the allowed assets media types. + * + * @var null|string[] + */ + private $allowedMediaTypes; + /** * AssetsMiddleware constructor. * @@ -174,6 +181,11 @@ private function buildAssetResponse(string $assetPath, ServerRequestInterface $r // 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) : @@ -198,6 +210,25 @@ private function buildAssetResponse(string $assetPath, ServerRequestInterface $r } } + /** + * Verifies if the assets media type is supported. + * + * @param string $assetMediaType + * @return bool + */ + protected function isMediaTypeAllowed(string $assetMediaType):bool + { + if (is_array($this->allowedMediaTypes) && !empty($this->allowedMediaTypes)) { + foreach ($this->allowedMediaTypes as $mediaType) { + if (strcasecmp($assetMediaType, $mediaType) === 0 || fnmatch($mediaType, $assetMediaType)) { + return true; + } + } + return false; + } + return true; + } + /** * Returns the public URI for a given asset. The asset must be within a registered assets directory. * diff --git a/src/Exceptions/InvalidAssetMediaTypeException.php b/src/Exceptions/InvalidAssetMediaTypeException.php new file mode 100644 index 0000000..e4843cb --- /dev/null +++ b/src/Exceptions/InvalidAssetMediaTypeException.php @@ -0,0 +1,78 @@ + +// Date: 04/10/2018 +// Project: AssetsMiddleware +// +declare(strict_types=1); +namespace CodeInc\AssetsMiddleware\Exceptions; +use Throwable; + + +/** + * Class InvalidAssetMediaType + * + * @package CodeInc\AssetsMiddleware\Exceptions + * @author Joan Fabrégat + */ +class InvalidAssetMediaTypeException extends \RuntimeException implements AssetsMiddlewareException +{ + /** + * @var string + */ + private $assetPath; + + /** + * @var string + */ + private $mediaType; + + /** + * InvalidAssetMediaTypeException constructor. + * + * @param string $assetPath + * @param string $mediaType + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $assetPath, string $mediaType, int $code = 0, Throwable $previous = null) + { + $this->assetPath = $assetPath; + $this->mediaType = $mediaType; + parent::__construct( + sprintf("The media type '%s' of the asset '%s' is not allowed.", $mediaType, $assetPath), + $code, + $previous + ); + } + + /** + * @return string + */ + public function getAssetPath():string + { + return $this->assetPath; + } + + /** + * @return string + */ + public function getMediaType():string + { + return $this->mediaType; + } +} \ No newline at end of file diff --git a/src/Responses/AssetResponse.php b/src/Responses/AssetResponse.php index ed6b34a..4bedaae 100644 --- a/src/Responses/AssetResponse.php +++ b/src/Responses/AssetResponse.php @@ -41,12 +41,13 @@ class AssetResponse extends FileResponse implements AssetResponseInterface * AssetResponse constructor. * * @param string $assetPath + * @param string $mediaType * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - public function __construct(string $assetPath) + public function __construct(string $assetPath, string $mediaType) { $this->assetPath = $assetPath; - parent::__construct($assetPath, basename($assetPath), null, false); + parent::__construct($assetPath, basename($assetPath), $mediaType, false); } diff --git a/src/Responses/MinifiedAssetResponse.php b/src/Responses/MinifiedAssetResponse.php index 1f66a24..62386d7 100644 --- a/src/Responses/MinifiedAssetResponse.php +++ b/src/Responses/MinifiedAssetResponse.php @@ -43,27 +43,32 @@ class MinifiedAssetResponse extends FileResponse implements AssetResponseInterfa */ private $assetPath; + /** + * @var string + */ + private $mediaType; + /** * MinifiedAssetResponse constructor. * * @param string $assetPath + * @param string $mediaType * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ - public function __construct(string $assetPath) + public function __construct(string $assetPath, string $mediaType) { $this->assetPath = $assetPath; - parent::__construct($this->buildStream($assetPath), basename($assetPath), - null, false); + $this->mediaType = $mediaType; + parent::__construct($this->buildStream($assetPath), basename($assetPath), $mediaType, false); } /** * @param string $filePath * @return StreamInterface - * @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException */ private function buildStream(string $filePath):StreamInterface { - switch (MediaTypes::getFilenameMediaType($filePath)) { + switch ($this->mediaType) { case 'text/css': $css = new Minify\CSS($filePath); $css->setImportExtensions([]); From 801fe505e1a098db5e7535414ff1f226c05cfa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Thu, 4 Oct 2018 09:41:17 +0200 Subject: [PATCH 09/11] bug fix --- src/AssetsMiddleware.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/AssetsMiddleware.php b/src/AssetsMiddleware.php index dfe52d3..875cfc9 100644 --- a/src/AssetsMiddleware.php +++ b/src/AssetsMiddleware.php @@ -34,6 +34,7 @@ use Micheh\Cache\CacheUtil; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -45,7 +46,7 @@ * @license MIT * @link https://github.com/CodeIncHQ/AssetsMiddleware */ -class AssetsMiddleware +class AssetsMiddleware implements MiddlewareInterface { /** * @var array From 50d277e9c246e78863d0bf90239f25a88e71ca51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Thu, 4 Oct 2018 09:57:14 +0200 Subject: [PATCH 10/11] v2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 75ee40d..cd33a41 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "codeinc/assets-middleware", - "version": "2.0.0-beta.1", + "version": "2.0.0", "description": "A PSR-15 middleware to server static assets (CSS, JS, images, etc.)", "homepage": "https://github.com/CodeIncHQ/AssetsMiddleware", "type": "library", From 5e8b25efa6353dac86d0ecd31e5b30ffddd68168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20Fabr=C3=A9gat?= Date: Thu, 4 Oct 2018 09:58:56 +0200 Subject: [PATCH 11/11] adds usage --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 1aa5c0c..09975eb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,34 @@ This PHP 7.1 library is a [PSR-15](https://www.php-fig.org/psr/psr-15/) middleware dedicated to manage static assets like CSS, JS, or image files. +## Usage + +```php +addAssetsDirectory('/path/to/my/first/web-assets-directory'); +$assetsMiddleware->addAssetsDirectory('/path/to/another/web-assets-directory'); + +// optionally you can limit the acceptable media types +$assetsMiddleware->setAllowMediaTypes([ + 'image/*', + 'text/css', + 'application/javascript' +]); + +// returns the computed path to the assets directory +$assetsMiddleware->getAssetUri('/path/to/another/web-assets-directory/an-image.jpg'); + +// processed a PSR-7 server request as a PSR-15 middleware +$assetsMiddleware->process($aPsr7ServerRequest, $aPsr15RequestHandler); // <-- returns a PSR-7 response +``` + ## Installation This library is available through [Packagist](https://packagist.org/packages/codeinc/assets-middleware) and can be installed using [Composer](https://getcomposer.org/):