Skip to content
This repository has been archived by the owner on Feb 5, 2019. It is now read-only.

Commit

Permalink
Merge pull request #10 from CodeIncHQ/2.x
Browse files Browse the repository at this point in the history
2.x
  • Loading branch information
Joan Fabrégat authored Oct 4, 2018
2 parents 7ef13e9 + 5e8b25e commit 8314d93
Show file tree
Hide file tree
Showing 14 changed files with 571 additions and 262 deletions.
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php
use CodeInc\AssetsMiddleware\AssetsMiddleware;

$assetsMiddleware = new AssetsMiddleware(
'/assets/' // <-- specifies the assets base URI path
);

// adding web assets directories
$assetsMiddleware->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/):
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "codeinc/assets-middleware",
"version": "1.2.3",
"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",
"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",
Expand Down
252 changes: 150 additions & 102 deletions src/AssetsMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
// +---------------------------------------------------------------------+
//
Expand All @@ -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;
Expand All @@ -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;
}
}
Loading

0 comments on commit 8314d93

Please sign in to comment.