Passed
Push — configuration ( 0ed11e...2208fe )
by Arnaud
04:08 queued 11s
created

Asset::minify()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 13.1488

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 28
c 2
b 0
f 0
nc 11
nop 0
dl 0
loc 44
ccs 17
cts 23
cp 0.7391
crap 13.1488
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Assets\Image\Optimizer;
17
use Cecil\Builder;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\ConfigException;
21
use Cecil\Exception\RuntimeException;
22
use Cecil\Url;
23
use Cecil\Util;
24
use MatthiasMullie\Minify;
25
use ScssPhp\ScssPhp\Compiler;
26
use ScssPhp\ScssPhp\OutputStyle;
27
use wapmorgan\Mp3Info\Mp3Info;
28
29
class Asset implements \ArrayAccess
30
{
31
    /** @var Builder */
32
    protected $builder;
33
34
    /** @var Config */
35
    protected $config;
36
37
    /** @var array */
38
    protected $data = [];
39
40
    /** @var array Cache tags */
41
    protected $tags = [];
42
43
    /**
44
     * Creates an Asset from a file path, an array of files path or an URL.
45
     * Options:
46
     * [
47
     *     'fingerprint' => <bool>,
48
     *     'minify' => <bool>,
49
     *     'optimize' => <bool>,
50
     *     'filename' => <string>,
51
     *     'ignore_missing' => <bool>,
52
     *     'fallback' => <string>,
53
     *     'leading_slash' => <bool>
54
     * ]
55
     *
56
     * @param Builder      $builder
57
     * @param string|array $paths
58
     * @param array|null   $options
59
     *
60
     * @throws RuntimeException
61
     */
62
    public function __construct(Builder $builder, string|array $paths, array|null $options = null)
63
    {
64
        $this->builder = $builder;
65
        $this->config = $builder->getConfig();
66
        $paths = \is_array($paths) ? $paths : [$paths];
67
        // checks path(s)
68 1
        array_walk($paths, function ($path) {
69
            // must be a string
70 1
            if (!\is_string($path)) {
71 1
                throw new RuntimeException(\sprintf('The path of an asset must be a string ("%s" given).', \gettype($path)));
72 1
            }
73
            // can't be empty
74 1
            if (empty($path)) {
75
                throw new RuntimeException('The path of an asset can\'t be empty.');
76 1
            }
77
            // can't be relative
78
            if (substr($path, 0, 2) == '..') {
79
                throw new RuntimeException(\sprintf('The path of asset "%s" is wrong: it must be directly relative to `assets` or `static` directory, or a remote URL.', $path));
80 1
            }
81
        });
82
        $this->data = [
83
            'file'     => '',    // absolute file path
84 1
            'files'    => [],    // array of absolute files path
85
            'path'     => '',    // public path
86
            'url'      => null,  // URL if it's a remote file
87 1
            'missing'  => false, // if file not found but missing allowed: 'missing' is true
88 1
            'ext'      => '',    // file extension
89 1
            'type'     => '',    // file type (e.g.: image, audio, video, etc.)
90 1
            'subtype'  => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
91 1
            'size'     => 0,     // file size (in bytes)
92 1
            'width'    => 0,     // image width (in pixels)
93 1
            'height'   => 0,     // image height (in pixels)
94 1
            'exif'     => [],    // image exif data
95 1
            'content'  => '',    // file content
96 1
        ];
97 1
98 1
        // handles options
99 1
        $options = array_merge(
100 1
            [
101 1
                'fingerprint'    => $this->config->isEnabled('assets.fingerprint'),
102 1
                'minify'         => $this->config->isEnabled('assets.minify'),
103
                'optimize'       => $this->config->isEnabled('assets.images.optimize'),
104
                'filename'       => '',
105 1
                'ignore_missing' => false,
106 1
                'fallback'       => null,
107 1
                'leading_slash'  => true,
108 1
            ],
109 1
            \is_array($options) ? $options : []
110 1
        );
111 1
        // cache tags
112 1
        foreach ($options as $key => $value) {
113 1
            if (\is_bool($value) && $value === true) {
114 1
                $this->tags[] = $key;
115 1
            }
116 1
            if (\is_string($value) && !empty($value)) {
117
                $this->tags[] = $value;
118 1
            }
119 1
        }
120 1
        // DEBUG
121 1
        //echo implode('_', $this->tags) . "\n";
122
        //echo hash('crc32', implode('_', $this->tags)) . "\n";
123 1
        //die('debug');
124 1
125
        // cache
126
        //$cache = new Cache($this->builder, 'assets');
127
        //$cacheKey = $cache->createKeyFromAsset($this, $fingerprint ? ['fingerprint'] : []);
128
129
        //$cacheKey = \sprintf('%s__%s', $filename ?: implode('_', $paths), $this->builder->getVersion());
130
131
        // locate file(s) and get content
132
        $pathsCount = \count($paths);
133
        for ($i = 0; $i < $pathsCount; $i++) {
134
            try {
135
                $this->data['missing'] = false;
136
                $locate = $this->locateFile($paths[$i], $options['fallback']);
137
                $file = $locate['file'];
138
                $path = $locate['path'];
139 1
                $type = Util\File::getMediaType($file)[0];
140 1
                if ($i > 0) { // bundle
141
                    if ($type != $this->data['type']) {
142 1
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $type, $this->data['type']));
143 1
                    }
144 1
                }
145 1
                $this->data['file'] = $file;
146 1
                $this->data['files'][] = $file;
147 1
                $this->data['path'] = $path;
148 1
                $this->data['url'] = Util\File::isRemote($paths[$i]) ? $paths[$i] : null;
149
                $this->data['ext'] = Util\File::getExtension($file);
150
                $this->data['type'] = $type;
151
                $this->data['subtype'] = Util\File::getMediaType($file)[1];
152 1
                $this->data['size'] += filesize($file);
153 1
                $this->data['content'] .= Util\File::fileGetContents($file);
154 1
                // bundle default filename
155 1
                $filename = $options['filename'];
156 1
                if ($pathsCount > 1 && empty($filename)) {
157 1
                    switch ($this->data['ext']) {
158 1
                        case 'scss':
159 1
                        case 'css':
160 1
                            $filename = 'styles.css';
161
                            break;
162 1
                        case 'js':
163 1
                            $filename = 'scripts.js';
164 1
                            break;
165 1
                        default:
166 1
                            throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
167 1
                    }
168 1
                }
169 1
                // apply bundle filename to path
170 1
                if (!empty($filename)) {
171 1
                    $this->data['path'] = '/' . ltrim($filename, '/');
172
                }
173
                // force root slash
174
                if ($options['leading_slash']) {
175
                    $this->data['path'] = '/' . ltrim($this->data['path'], '/');
176
                }
177 1
            } catch (RuntimeException $e) {
178 1
                if ($options['ignore_missing']) {
179
                    $this->data['missing'] = true;
180
                    continue;
181 1
                }
182 1
                throw new RuntimeException(\sprintf('Can\'t handle asset "%s" (%s).', $paths[$i], $e->getMessage()));
183
            }
184 1
        }
