This repository has been archived by the owner on Feb 5, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from CodeIncHQ/2.x
2.x
- Loading branch information
Showing
14 changed files
with
571 additions
and
262 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | | ||
// +---------------------------------------------------------------------+ | ||
// | ||
|
@@ -21,10 +21,16 @@ | |
// | ||
declare(strict_types = 1); | ||
namespace CodeInc\AssetsMiddleware; | ||
use CodeInc\AssetsMiddleware\Assets\AssetCompressedResponse; | ||
use CodeInc\AssetsMiddleware\Assets\AssetNotModifiedResponse; | ||
use CodeInc\AssetsMiddleware\Assets\AssetResponse; | ||
use CodeInc\AssetsMiddleware\Test\AssetsMiddlewareTest; | ||
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\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; | ||
|
@@ -39,166 +45,208 @@ | |
* @author Joan Fabrégat <[email protected]> | ||
* @license MIT <https://github.com/CodeIncHQ/AssetsMiddleware/blob/master/LICENSE> | ||
* @link https://github.com/CodeIncHQ/AssetsMiddleware | ||
* @see AssetsMiddlewareTest | ||
*/ | ||
class AssetsMiddleware implements MiddlewareInterface | ||
{ | ||
/** | ||
* @var string | ||
* @var array | ||
*/ | ||
private $assetsLocalPath; | ||
private $assetsDirectories = []; | ||
|
||
/** | ||
* Base assets URI path. | ||
* | ||
* @var string | ||
*/ | ||
private $assetsUriPath; | ||
private $assetsUriPrefix; | ||
|
||
/** | ||
* Allows the assets to the cached in the web browser. | ||
* | ||
* @var bool | ||
*/ | ||
private $allowAssetsCache; | ||
private $cacheAssets; | ||
|
||
/** | ||
* Allows the assets to be minimized. | ||
* | ||
* @var bool | ||
*/ | ||
private $allowAssetsCompression; | ||
private $minimizeAssets; | ||
|
||
/** | ||
* AssetsMiddleware constructor. | ||
* Limits the allowed assets media types. | ||
* | ||
* @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 | ||
* @var null|string[] | ||
*/ | ||
public function __construct(string $assetsLocalPath, string $assetsUriPath, | ||
bool $allowAssetsCache = true, bool $allowAssetsCompression = 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->allowAssetsCache = $allowAssetsCache; | ||
$this->allowAssetsCompression = $allowAssetsCompression; | ||
} | ||
private $allowedMediaTypes; | ||
|
||
/** | ||
* @inheritdoc | ||
* @param ServerRequestInterface $request | ||
* @param RequestHandlerInterface $handler | ||
* @return ResponseInterface | ||
* @throws \CodeInc\MediaTypes\Exceptions\MediaTypesException | ||
* @throws \CodeInc\Psr7Responses\ResponseException | ||
* 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 process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface | ||
public function __construct(string $assetsUriPrefix, bool $cacheAssets = true, | ||
bool $minimizeAssets = false) | ||
{ | ||
// if the response points toward a valid asset | ||
if (($assetName = $this->getAssetName($request)) !== null) { | ||
$assetPath = $this->getAssetPath($assetName); | ||
if (file_exists($assetPath)) { | ||
|
||
// builds the response | ||
if (!$this->allowAssetsCompression) { | ||
$response = new AssetResponse($assetPath, $assetName); | ||
} | ||
else { | ||
$response = new AssetCompressedResponse($assetPath, $assetName); | ||
} | ||
|
||
// 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($assetName); | ||
} | ||
} | ||
|
||
return $response; | ||
} | ||
} | ||
|
||
// returns the handler response | ||
return $handler->handle($request); | ||
$this->assetsUriPrefix = $assetsUriPrefix; | ||
$this->cacheAssets = $cacheAssets; | ||
$this->minimizeAssets = $minimizeAssets; | ||
} | ||
|
||
/** | ||
* @return string | ||
* Adds an assets directory | ||
* | ||
* @param string $directoryPath | ||
* @param string|null $directoryKey | ||
*/ | ||
public function getAssetsLocalPath():string | ||
public function addAssetsDirectory(string $directoryPath, string $directoryKey = null):void | ||
{ | ||
return $this->assetsLocalPath; | ||
if (!is_dir($directoryPath) || ($directoryPath = realpath($directoryPath)) === false) { | ||
throw new NotADirectoryException($directoryPath); | ||
} | ||
if ($directoryKey !== null && empty($directoryKey)) { | ||
throw new EmptyDirectoryKeyException($directoryPath); | ||
} | ||
$this->assetsDirectories[$directoryKey ?? md5($directoryPath)] = $directoryPath; | ||
} | ||
|
||
/** | ||
* @return string | ||
* @inheritdoc | ||
* @return iterable | ||
*/ | ||
public function getAssetsUriPath():string | ||
{ | ||
return $this->assetsUriPath; | ||
} | ||
protected function getAssetsDirectories():iterable | ||
{ | ||
return $this->assetsDirectories; | ||
} | ||
|
||
/** | ||
* Enables the assets cache (enabled by default). | ||
* Sets the allowed media types for the assets. The comparison supports shell patterns with operators | ||
* like *, ?, etc. | ||
* | ||
* @param iterable $allowMediaTypes | ||
*/ | ||
public function enableAssetsCache():void | ||
public function setAllowMediaTypes(iterable $allowMediaTypes):void | ||
{ | ||
$this->allowAssetsCache = false; | ||
$this->allowedMediaTypes = ($allowMediaTypes instanceof \Traversable) | ||
? iterator_to_array($allowMediaTypes) | ||
: $allowMediaTypes; | ||
} | ||
|
||
/** | ||
* Disables the assets cache (enabled by default). | ||
* @inheritdoc | ||
* @param ServerRequestInterface $request | ||
* @param RequestHandlerInterface $handler | ||
* @return AssetResponseInterface | ||
*/ | ||
public function disableAssetsCache():void | ||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler):ResponseInterface | ||
{ | ||
$this->allowAssetsCache = false; | ||
// 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); | ||
} | ||
|
||
/** | ||
* Returns an asset's name from a request or null if the request does'nt points toward an asset. | ||
* Builds and returns the asset's PSR-7 response. | ||
* | ||
* @param string $assetPath | ||
* @param ServerRequestInterface $request | ||
* @return null|string | ||
* @return AssetResponseInterface | ||
*/ | ||
public function getAssetName(ServerRequestInterface $request):?string | ||
private function buildAssetResponse(string $assetPath, ServerRequestInterface $request):AssetResponseInterface | ||
{ | ||
if (preg_match('#^'.preg_quote($this->assetsUriPath, '#').'([\\w\\-_./]+)$#ui', | ||
$request->getUri()->getPath(), $matches)) { | ||
return $matches[1]; | ||
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); | ||
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Returns an asset's path. | ||
* Verifies if the assets media type is supported. | ||
* | ||
* @param string $assetName | ||
* @return string | ||
* @param string $assetMediaType | ||
* @return bool | ||
*/ | ||
public function getAssetPath(string $assetName):string | ||
protected function isMediaTypeAllowed(string $assetMediaType):bool | ||
{ | ||
if (substr($assetName, 0, strlen(DIRECTORY_SEPARATOR)) == DIRECTORY_SEPARATOR) { | ||
$assetName = substr($assetName, strlen(DIRECTORY_SEPARATOR)); | ||
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 $this->assetsLocalPath.DIRECTORY_SEPARATOR.$assetName; | ||
return true; | ||
} | ||
|
||
/** | ||
* 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 | ||
{ | ||
return $this->assetsUriPath.$asset; | ||
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; | ||
} | ||
} |
Oops, something went wrong.