1 | <?php |
||||
2 | /** |
||||
3 | * laravel-assets: asset management for Laravel 5 |
||||
4 | * |
||||
5 | * Copyright (c) 2021 Greg Roach |
||||
6 | * |
||||
7 | * This program is free software: you can redistribute it and/or modify |
||||
8 | * it under the terms of the GNU General Public License as published by |
||||
9 | * the Free Software Foundation, either version 3 of the License, or |
||||
10 | * (at your option) any later version. |
||||
11 | * |
||||
12 | * This program is distributed in the hope that it will be useful, |
||||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
15 | * GNU General Public License for more details. |
||||
16 | * |
||||
17 | * You should have received a copy of the GNU General Public License |
||||
18 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
19 | */ |
||||
20 | |||||
21 | namespace Fisharebest\LaravelAssets; |
||||
22 | |||||
23 | use Fisharebest\LaravelAssets\Commands\Purge; |
||||
24 | use Fisharebest\LaravelAssets\Filters\FilterInterface; |
||||
25 | use Fisharebest\LaravelAssets\Loaders\LoaderInterface; |
||||
26 | use Fisharebest\LaravelAssets\Notifiers\NotifierInterface; |
||||
27 | use InvalidArgumentException; |
||||
28 | use League\Flysystem\FileExistsException; |
||||
29 | use League\Flysystem\FileNotFoundException; |
||||
30 | use League\Flysystem\Filesystem; |
||||
31 | |||||
32 | class Assets |
||||
33 | { |
||||
34 | /** |
||||
35 | * Regular expression to match a CSS url |
||||
36 | */ |
||||
37 | const REGEX_CSS = '/\.css$/i'; |
||||
38 | |||||
39 | /** |
||||
40 | * Regular expression to match a JS url |
||||
41 | */ |
||||
42 | const REGEX_JS = '/\.js$/i'; |
||||
43 | |||||
44 | /** |
||||
45 | * Regular expression to match a minified CSS url |
||||
46 | */ |
||||
47 | const REGEX_MINIFIED_CSS = '/[.-]min\.css$/i'; |
||||
48 | |||||
49 | /** |
||||
50 | * Regular expression to match a minified JS url |
||||
51 | */ |
||||
52 | const REGEX_MINIFIED_JS = '/[.-]min\.js$/i'; |
||||
53 | |||||
54 | /** |
||||
55 | * Regular expression to match an external url |
||||
56 | */ |
||||
57 | const REGEX_EXTERNAL_URL = '/^((https?:)?\/\/|data:)/i'; |
||||
58 | |||||
59 | /** |
||||
60 | * File type detection options |
||||
61 | */ |
||||
62 | const TYPE_CSS = 'css'; |
||||
63 | const TYPE_JS = 'js'; |
||||
64 | const TYPE_AUTO = 'auto'; |
||||
65 | |||||
66 | /** |
||||
67 | * File group options. Most sites will only use the default group. |
||||
68 | */ |
||||
69 | const GROUP_DEFAULT = ''; |
||||
70 | |||||
71 | /** |
||||
72 | * Format HTML links using printf() |
||||
73 | */ |
||||
74 | const FORMAT_CSS_LINK = '<link%s rel="stylesheet" href="%s">'; |
||||
75 | const FORMAT_JS_LINK = '<script%s src="%s"></script>'; |
||||
76 | |||||
77 | /** |
||||
78 | * Format inline assets using printf() |
||||
79 | */ |
||||
80 | const FORMAT_CSS_INLINE = '<style>%s</style>'; |
||||
81 | const FORMAT_JS_INLINE = '<script>%s</script>'; |
||||
82 | |||||
83 | /** |
||||
84 | * Enable the pipeline and minify functions. |
||||
85 | * |
||||
86 | * @var bool |
||||
87 | */ |
||||
88 | private $enabled; |
||||
89 | |||||
90 | /** |
||||
91 | * Where do we read CSS files. |
||||
92 | * |
||||
93 | * @var string |
||||
94 | */ |
||||
95 | private $css_source; |
||||
96 | |||||
97 | /** |
||||
98 | * Where do we read JS files. |
||||
99 | * |
||||
100 | * @var string |
||||
101 | */ |
||||
102 | private $js_source; |
||||
103 | |||||
104 | /** |
||||
105 | * Where do we write CSS/JS files. |
||||
106 | * |
||||
107 | * @var string |
||||
108 | */ |
||||
109 | private $destination; |
||||
110 | |||||
111 | /** |
||||
112 | * Where does the client read CSS/JS files. |
||||
113 | * |
||||
114 | * @var string |
||||
115 | */ |
||||
116 | private $destination_url; |
||||
117 | |||||
118 | /** |
||||
119 | * How to process CSS files. |
||||
120 | * |
||||
121 | * @var FilterInterface[] |
||||
122 | */ |
||||
123 | private $css_filters; |
||||
124 | |||||
125 | /** |
||||
126 | * How to process JS files. |
||||
127 | * |
||||
128 | * @var FilterInterface[] |
||||
129 | */ |
||||
130 | private $js_filters; |
||||
131 | |||||
132 | /** |
||||
133 | * How to load external files. |
||||
134 | * |
||||
135 | * @var LoaderInterface |
||||
136 | */ |
||||
137 | private $loader; |
||||
138 | |||||
139 | /** |
||||
140 | * Do something when we create an asset file. |
||||
141 | * |
||||
142 | * @var NotifierInterface[] |
||||
143 | */ |
||||
144 | private $notifiers; |
||||
145 | |||||
146 | /** |
||||
147 | * Assets smaller than this will be rendered inline, saving an HTTP request. |
||||
148 | * |
||||
149 | * @var int |
||||
150 | */ |
||||
151 | private $inline_threshold; |
||||
152 | |||||
153 | /** |
||||
154 | * Create compressed version of assets, to support the NGINX gzip_static option. |
||||
155 | * |
||||
156 | * @var int |
||||
157 | */ |
||||
158 | private $gzip_static; |
||||
159 | |||||
160 | /** |
||||
161 | * Predefined sets of resources. Can be nested to arbitrary depth. |
||||
162 | * |
||||
163 | * @var string[]|array[] |
||||
164 | */ |
||||
165 | private $collections; |
||||
166 | |||||
167 | /** |
||||
168 | * CSS assets to be processed |
||||
169 | * |
||||
170 | * @var string[][] |
||||
171 | */ |
||||
172 | private $css_assets = array(); |
||||
173 | |||||
174 | /** |
||||
175 | * Javascript assets to be processed |
||||
176 | * |
||||
177 | * @var string[][] |
||||
178 | */ |
||||
179 | private $js_assets = array(); |
||||
180 | |||||
181 | /** |
||||
182 | * The filesystem corresponding to our public path. |
||||
183 | * |
||||
184 | * @var Filesystem |
||||
185 | */ |
||||
186 | private $public; |
||||
187 | |||||
188 | /** |
||||
189 | * Create an asset manager. |
||||
190 | * |
||||
191 | * @param array $config The local config, merged with the default config |
||||
192 | * @param Filesystem $filesystem The public filesystem, where we read/write assets |
||||
193 | */ |
||||
194 | public function __construct(array $config, Filesystem $filesystem) |
||||
195 | { |
||||
196 | $this |
||||
197 | ->setEnabled($config['enabled']) |
||||
198 | ->setCssSource($config['css_source']) |
||||
199 | ->setJsSource($config['js_source']) |
||||
200 | ->setDestination($config['destination']) |
||||
201 | ->setDestinationUrl($config['destination_url']) |
||||
202 | ->setCssFilters($config['css_filters']) |
||||
203 | ->setJsFilters($config['js_filters']) |
||||
204 | ->setLoader($config['loader']) |
||||
205 | ->setNotifiers($config['notifiers']) |
||||
206 | ->setInlineThreshold($config['inline_threshold']) |
||||
207 | ->setGzipStatic($config['gzip_static']) |
||||
208 | ->setCollections($config['collections']); |
||||
209 | |||||
210 | $this->public = $filesystem; |
||||
211 | } |
||||
212 | |||||
213 | /** |
||||
214 | * @param string $css_source |
||||
215 | * |
||||
216 | * @return Assets |
||||
217 | */ |
||||
218 | public function setCssSource($css_source) |
||||
219 | { |
||||
220 | $this->css_source = trim($css_source, '/'); |
||||
221 | |||||
222 | return $this; |
||||
223 | } |
||||
224 | |||||
225 | /** |
||||
226 | * @return string |
||||
227 | */ |
||||
228 | public function getCssSource() |
||||
229 | { |
||||
230 | return $this->css_source; |
||||
231 | } |
||||
232 | |||||
233 | /** |
||||
234 | * @param string $js_source |
||||
235 | * |
||||
236 | * @return Assets |
||||
237 | */ |
||||
238 | public function setJsSource($js_source) |
||||
239 | { |
||||
240 | $this->js_source = trim($js_source, '/'); |
||||
241 | |||||
242 | return $this; |
||||
243 | } |
||||
244 | |||||
245 | /** |
||||
246 | * @return string |
||||
247 | */ |
||||
248 | public function getJsSource() |
||||
249 | { |
||||
250 | return $this->js_source; |
||||
251 | } |
||||
252 | |||||
253 | /** |
||||
254 | * @param string $destination |
||||
255 | * |
||||
256 | * @return Assets |
||||
257 | */ |
||||
258 | public function setDestination($destination) |
||||
259 | { |
||||
260 | $this->destination = trim($destination, '/'); |
||||
261 | |||||
262 | return $this; |
||||
263 | } |
||||
264 | |||||
265 | /** |
||||
266 | * @return string |
||||
267 | */ |
||||
268 | public function getDestination() |
||||
269 | { |
||||
270 | return $this->destination; |
||||
271 | } |
||||
272 | |||||
273 | /** |
||||
274 | * An (optional) absolute URL for fetching generated assets. |
||||
275 | * |
||||
276 | * @param string $destination_url |
||||
277 | * |
||||
278 | * @return Assets |
||||
279 | */ |
||||
280 | public function setDestinationUrl($destination_url) |
||||
281 | { |
||||
282 | $this->destination_url = rtrim($destination_url, '/'); |
||||
283 | |||||
284 | return $this; |
||||
285 | } |
||||
286 | |||||
287 | /** |
||||
288 | * @return string |
||||
289 | */ |
||||
290 | public function getDestinationUrl() |
||||
291 | { |
||||
292 | return $this->destination_url; |
||||
293 | } |
||||
294 | |||||
295 | /** |
||||
296 | * @param FilterInterface[] $css_filters |
||||
297 | * |
||||
298 | * @return Assets |
||||
299 | */ |
||||
300 | public function setCssFilters(array $css_filters) |
||||
301 | { |
||||
302 | $this->css_filters = $css_filters; |
||||
303 | |||||
304 | return $this; |
||||
305 | } |
||||
306 | |||||
307 | /** |
||||
308 | * @return FilterInterface[] |
||||
309 | */ |
||||
310 | public function getCssFilters() |
||||
311 | { |
||||
312 | return $this->css_filters; |
||||
313 | } |
||||
314 | |||||
315 | /** |
||||
316 | * @param FilterInterface[] $js_filters |
||||
317 | * |
||||
318 | * @return Assets |
||||
319 | */ |
||||
320 | public function setJsFilters(array $js_filters) |
||||
321 | { |
||||
322 | $this->js_filters = $js_filters; |
||||
323 | |||||
324 | return $this; |
||||
325 | } |
||||
326 | |||||
327 | /** |
||||
328 | * @return FilterInterface[] |
||||
329 | */ |
||||
330 | public function getJsFilters() |
||||
331 | { |
||||
332 | return $this->js_filters; |
||||
333 | } |
||||
334 | |||||
335 | /** |
||||
336 | * @param LoaderInterface $loader |
||||
337 | * |
||||
338 | * @return Assets |
||||
339 | */ |
||||
340 | public function setLoader($loader) |
||||
341 | { |
||||
342 | $this->loader = $loader; |
||||
343 | |||||
344 | return $this; |
||||
345 | } |
||||
346 | |||||
347 | /** |
||||
348 | * @return LoaderInterface |
||||
349 | */ |
||||
350 | public function getLoader() |
||||
351 | { |
||||
352 | return $this->loader; |
||||
353 | } |
||||
354 | |||||
355 | /** |
||||
356 | * @param NotifierInterface[] $notifiers |
||||
357 | * |
||||
358 | * @return Assets |
||||
359 | */ |
||||
360 | public function setNotifiers(array $notifiers) |
||||
361 | { |
||||
362 | $this->notifiers = $notifiers; |
||||
363 | |||||
364 | return $this; |
||||
365 | } |
||||
366 | |||||
367 | /** |
||||
368 | * @return NotifierInterface[] |
||||
369 | */ |
||||
370 | public function getNotifiers() |
||||
371 | { |
||||
372 | return $this->notifiers; |
||||
373 | } |
||||
374 | |||||
375 | /** |
||||
376 | * @param boolean $enabled |
||||
377 | * |
||||
378 | * @return Assets |
||||
379 | */ |
||||
380 | public function setEnabled($enabled) |
||||
381 | { |
||||
382 | $this->enabled = (bool) $enabled; |
||||
383 | |||||
384 | return $this; |
||||
385 | } |
||||
386 | |||||
387 | /** |
||||
388 | * @return boolean |
||||
389 | */ |
||||
390 | public function isEnabled() |
||||
391 | { |
||||
392 | return $this->enabled; |
||||
393 | } |
||||
394 | |||||
395 | /** |
||||
396 | * @param int $inline_threshold |
||||
397 | * |
||||
398 | * @return Assets |
||||
399 | */ |
||||
400 | public function setInlineThreshold($inline_threshold) |
||||
401 | { |
||||
402 | $this->inline_threshold = (int) $inline_threshold; |
||||
403 | |||||
404 | return $this; |
||||
405 | } |
||||
406 | |||||
407 | /** |
||||
408 | * @return int |
||||
409 | */ |
||||
410 | public function getInlineThreshold() |
||||
411 | { |
||||
412 | return $this->inline_threshold; |
||||
413 | } |
||||
414 | |||||
415 | /** |
||||
416 | * @param int $gzip_static |
||||
417 | * |
||||
418 | * @return Assets |
||||
419 | */ |
||||
420 | public function setGzipStatic($gzip_static) |
||||
421 | { |
||||
422 | $this->gzip_static = (int) $gzip_static; |
||||
423 | |||||
424 | return $this; |
||||
425 | } |
||||
426 | |||||
427 | /** |
||||
428 | * @return int |
||||
429 | */ |
||||
430 | public function getGzipStatic() |
||||
431 | { |
||||
432 | return $this->gzip_static; |
||||
433 | } |
||||
434 | |||||
435 | /** |
||||
436 | * @param array[]|string[] $collections |
||||
437 | * |
||||
438 | * @return Assets |
||||
439 | */ |
||||
440 | public function setCollections($collections) |
||||
441 | { |
||||
442 | $this->collections = $collections; |
||||
443 | |||||
444 | return $this; |
||||
445 | } |
||||
446 | |||||
447 | /** |
||||
448 | * @return array[]|string[] |
||||
449 | */ |
||||
450 | public function getCollections() |
||||
451 | { |
||||
452 | return $this->collections; |
||||
453 | } |
||||
454 | |||||
455 | /** |
||||
456 | * Add one or more assets. |
||||
457 | * |
||||
458 | * @param string|string[] $asset A local filename, a remote URL or the name of a collection. |
||||
459 | * @param string $type Force a file type, "css" or "js", instead of using the extension. |
||||
460 | * @param string $group Optionally split your assets into multiple groups, such as "head" and "body". |
||||
461 | * |
||||
462 | * @return Assets |
||||
463 | */ |
||||
464 | public function add($asset, $type = self::TYPE_AUTO, $group = self::GROUP_DEFAULT) |
||||
465 | { |
||||
466 | $this->checkGroupExists($group); |
||||
467 | |||||
468 | if (is_array($asset)) { |
||||
469 | foreach ($asset as $a) { |
||||
470 | $this->add($a, $type, $group); |
||||
471 | } |
||||
472 | } elseif ($type === self::TYPE_CSS || $type === self::TYPE_AUTO && preg_match(self::REGEX_CSS, $asset)) { |
||||
473 | if (!in_array($asset, $this->css_assets[$group], true)) { |
||||
474 | $this->css_assets[$group][] = $asset; |
||||
475 | } |
||||
476 | } elseif ($type === self::TYPE_JS || $type === self::TYPE_AUTO && preg_match(self::REGEX_JS, $asset)) { |
||||
477 | if (!in_array($asset, $this->js_assets[$group], true)) { |
||||
478 | $this->js_assets[$group][] = $asset; |
||||
479 | } |
||||
480 | } elseif (array_key_exists($asset, $this->collections)) { |
||||
481 | $this->add($this->collections[$asset], $type, $group); |
||||
482 | } else { |
||||
483 | throw new InvalidArgumentException('Unknown asset type: ' . $asset); |
||||
484 | } |
||||
485 | |||||
486 | return $this; |
||||
487 | } |
||||
488 | |||||
489 | /** |
||||
490 | * Render markup to load the CSS assets. |
||||
491 | * |
||||
492 | * @param string $group Optionally split your assets into multiple groups, such as "head" and "body". |
||||
493 | * @param string[] $attributes Optional attributes, such as ['media' => 'print'] |
||||
494 | * |
||||
495 | * @return string |
||||
496 | * @throws FileExistsException |
||||
497 | * @throws FileNotFoundException |
||||
498 | */ |
||||
499 | public function css($group = self::GROUP_DEFAULT, array $attributes = []) |
||||
500 | { |
||||
501 | $this->checkGroupExists($group); |
||||
502 | |||||
503 | return $this->processAssets( |
||||
504 | $attributes, |
||||
505 | $this->css_assets[$group], |
||||
506 | '.css', |
||||
507 | $this->getCssSource(), |
||||
508 | $this->getCssFilters(), |
||||
509 | self::FORMAT_CSS_LINK, |
||||
510 | self::FORMAT_CSS_INLINE |
||||
511 | ); |
||||
512 | } |
||||
513 | |||||
514 | /** |
||||
515 | * Render markup to load the JS assets. |
||||
516 | * |
||||
517 | * @param string $group Optionally split your assets into multiple groups, such as "head" and "body". |
||||
518 | * @param string[] $attributes Optional attributes, such as ['async'] |
||||
519 | * |
||||
520 | * @return string |
||||
521 | * @throws FileExistsException |
||||
522 | * @throws FileNotFoundException |
||||
523 | */ |
||||
524 | public function js($group = self::GROUP_DEFAULT, array $attributes = []) |
||||
525 | { |
||||
526 | $this->checkGroupExists($group); |
||||
527 | |||||
528 | return $this->processAssets( |
||||
529 | $attributes, |
||||
530 | $this->js_assets[$group], |
||||
531 | '.js', |
||||
532 | $this->getJsSource(), |
||||
533 | $this->getJsFilters(), |
||||
534 | self::FORMAT_JS_LINK, |
||||
535 | self::FORMAT_JS_INLINE |
||||
536 | ); |
||||
537 | } |
||||
538 | |||||
539 | /** |
||||
540 | * Is a URL absolute or relative? |
||||
541 | * |
||||
542 | * @param string $url |
||||
543 | * |
||||
544 | * @return bool |
||||
545 | */ |
||||
546 | public function isAbsoluteUrl($url) |
||||
547 | { |
||||
548 | return preg_match(self::REGEX_EXTERNAL_URL, $url) === 1; |
||||
549 | } |
||||
550 | |||||
551 | /** |
||||
552 | * Normalize a path, removing '.' and '..' folders. e.g. |
||||
553 | * |
||||
554 | * "a/b/./c/../../d" becomes "a/d" |
||||
555 | * |
||||
556 | * @param string $url |
||||
557 | * |
||||
558 | * @return string |
||||
559 | */ |
||||
560 | public function normalizePath($url) |
||||
561 | { |
||||
562 | while (strpos($url, '/./') !== false) { |
||||
563 | $url = str_replace('/./', '/', $url); |
||||
564 | } |
||||
565 | while (strpos($url, '/../') !== false) { |
||||
566 | $url = preg_replace('/[^\/]+\/\.\.\//', '', $url, 1); |
||||
567 | } |
||||
568 | |||||
569 | return $url; |
||||
570 | } |
||||
571 | |||||
572 | /** |
||||
573 | * Create a relative path between two URLs. |
||||
574 | * |
||||
575 | * e.g. the relative path from "a/b/c" to "a/d" is "../../d" |
||||
576 | * |
||||
577 | * @param string $source |
||||
578 | * @param string $destination |
||||
579 | * |
||||
580 | * @return string |
||||
581 | */ |
||||
582 | public function relativePath($source, $destination) |
||||
583 | { |
||||
584 | if ($source === '') { |
||||
585 | return $destination; |
||||
586 | } |
||||
587 | |||||
588 | $parts1 = explode('/', $source); |
||||
589 | $parts2 = explode('/', $destination); |
||||
590 | |||||
591 | while (!empty($parts1) && !empty($parts2) && $parts1[0] === $parts2[0]) { |
||||
592 | array_shift($parts1); |
||||
593 | array_shift($parts2); |
||||
594 | } |
||||
595 | |||||
596 | return str_repeat('../', count($parts1)) . implode('/', $parts2); |
||||
597 | } |
||||
598 | |||||
599 | /** |
||||
600 | * Purge generated assets older than a given number of days |
||||
601 | * |
||||
602 | * @param Purge $command |
||||
603 | * |
||||
604 | * @throws FileNotFoundException |
||||
605 | */ |
||||
606 | public function purge(Purge $command) |
||||
607 | { |
||||
608 | $days = (int) $command->option('days'); |
||||
609 | $verbose = (bool) $command->option('verbose'); |
||||
610 | $files = $this->public->listContents($this->getDestination(), true); |
||||
611 | $timestamp = time() - $days * 86400; |
||||
612 | |||||
613 | foreach ($files as $file) { |
||||
614 | if ($this->needsPurge($file, $timestamp)) { |
||||
615 | $this->public->delete($file['path']); |
||||
616 | $command->info('Deleted: ' . $file['path']); |
||||
617 | } elseif ($verbose) { |
||||
618 | $command->info('Keeping: ' . $file['path']); |
||||
619 | } |
||||
620 | } |
||||
621 | } |
||||
622 | |||||
623 | /** |
||||
624 | * Render markup to load the CSS or JS assets. |
||||
625 | * |
||||
626 | * @param string[] $attributes Optional attributes, such as ['async'] |
||||
627 | * @param string[] $assets The files to be processed |
||||
628 | * @param string $extension ".css" or ".js" |
||||
629 | * @param string $source_dir The folder containing the source assets |
||||
630 | * @param FilterInterface[] $filters How to process these assets |
||||
631 | * @param string $format_link Template for an HTML link to the asset |
||||
632 | * @param string $format_inline Template for an inline asset |
||||
633 | * |
||||
634 | * @return string |
||||
635 | * @throws FileNotFoundException |
||||
636 | * @throws FileExistsException |
||||
637 | */ |
||||
638 | private function processAssets( |
||||
639 | array $attributes, |
||||
640 | array $assets, |
||||
641 | $extension, |
||||
642 | $source_dir, |
||||
643 | $filters, |
||||
644 | $format_link, |
||||
645 | $format_inline |
||||
646 | ) { |
||||
647 | $hashes = []; |
||||
648 | $path = $this->getDestination(); |
||||
649 | |||||
650 | foreach ($assets as $asset) { |
||||
651 | if ($this->isAbsoluteUrl($asset)) { |
||||
652 | $hash = $this->hash($asset); |
||||
653 | } else { |
||||
654 | $hash = $this->hash($asset . $this->public->getTimestamp($source_dir . '/' . $asset)); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
655 | } |
||||
656 | if (!$this->public->has($path . '/' . $hash . $extension)) { |
||||
657 | if ($this->isAbsoluteUrl($asset)) { |
||||
658 | $data = $this->getLoader()->loadUrl($asset); |
||||
659 | } else { |
||||
660 | $data = $this->public->read($source_dir . '/' . $asset); |
||||
661 | } |
||||
662 | foreach ($filters as $filter) { |
||||
663 | $data = $filter->filter($data, $asset, $this); |
||||
664 | } |
||||
665 | $this->public->write($path . '/' . $hash . $extension, $data); |
||||
666 | $this->public->write($path . '/' . $hash . '.min' . $extension, $data); |
||||
667 | } |
||||
668 | $hashes[] = $hash; |
||||
669 | } |
||||
670 | |||||
671 | // The file name of our pipelined asset. |
||||
672 | $hash = $this->hash(implode('', $hashes)); |
||||
673 | $asset_file = $path . '/' . $hash . '.min' . $extension; |
||||
674 | |||||
675 | $this->concatenateFiles($path, $hashes, $hash, $extension); |
||||
676 | $this->concatenateFiles($path, $hashes, $hash, '.min' . $extension); |
||||
677 | |||||
678 | $this->createGzip($asset_file); |
||||
679 | |||||
680 | foreach ($this->notifiers as $notifier) { |
||||
681 | $notifier->created($asset_file); |
||||
682 | } |
||||
683 | |||||
684 | if ($this->getDestinationUrl() === '') { |
||||
685 | $url = url($path); |
||||
686 | } else { |
||||
687 | $url = $this->getDestinationUrl(); |
||||
688 | } |
||||
689 | |||||
690 | if ($this->isEnabled()) { |
||||
691 | $inline_threshold = $this->getInlineThreshold(); |
||||
692 | if ($inline_threshold > 0 && $this->public->getSize($asset_file) <= $inline_threshold) { |
||||
693 | return sprintf($format_inline, $this->public->read($asset_file)); |
||||
0 ignored issues
–
show
It seems like
$this->public->read($asset_file) can also be of type false ; however, parameter $values of sprintf() does only seem to accept double|integer|string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
694 | } |
||||
695 | |||||
696 | return $this->htmlLinks($url, [$hash], '.min' . $extension, $format_link, $attributes); |
||||
697 | } |
||||
698 | |||||
699 | return $this->htmlLinks($url, $hashes, $extension, $format_link, $attributes); |
||||
700 | } |
||||
701 | |||||
702 | /** |
||||
703 | * Make sure that the specified group (i.e. array key) exists. |
||||
704 | * |
||||
705 | * @param string $group |
||||
706 | */ |
||||
707 | private function checkGroupExists($group) |
||||
708 | { |
||||
709 | if (!array_key_exists($group, $this->css_assets)) { |
||||
710 | $this->css_assets[$group] = []; |
||||
711 | } |
||||
712 | if (!array_key_exists($group, $this->js_assets)) { |
||||
713 | $this->js_assets[$group] = []; |
||||
714 | } |
||||
715 | } |
||||
716 | |||||
717 | /** |
||||
718 | * Concatenate a number of files. |
||||
719 | * |
||||
720 | * @param string $path subfolder containing assets to be combined |
||||
721 | * @param string[] $sources Filenames (without extension) to be combined |
||||
722 | * @param string $destination Filename (without extension) to be created |
||||
723 | * @param string $extension ".css", ".min.js", etc. |
||||
724 | * |
||||
725 | * @throws FileNotFoundException |
||||
726 | * @throws FileExistsException |
||||
727 | */ |
||||
728 | private function concatenateFiles($path, $sources, $destination, $extension) |
||||
729 | { |
||||
730 | if (!$this->public->has($path . '/' . $destination . $extension)) { |
||||
731 | $data = ''; |
||||
732 | foreach ($sources as $source) { |
||||
733 | $data .= $this->public->read($path . '/' . $source . $extension); |
||||
734 | } |
||||
735 | $this->public->write($path . '/' . $destination . $extension, $data); |
||||
736 | } |
||||
737 | } |
||||
738 | |||||
739 | /** |
||||
740 | * Generate a hash, to use as a filename for generated assets. |
||||
741 | * |
||||
742 | * @param string $text |
||||
743 | * |
||||
744 | * @return string |
||||
745 | */ |
||||
746 | private function hash($text) |
||||
747 | { |
||||
748 | return md5($text); |
||||
749 | } |
||||
750 | |||||
751 | /** |
||||
752 | * Optionally create a .gz version of a file - to support the NGINX gzip_static option. |
||||
753 | * |
||||
754 | * @param string $path |
||||
755 | * |
||||
756 | * @throws FileNotFoundException |
||||
757 | * @throws FileExistsException |
||||
758 | */ |
||||
759 | private function createGzip($path) |
||||
760 | { |
||||
761 | $gzip = $this->getGzipStatic(); |
||||
762 | |||||
763 | if ($gzip >= 1 && $gzip <= 9 && function_exists('gzcompress') && !$this->public->has($path . '.gz')) { |
||||
764 | $content = $this->public->read($path); |
||||
765 | $content_gz = gzcompress($content, $gzip); |
||||
0 ignored issues
–
show
It seems like
$content can also be of type false ; however, parameter $data of gzcompress() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
766 | $this->public->write($path . '.gz', $content_gz); |
||||
767 | } |
||||
768 | } |
||||
769 | |||||
770 | /** |
||||
771 | * Generate HTML links to a list of processed asset files. |
||||
772 | * |
||||
773 | * @param string $url path to the assets |
||||
774 | * @param string[] $hashes base filename |
||||
775 | * @param string $extension ".css", ".min.js", etc. |
||||
776 | * @param string $format |
||||
777 | * @param string[] $attributes |
||||
778 | * |
||||
779 | * @return string |
||||
780 | */ |
||||
781 | private function htmlLinks($url, $hashes, $extension, $format, $attributes) |
||||
782 | { |
||||
783 | $html_attributes = $this->convertAttributesToHtml($attributes); |
||||
784 | |||||
785 | $html_links = ''; |
||||
786 | foreach ($hashes as $asset) { |
||||
787 | $html_links .= sprintf($format, $html_attributes, $url . '/' . $asset . $extension); |
||||
788 | } |
||||
789 | |||||
790 | return $html_links; |
||||
791 | } |
||||
792 | |||||
793 | /** |
||||
794 | * Convert an array of attributes to HTML. |
||||
795 | * |
||||
796 | * @param string[] $attributes |
||||
797 | * |
||||
798 | * @return string |
||||
799 | */ |
||||
800 | private function convertAttributesToHtml(array $attributes) |
||||
801 | { |
||||
802 | $html = ''; |
||||
803 | foreach ($attributes as $key => $value) { |
||||
804 | if (is_int($key)) { |
||||
805 | $html .= ' ' . $value; |
||||
806 | } else { |
||||
807 | $html .= ' ' . $key . '="' . $value . '"'; |
||||
808 | } |
||||
809 | } |
||||
810 | |||||
811 | return $html; |
||||
812 | } |
||||
813 | |||||
814 | /** |
||||
815 | * @param array $file |
||||
816 | * @param int $timestamp |
||||
817 | * |
||||
818 | * @return bool |
||||
819 | */ |
||||
820 | private function needsPurge(array $file, $timestamp) |
||||
821 | { |
||||
822 | $eligible = preg_match(self::REGEX_JS, $file['path']) || preg_match(self::REGEX_CSS, $file['path']); |
||||
823 | |||||
824 | return $eligible && $file['timestamp'] <= $timestamp; |
||||
825 | } |
||||
826 | } |
||||
827 |