185 1
186 1
        // missing
187 1
        if ($this->data['missing']) {
188
            return;
189
        }
190
191
        // cache
192
        $cache = new Cache($this->builder, 'assets');
193
        $cacheKey = $cache->createKeyFromAsset($this, $this->tags);
194 1
        if (!$cache->has($cacheKey)) {
195 1
            // image: width, height and exif
196
            if ($this->data['type'] == 'image') {
197
                $this->data['width'] = $this->getWidth();
198
                $this->data['height'] = $this->getHeight();
199 1
                if ($this->data['subtype'] == 'image/jpeg') {
200 1
                    $this->data['exif'] = Util\File::readExif($this->data['file']);
201 1
                }
202
            }
203 1
            // fingerprinting
204 1
            if ($options['fingerprint']) {
205 1
                $this->fingerprint();
206 1
            }
207 1
            $cache->set($cacheKey, $this->data);
208
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
209
            // optimizing images files (in cache)
210
            if ($options['optimize'] && $this->data['type'] == 'image' && !$this->isImageInCdn()) {
211 1
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
212 1
            }
213
        }
214 1
        $this->data = $cache->get($cacheKey);
215 1
216
        // compiling Sass files
217 1
        $this->compile();
218 1
        // minifying (CSS and JavScript files)
219
        if ($options['minify']) {
220
            $this->minify();
221 1
        }
222
    }
223
224 1
    /**
225
     * Returns path.
226 1
     */
227 1
    public function __toString(): string
228
    {
229
        $this->save();
230
231
        if ($this->isImageInCdn()) {
232
            return $this->buildImageCdnUrl();
233
        }
234 1
235
        if ($this->builder->getConfig()->isEnabled('canonicalurl')) {
236 1
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
237
        }
238 1
239
        return $this->data['path'];
240
    }
241
242 1
    /**
243
     * Compiles a SCSS.
244
     *
245
     * @throws RuntimeException
246 1
     */
247
    public function compile(): self
248
    {
249
        if ($this->data['ext'] != 'scss') {
250
            return $this;
251
        }
252
253
        $cache = new Cache($this->builder, 'assets');
254 1
255
        $this->tags[] = 'compile';
256 1
        $this->tags = array_unique($this->tags);
257 1
258
        $cacheKey = $cache->createKeyFromAsset($this, $this->tags);
259
        if (!$cache->has($cacheKey)) {
260 1
            $scssPhp = new Compiler();
261 1
            // import paths
262
            $importDir = [];
263
            $importDir[] = Util::joinPath($this->config->getStaticPath());
264 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
265 1
            $scssDir = (array) $this->config->get('assets.compile.import');
266 1
            $themes = $this->config->getTheme() ?? [];
267 1
            foreach ($scssDir as $dir) {
268
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
269 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
270 1
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
271 1
                foreach ($themes as $theme) {
272 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
273 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
274 1
                }
275 1
            }
276 1
            $scssPhp->setQuietDeps(true);
277 1
            $scssPhp->setImportPaths(array_unique($importDir));
278 1
            // source map
279 1
            if ($this->builder->isDebug() && $this->config->isEnabled('assets.compile.sourcemap')) {
280 1
                $importDir = [];
281
                $assetDir = (string) $this->config->get('assets.dir');
282
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
283 1
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
284 1
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
285
                $importDir[] = \dirname($filePath);
286 1
                foreach ($scssDir as $dir) {
287
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
288
                }
289
                $scssPhp->setImportPaths(array_unique($importDir));
290
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
291
                $scssPhp->setSourceMapOptions([
292
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
293
                    'sourceRoot'        => '/',
294
                ]);
295
            }
296
            // output style
297
            $outputStyles = ['expanded', 'compressed'];
298
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
299
            if (!\in_array($outputStyle, $outputStyles)) {
300
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
301
            }
302
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
303
            // variables
304 1
            $variables = $this->config->get('assets.compile.variables');
305 1
            if (!empty($variables)) {
306 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
307
                $scssPhp->replaceVariables($variables);
308
            }
309 1
            // debug
310
            if ($this->builder->isDebug()) {
311 1
                $scssPhp->setQuietDeps(false);
312 1
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
313 1
            }
314 1
            // update data
315
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
316
            $this->data['ext'] = 'css';
317 1
            $this->data['type'] = 'text';
318 1
            $this->data['subtype'] = 'text/css';
319 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
320
            $this->data['size'] = \strlen($this->data['content']);
321
            $cache->set($cacheKey, $this->data);
322 1
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
323 1
        }
324 1
        $this->data = $cache->get($cacheKey);
325 1
326 1
        return $this;
327 1
    }
328 1
329 1
    /**
330 1
     * Minifying a CSS or a JS.
331
     *
332 1
     * @throws RuntimeException
333
     */
334 1
    public function minify(): self
335
    {
336
        // in debug mode, disable minify to preserve inline source map
337
        if ($this->builder->isDebug() && $this->config->isEnabled('assets.compile.sourcemap')) {
338
            return $this;
339
        }
340
341
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
342 1
            return $this;
343
        }
344
345 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
346
            return $this;
347
        }
348
349 1
        if ($this->data['ext'] == 'scss') {
350
            $this->compile();
351
        }
352
353 1
        $cache = new Cache($this->builder, 'assets');
354
355
        $this->tags[] = 'minify';
356
        $this->tags = array_unique($this->tags);
357 1
358
        $cacheKey = $cache->createKeyFromAsset($this, $this->tags);
359
        if (!$cache->has($cacheKey)) {
360
            switch ($this->data['ext']) {
361 1
                case 'css':
362
                    $minifier = new Minify\CSS($this->data['content']);
363
                    break;
364
                case 'js':
365 1
                    $minifier = new Minify\JS($this->data['content']);
366 1
                    break;
367 1
                default:
368 1
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
369 1
            }
370 1
            $this->data['content'] = $minifier->minify();
371 1
            $this->data['size'] = \strlen($this->data['content']);
372 1
            $cache->set($cacheKey, $this->data);
373 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
374 1
        }
375
        $this->data = $cache->get($cacheKey);
376
377
        return $this;
378 1
    }
379 1
380 1
    /**
381 1
     * Add hash to the file name.
382 1
     */
383
    public function fingerprint(): self
384 1
    {
385
        $cache = new Cache($this->builder, 'assets');
386 1
387
        $this->tags[] = 'fingerprint';
388
        $this->tags = array_unique($this->tags);
389
390
        $cacheKey = $cache->createKeyFromAsset($this, $this->tags);
391
        if (!$cache->has($cacheKey)) {
392 1
            $hash = hash('md5', $this->data['content']);
393
            $this->data['path'] = preg_replace(
394 1
                '/\.' . $this->data['ext'] . '$/m',
395
                ".$hash." . $this->data['ext'],
396
                $this->data['path']
397
            );
398 1
            $cache->set($cacheKey, $this->data);
399 1
            $this->builder->getLogger()->debug(\sprintf('Asset fingerprinted: "%s"', $this->data['path']));
400 1
        }
401 1
        $this->data = $cache->get($cacheKey);
402 1
403 1
        return $this;
404 1
    }
405 1
406 1
    /**
407 1
     * Optimizing $filepath image.
408 1
     * Returns the new file size.
409 1
     */
410
    public function optimize(string $filepath, string $path): int
411 1
    {
412
        $quality = (int) $this->config->get('assets.images.quality');
413 1
        $message = \sprintf('Asset processed: "%s"', $path);
414
        $sizeBefore = filesize($filepath);
415
        Optimizer::create($quality)->optimize($filepath);
416
        $sizeAfter = filesize($filepath);
417
        if ($sizeAfter < $sizeBefore) {
418
            $message = \sprintf(
419
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
420 1
                $path,
421
                ceil($sizeBefore / 1000),
422 1
                ceil($sizeAfter / 1000)
423 1
            );
424 1
        }
425 1
        $this->builder->getLogger()->debug($message);
426 1
427 1
        return $sizeAfter;
428
    }
429
430
    /**
431
     * Resizes an image with a new $width.
432
     *
433
     * @throws RuntimeException
434
     */
435 1
    public function resize(int $width): self
436
    {
437 1
        if ($this->data['missing']) {
438
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
439
        }
440
        if ($this->data['type'] != 'image') {
441
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
442
        }
443
        if ($width >= $this->data['width']) {
444
            return $this;
445 1
        }
446
447 1
        $assetResized = clone $this;
448
        $assetResized->data['width'] = $width;
449
450 1
        if ($this->isImageInCdn()) {
451
            $assetResized->data['height'] = round($this->data['height'] / ($this->data['width'] / $width));
452
453 1
            return $assetResized; // returns asset with the new dimensions only: CDN do the rest of the job
454 1
        }
455
456
        $quality = (int) $this->config->get('assets.images.quality');
457 1
        $cache = new Cache($this->builder, 'assets');
458 1
459
        $this->tags[] = "{$width}x";
460 1
        $this->tags[] = "q$quality";
461
        $this->tags = array_unique($this->tags);
462
463
        $cacheKey = $cache->createKeyFromAsset($assetResized, $this->tags);
464
        if (!$cache->has($cacheKey)) {
465
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
466 1
            $assetResized->data['path'] = '/' . Util::joinPath(
467 1
                (string) $this->config->get('assets.target'),
468 1
                'thumbnails',
469 1
                (string) $width,
470 1
                $assetResized->data['path']
471 1
            );
472 1
            $assetResized->data['height'] = $assetResized->getHeight();
473 1
            $assetResized->data['size'] = \strlen($assetResized->data['content']);
474 1
475 1
            $cache->set($cacheKey, $assetResized->data);
476 1
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
477 1
        }
478 1
        $assetResized->data = $cache->get($cacheKey);
479
480 1
        return $assetResized;
481 1
    }
482
483 1
    /**
484
     * Converts an image asset to $format format.
485 1
     *
486
     * @throws RuntimeException
487
     */
488
    public function convert(string $format, ?int $quality = null): self
489
    {
490
        if ($this->data['type'] != 'image') {
491
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
492
        }
493 1
494
        if ($quality === null) {
495 1
            $quality = (int) $this->config->get('assets.images.quality');
496
        }
497
498
        $asset = clone $this;
499 1
        $asset['ext'] = $format;
500 1
        $asset->data['subtype'] = "image/$format";
501
502
        if ($this->isImageInCdn()) {
503 1
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
504 1
        }
505 1
506
        $cache = new Cache($this->builder, 'assets');
507 1
508
        $this->tags[] = "q$quality";
509
        $this->tags = array_unique($this->tags);
510
511 1
        if ($this->data['width']) {
512 1
            $this->tags[] = "{$this->data['width']}x";
513 1
            $this->tags = array_unique($this->tags);
514 1
        }
515
        $cacheKey = $cache->createKeyFromAsset($asset, $this->tags);
516 1
        if (!$cache->has($cacheKey)) {
517 1
            $asset->data['content'] = Image::convert($asset, $format, $quality);
518 1
            $asset->data['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
519
            $asset->data['size'] = \strlen($asset->data['content']);
520
            $cache->set($cacheKey, $asset->data);
521
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
522
        }
523
        $asset->data = $cache->get($cacheKey);
524
525
        return $asset;
526
    }
527
528
    /**
529
     * Converts an image asset to WebP format.
530
     *
531
     * @throws RuntimeException
532
     */
533
    public function webp(?int $quality = null): self
534
    {
535
        return $this->convert('webp', $quality);
536
    }
537
538
    /**
539
     * Converts an image asset to AVIF format.
540
     *
541
     * @throws RuntimeException
542
     */
543
    public function avif(?int $quality = null): self
544 1
    {
545
        return $this->convert('avif', $quality);
546 1
    }
547
548
    /**
549
     * Implements \ArrayAccess.
550
     */
551
    #[\ReturnTypeWillChange]
552 1
    public function offsetSet($offset, $value): void
553
    {
554
        if (!\is_null($offset)) {
555 1
            $this->data[$offset] = $value;
556 1
        }
557
    }
558
559
    /**
560
     * Implements \ArrayAccess.
561
     */
562
    #[\ReturnTypeWillChange]
563 1
    public function offsetExists($offset): bool
564
    {
565
        return isset($this->data[$offset]);
566 1
    }
567
568
    /**
569
     * Implements \ArrayAccess.
570
     */
571
    #[\ReturnTypeWillChange]
572
    public function offsetUnset($offset): void
573
    {
574
        unset($this->data[$offset]);
575
    }
576
577
    /**
578
     * Implements \ArrayAccess.
579
     */
580
    #[\ReturnTypeWillChange]
581 1
    public function offsetGet($offset)
582
    {
583
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
584 1
    }
585
586
    /**
587
     * Hashing content of an asset with the specified algo, sha384 by default.
588
     * Used for SRI (Subresource Integrity).
589
     *
590
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
591
     */
592
    public function getIntegrity(string $algo = 'sha384'): string
593 1
    {
594
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
595 1
    }
596
597
    /**
598
     * Returns MP3 file infos.
599
     *
600
     * @see https://github.com/wapmorgan/Mp3Info
601
     */
602
    public function getAudio(): Mp3Info
603 1
    {
604
        if ($this->data['type'] !== 'audio') {
605 1
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
606
        }
607
608
        return new Mp3Info($this->data['file']);
609 1
    }
610
611
    /**
612
     * Returns MP4 file infos.
613
     *
614
     * @see https://github.com/clwu88/php-read-mp4info
615
     */
616
    public function getVideo(): array
617 1
    {
618
        if ($this->data['type'] !== 'video') {
619 1
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
620
        }
621
622
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
623 1
    }
624
625
    /**
626
     * Returns the Data URL (encoded in Base64).
627
     *
628
     * @throws RuntimeException
629
     */
630
    public function dataurl(): string
631 1
    {
632
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
633 1
            return Image::getDataUrl($this, (int) $this->config->get('assets.images.quality'));
634 1
        }
635
636
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
637 1
    }
