1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\Routing\Middleware;
16:
17: use Cake\Core\Plugin;
18: use Cake\Filesystem\File;
19: use Cake\Utility\Inflector;
20: use Psr\Http\Message\ResponseInterface;
21: use Psr\Http\Message\ServerRequestInterface;
22: use Zend\Diactoros\Response;
23: use Zend\Diactoros\Stream;
24:
25: 26: 27: 28: 29: 30: 31:
32: class AssetMiddleware
33: {
34: 35: 36: 37: 38:
39: protected $cacheTime = '+1 day';
40:
41: 42: 43: 44: 45: 46: 47: 48:
49: protected $typeMap = [
50: 'css' => 'text/css',
51: 'json' => 'application/json',
52: 'js' => 'application/javascript',
53: 'ico' => 'image/x-icon',
54: 'eot' => 'application/vnd.ms-fontobject',
55: 'svg' => 'image/svg+xml',
56: 'html' => 'text/html',
57: 'rss' => 'application/rss+xml',
58: 'xml' => 'application/xml',
59: ];
60:
61: 62: 63: 64: 65:
66: public function __construct(array $options = [])
67: {
68: if (!empty($options['cacheTime'])) {
69: $this->cacheTime = $options['cacheTime'];
70: }
71: if (!empty($options['types'])) {
72: $this->typeMap = array_merge($this->typeMap, $options['types']);
73: }
74: }
75:
76: 77: 78: 79: 80: 81: 82: 83:
84: public function __invoke($request, $response, $next)
85: {
86: $url = $request->getUri()->getPath();
87: if (strpos($url, '..') !== false || strpos($url, '.') === false) {
88: return $next($request, $response);
89: }
90:
91: if (strpos($url, '/.') !== false) {
92: return $next($request, $response);
93: }
94:
95: $assetFile = $this->_getAssetFile($url);
96: if ($assetFile === null || !file_exists($assetFile)) {
97: return $next($request, $response);
98: }
99:
100: $file = new File($assetFile);
101: $modifiedTime = $file->lastChange();
102: if ($this->isNotModified($request, $file)) {
103: $headers = $response->getHeaders();
104: $headers['Last-Modified'] = date(DATE_RFC850, $modifiedTime);
105:
106: return new Response('php://memory', 304, $headers);
107: }
108:
109: return $this->deliverAsset($request, $response, $file);
110: }
111:
112: 113: 114: 115: 116: 117: 118:
119: protected function isNotModified($request, $file)
120: {
121: $modifiedSince = $request->getHeaderLine('If-Modified-Since');
122: if (!$modifiedSince) {
123: return false;
124: }
125:
126: return strtotime($modifiedSince) === $file->lastChange();
127: }
128:
129: 130: 131: 132: 133: 134:
135: protected function _getAssetFile($url)
136: {
137: $parts = explode('/', ltrim($url, '/'));
138: $pluginPart = [];
139: for ($i = 0; $i < 2; $i++) {
140: if (!isset($parts[$i])) {
141: break;
142: }
143: $pluginPart[] = Inflector::camelize($parts[$i]);
144: $plugin = implode('/', $pluginPart);
145: if ($plugin && Plugin::isLoaded($plugin)) {
146: $parts = array_slice($parts, $i + 1);
147: $fileFragment = implode(DIRECTORY_SEPARATOR, $parts);
148: $pluginWebroot = Plugin::path($plugin) . 'webroot' . DIRECTORY_SEPARATOR;
149:
150: return $pluginWebroot . $fileFragment;
151: }
152: }
153:
154: return null;
155: }
156:
157: 158: 159: 160: 161: 162: 163: 164:
165: protected function deliverAsset(ServerRequestInterface $request, ResponseInterface $response, $file)
166: {
167: $contentType = $this->getType($file);
168: $modified = $file->lastChange();
169: $expire = strtotime($this->cacheTime);
170: $maxAge = $expire - time();
171:
172: $stream = new Stream(fopen($file->path, 'rb'));
173:
174: return $response->withBody($stream)
175: ->withHeader('Content-Type', $contentType)
176: ->withHeader('Cache-Control', 'public,max-age=' . $maxAge)
177: ->withHeader('Date', gmdate('D, j M Y G:i:s \G\M\T', time()))
178: ->withHeader('Last-Modified', gmdate('D, j M Y G:i:s \G\M\T', $modified))
179: ->withHeader('Expires', gmdate('D, j M Y G:i:s \G\M\T', $expire));
180: }
181:
182: 183: 184: 185: 186: 187:
188: protected function getType($file)
189: {
190: $extension = $file->ext();
191: if (isset($this->typeMap[$extension])) {
192: return $this->typeMap[$extension];
193: }
194:
195: return $file->mime() ?: 'application/octet-stream';
196: }
197: }
198: