Passed
Pull Request — master (#2148)
by Arnaud
11:26 queued 05:00
created

Asset::locateFile()   B

Complexity

Conditions 11
Paths 89

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 11.0013

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 11
eloc 40
c 6
b 0
f 0
nc 89
nop 2
dl 0
loc 67
ccs 44
cts 45
cp 0.9778
crap 11.0013
rs 7.3166

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