638
639
    /**
640
     * Adds asset path to the list of assets to save.
641
     *
642
     * @throws RuntimeException
643
     */
644
    public function save(): void
645 1
    {
646
        if ($this->data['missing']) {
647 1
            return;
648 1
        }
649
650
        $cache = new Cache($this->builder, 'assets');
651 1
        if (!Util\File::getFS()->exists($cache->getContentFilePathname($this->data['path']))) {
652 1
            throw new RuntimeException(
653
                \sprintf('Can\'t add "%s" to assets list. Please clear cache and retry.', $this->data['path'])
654
            );
655
        }
656
657
        $this->builder->addAsset($this->data['path']);
658 1
    }
659
660
    /**
661
     * Is the asset an image and is it in CDN?
662
     */
663
    public function isImageInCdn(): bool
664 1
    {
665
        if (
666
            $this->data['type'] == 'image'
667 1
            && $this->config->isEnabled('assets.images.cdn')
668 1
            && $this->data['ext'] != 'ico'
669 1
            && (Image::isSVG($this) && $this->config->isEnabled('assets.images.cdn.svg'))
670 1
        ) {
671
            return true;
672
        }
673
        // handle remote image?
674
        if ($this->data['url'] !== null && $this->config->isEnabled('assets.images.cdn.remote')) {
675 1
            return true;
676
        }
677
678
        return false;
679 1
    }
680
681
    /**
682
     * Builds a relative path from a URL.
683
     * Used for remote files.
684
     */
685
    public static function buildPathFromUrl(string $url): string
686 1
    {
687
        $host = parse_url($url, PHP_URL_HOST);
688 1
        $path = parse_url($url, PHP_URL_PATH);
689 1
        $query = parse_url($url, PHP_URL_QUERY);
690 1
        $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
691 1
692
        // Google Fonts hack
693
        if (Util\Str::endsWith($path, '/css') || Util\Str::endsWith($path, '/css2')) {
694 1
            $ext = 'css';
695 1
        }
696
697
        return Page::slugify(\sprintf('%s%s%s%s', $host, self::sanitize($path), $query ? "-$query" : '', $query && $ext ? ".$ext" : ''));
698 1
    }
699
700
    /**
701
     * Replaces some characters by '_'.
702
     */
703
    public static function sanitize(string $string): string
704 1
    {
705
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
706 1
    }
707
708
    /**
709
     * Returns local file path and updated path, or throw an exception.
710
     *
711
     * Try to locate the file in:
712
     *   (1. remote file)
713
     *   1. assets
714
     *   2. themes/<theme>/assets
715
     *   3. static
716
     *   4. themes/<theme>/static
717
     *
718
     * If $fallback is set, it will be used if the file is not found.
719
     *
720
     * @throws RuntimeException
721
     */
722
    private function locateFile(string $path, ?string $fallback = null): array
723 1
    {
724
        // remote file
725
        if (Util\File::isRemote($path)) {
726 1
            try {
727
                $content = $this->getRemoteFileContent($path);
728 1
                $path = self::buildPathFromUrl($path);
729 1
                $cache = new Cache($this->builder, 'assets/remote');
730 1
                if (!$cache->has($path)) {
731 1
                    $cache->set($path, [
732 1
                        'content' => $content,
733 1
                        'path'    => $path,
734 1
                    ], \DateInterval::createFromDateString('7 days'));
735 1
                }
736
                return [
737 1
                    'file' => $cache->getContentFilePathname($path),
738 1
                    'path' => $path,
739 1
                ];
740 1
            } catch (RuntimeException $e) {
741 1
                if ($fallback === null) {
742 1
                    throw new RuntimeException($e->getMessage(), previous: $e);
743
                }
744
                $path = $fallback;
745 1
            }
746
        }
747
748
        // checks in assets/
749
        $file = Util::joinFile($this->config->getAssetsPath(), $path);
750 1
        if (Util\File::getFS()->exists($file)) {
751 1
            return [
752 1
                'file' => $file,
753 1
                'path' => $path,
754 1
            ];
755 1
        }
756
757
        // checks in each themes/<theme>/assets/
758
        foreach ($this->config->getTheme() ?? [] as $theme) {
759 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
760 1
            if (Util\File::getFS()->exists($file)) {
761 1
                return [
762 1
                    'file' => $file,
763 1
                    'path' => $path,
764 1
                ];
765 1
            }
766
        }
767
768
        // checks in static/
769
        $file = Util::joinFile($this->config->getStaticTargetPath(), $path);
770 1
        if (Util\File::getFS()->exists($file)) {
771 1
            return [
772 1
                'file' => $file,
773 1
                'path' => $path,
774 1
            ];
775 1
        }
776
777
        // checks in each themes/<theme>/static/
778
        foreach ($this->config->getTheme() ?? [] as $theme) {
779 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
780 1
            if (Util\File::getFS()->exists($file)) {
781 1
                return [
782 1
                    'file' => $file,
783 1
                    'path' => $path,
784 1
                ];
785 1
            }
786
        }
787
788
        throw new RuntimeException(\sprintf('Can\'t locate file "%s".', $path));
789 1
    }
790
791
    /**
792
     * Try to get remote file content.
793
     * Returns file content or throw an exception.
794
     *
795
     * @throws RuntimeException
796
     */
797
    private function getRemoteFileContent(string $path): string
798 1
    {
799
        try {
800
            if (!Util\File::isRemoteExists($path)) {
801 1
                throw new RuntimeException(\sprintf('Remote file "%s" doesn\'t exists', $path));
802 1
            }
803
            if (false === $content = Util\File::fileGetContents($path, true)) {
804 1
                throw new RuntimeException(\sprintf('Can\'t get content of remote file "%s".', $path));
805
            }
806
            if (\strlen($content) <= 1) {
807 1
                throw new RuntimeException(\sprintf('Remote file "%s" is empty.', $path));
808 1
            }
809
        } catch (RuntimeException $e) {
810 1
            throw new RuntimeException($e->getMessage());
811 1
        }
812
813
        return $content;
814 1
    }
815
816
    /**
817
     * Returns the width of an image/SVG.
818
     *
819
     * @throws RuntimeException
820
     */
821
    private function getWidth(): int
822 1
    {
823
        if ($this->data['type'] != 'image') {
824 1
            return 0;
825
        }
826
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
827 1
            return (int) $svg->width;
828 1
        }
829
        if (false === $size = $this->getImageSize()) {
830 1
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
831
        }
832
833
        return $size[0];
834 1
    }
835
836
    /**
837
     * Returns the height of an image/SVG.
838
     *
839
     * @throws RuntimeException
840
     */
841
    private function getHeight(): int
842 1
    {
843
        if ($this->data['type'] != 'image') {
844 1
            return 0;
845
        }
846
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
847 1
            return (int) $svg->height;
848 1
        }
849
        if (false === $size = $this->getImageSize()) {
850 1
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
851
        }
852
853
        return $size[1];
854 1
    }
855
856
    /**
857
     * Returns image size informations.
858
     *
859
     * @see https://www.php.net/manual/function.getimagesize.php
860
     *
861
     * @return array|false
862
     */
863
    private function getImageSize()
864 1
    {
865
        if (!$this->data['type'] == 'image') {
866 1
            return false;
867
        }
868
869
        try {
870
            if (false === $size = getimagesizefromstring($this->data['content'])) {
871 1
                return false;
872 1
            }
873
        } catch (\Exception $e) {
874
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path'], $e->getMessage()));
875
        }
876
877
        return $size;
878 1
    }
879
880
    /**
881
     * Builds CDN image URL.
882
     */
883
    private function buildImageCdnUrl(): string
884
    {
885
        return str_replace(
886
            [
887
                '%account%',
888
                '%image_url%',
889
                '%width%',
890
                '%quality%',
891
                '%format%',
892
            ],
893
            [
894
                $this->config->get('assets.images.cdn.account') ?? '',
895
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
896
                $this->data['width'],
897
                (int) $this->config->get('assets.images.quality'),
898
                $this->data['ext'],
899
            ],
900
            (string) $this->config->get('assets.images.cdn.url')
901
        );
902
    }
903
}
904