Passed
Pull Request — master (#1676)
by Arnaud
09:49 queued 04:14
created

Asset::findFile()   D

Complexity

Conditions 22
Paths 54

Size

Total Lines 87
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 26.579

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 22
eloc 51
c 3
b 1
f 0
nc 54
nop 2
dl 0
loc 87
ccs 41
cts 52
cp 0.7885
crap 26.579
rs 4.1666

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\RuntimeException;
21
use Cecil\Util;
22
use Intervention\Image\ImageManagerStatic as ImageManager;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use wapmorgan\Mp3Info\Mp3Info;
26
27
class Asset implements \ArrayAccess
28
{
29
    /** @var Builder */
30
    protected $builder;
31
32
    /** @var Config */
33
    protected $config;
34
35
    /** @var array */
36
    protected $data = [];
37
38
    /** @var bool */
39
    protected $fingerprinted = false;
40
41
    /** @var bool */
42
    protected $compiled = false;
43
44
    /** @var bool */
45
    protected $minified = false;
46
47
    /** @var bool */
48
    protected $optimize = false;
49
50
    /** @var bool */
51
    protected $ignore_missing = false;
52
53
    /**
54
     * Creates an Asset from a file path, an array of files path or an URL.
55
     *
56
     * @param Builder      $builder
57
     * @param string|array $paths
58
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
59
     *
60
     * @throws RuntimeException
61
     */
62 1
    public function __construct(Builder $builder, $paths, array $options = null)
63
    {
64 1
        $this->builder = $builder;
65 1
        $this->config = $builder->getConfig();
66 1
        $paths = \is_array($paths) ? $paths : [$paths];
67 1
        array_walk($paths, function ($path) {
68 1
            if (!\is_string($path)) {
69
                throw new RuntimeException(sprintf('The path to an asset must be a string (%s given).', \gettype($path)));
70
            }
71 1
            if (empty($path)) {
72
                throw new RuntimeException('The path to an asset can\'t be empty.');
73
            }
74 1
            if (substr($path, 0, 2) == '..') {
75
                throw new RuntimeException(sprintf('The path to asset "%s" is wrong: it must be directly relative to "assets" or "static" directory, or a remote URL.', $path));
76
            }
77 1
        });
78 1
        $this->data = [
79 1
            'file'           => '',    // absolute file path
80 1
            'files'          => [],    // array of files path (if bundle)
81 1
            'filename'       => '',    // filename
82 1
            'path_source'    => '',    // public path to the file, before transformations
83 1
            'path'           => '',    // public path to the file, after transformations
84 1
            'url'            => null,  // URL of a remote image
85 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
86 1
            'ext'            => '',    // file extension
87 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
88 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
89 1
            'size'           => 0,     // file size (in bytes)
90 1
            'content_source' => '',    // file content, before transformations
91 1
            'content'        => '',    // file content, after transformations
92 1
            'width'          => 0,     // width (in pixels) in case of an image
93 1
            'height'         => 0,     // height (in pixels) in case of an image
94 1
            'exif'           => [],    // exif data
95 1
        ];
96
97
        // handles options
98 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
99 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
100 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
101 1
        $filename = '';
102 1
        $ignore_missing = false;
103 1
        $remote_fallback = null;
104 1
        $force_slash = true;
105 1
        extract(\is_array($options) ? $options : [], EXTR_IF_EXISTS);
106 1
        $this->ignore_missing = $ignore_missing;
107
108
        // fill data array with file(s) informations
109 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
110 1
        $cacheKey = sprintf('%s__%s', implode('_', $paths), $this->builder->getVersion());
111 1
        if (!$cache->has($cacheKey)) {
112 1
            $pathsCount = \count($paths);
113 1
            $file = [];
114 1
            for ($i = 0; $i < $pathsCount; $i++) {
115
                // loads file(s)
116 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
117
                // bundle: same type/ext only
118 1
                if ($i > 0) {
119 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
120
                        throw new RuntimeException(sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
121
                    }
122 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
123
                        throw new RuntimeException(sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
124
                    }
125
                }
126
                // missing allowed = empty path
127 1
                if ($file[$i]['missing']) {
128 1
                    $this->data['missing'] = true;
129 1
                    $this->data['path'] = $file[$i]['path'];
130
131 1
                    continue;
132
                }
133
                // set data
134 1
                $this->data['size'] += $file[$i]['size'];
135 1
                $this->data['content_source'] .= $file[$i]['content'];
136 1
                $this->data['content'] .= $file[$i]['content'];
137 1
                if ($i == 0) {
138 1
                    $this->data['file'] = $file[$i]['filepath'];
139 1
                    $this->data['filename'] = $file[$i]['path'];
140 1
                    $this->data['path_source'] = $file[$i]['path'];
141 1
                    $this->data['path'] = $file[$i]['path'];
142 1
                    $this->data['url'] = $file[$i]['url'];
143 1
                    $this->data['ext'] = $file[$i]['ext'];
144 1
                    $this->data['type'] = $file[$i]['type'];
145 1
                    $this->data['subtype'] = $file[$i]['subtype'];
146 1
                    if ($this->data['type'] == 'image') {
147 1
                        $this->data['width'] = $this->getWidth();
148 1
                        $this->data['height'] = $this->getHeight();
149 1
                        if ($this->data['subtype'] == 'jpeg') {
150
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
151
                        }
152
                    }
153
                    // bundle: default filename
154 1
                    if ($pathsCount > 1 && empty($filename)) {
155 1
                        switch ($this->data['ext']) {
156 1
                            case 'scss':
157 1
                            case 'css':
158 1
                                $filename = '/styles.css';
159 1
                                break;
160 1
                            case 'js':
161 1
                                $filename = '/scripts.js';
162 1
                                break;
163
                            default:
164
                                throw new RuntimeException(sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
165
                        }
166
                    }
167
                    // bundle: filename and path
168 1
                    if (!empty($filename)) {
169 1
                        $this->data['filename'] = $filename;
170 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
171
                    }
172
                }
173
                // bundle: files path
174 1
                $this->data['files'][] = $file[$i]['filepath'];
175
            }
176
            // bundle: define path
177 1
            if ($pathsCount > 1 && empty($filename)) {
178
                switch ($this->data['ext']) {
179
                    case 'scss':
180
                    case 'css':
181
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
182
                        break;
183
                    case 'js':
184
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
185
                        break;
186
                    default:
187
                        throw new RuntimeException(sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
188
                }
189
            }
190 1
            $cache->set($cacheKey, $this->data);
191
        }
192 1
        $this->data = $cache->get($cacheKey);
193
194
        // fingerprinting
195 1
        if ($fingerprint) {
196 1
            $this->fingerprint();
197
        }
198
        // compiling (Sass files)
199 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
200 1
            $this->compile();
201
        }
202
        // minifying (CSS and JavScript files)
203 1
        if ($minify) {
204 1
            $this->minify();
205
        }
206
        // optimizing (images files)
207 1
        if ($optimize) {
208 1
            $this->optimize = true;
209
        }
210
    }
211
212
    /**
213
     * Returns path.
214
     *
215
     * @throws RuntimeException
216
     */
217 1
    public function __toString(): string
218
    {
219
        try {
220 1
            $this->save();
221
        } catch (\Exception $e) {
222
            $this->builder->getLogger()->error($e->getMessage());
223
        }
224
225 1
        if ($this->isImageInCdn()) {
226
            return $this->buildImageCdnUrl();
227
        }
228
229 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
230
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
231
        }
232
233 1
        return $this->data['path'];
234
    }
235
236
    /**
237
     * Fingerprints a file.
238
     */
239 1
    public function fingerprint(): self
240
    {
241 1
        if ($this->fingerprinted) {
242 1
            return $this;
243
        }
244
245 1
        $fingerprint = hash('md5', $this->data['content_source']);
246 1
        $this->data['path'] = preg_replace(
247 1
            '/\.' . $this->data['ext'] . '$/m',
248 1
            ".$fingerprint." . $this->data['ext'],
249 1
            $this->data['path']
250 1
        );
251
252 1
        $this->fingerprinted = true;
253
254 1
        return $this;
255
    }
256
257
    /**
258
     * Compiles a SCSS.
259
     *
260
     * @throws RuntimeException
261
     */
262 1
    public function compile(): self
263
    {
264 1
        if ($this->compiled) {
265 1
            return $this;
266
        }
267
268 1
        if ($this->data['ext'] != 'scss') {
269 1
            return $this;
270
        }
271
272 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
273 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
274 1
        if (!$cache->has($cacheKey)) {
275 1
            $scssPhp = new Compiler();
276 1
            $importDir = [];
277 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
278 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
279 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
280 1
            $themes = $this->config->getTheme() ?? [];
281 1
            foreach ($scssDir as $dir) {
282 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
283 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
284 1
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
285 1
                foreach ($themes as $theme) {
286 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
287 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
288
                }
289
            }
290 1
            $scssPhp->setImportPaths(array_unique($importDir));
291
            // source map
292 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
293
                $importDir = [];
294
                $assetDir = (string) $this->config->get('assets.dir');
295
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
296
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
297
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
298
                $importDir[] = \dirname($filePath);
299
                foreach ($scssDir as $dir) {
300
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
301
                }
302
                $scssPhp->setImportPaths(array_unique($importDir));
303
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
304
                $scssPhp->setSourceMapOptions([
305
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
306
                    'sourceRoot'        => '/',
307
                ]);
308
            }
309
            // output style
310 1
            $outputStyles = ['expanded', 'compressed'];
311 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
312 1
            if (!\in_array($outputStyle, $outputStyles)) {
313
                throw new RuntimeException(sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
314
            }
315 1
            $scssPhp->setOutputStyle($outputStyle);
316
            // variables
317 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
318 1
            if (!empty($variables)) {
319 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
320 1
                $scssPhp->replaceVariables($variables);
321
            }
322
            // update data
323 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
324 1
            $this->data['ext'] = 'css';
325 1
            $this->data['type'] = 'text';
326 1
            $this->data['subtype'] = 'text/css';
327 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
328 1
            $this->data['size'] = \strlen($this->data['content']);
329 1
            $this->compiled = true;
330 1
            $cache->set($cacheKey, $this->data);
331
        }
332 1
        $this->data = $cache->get($cacheKey);
333
334 1
        return $this;
335
    }
336
337
    /**
338
     * Minifying a CSS or a JS.
339
     *
340
     * @throws RuntimeException
341
     */
342 1
    public function minify(): self
343
    {
344
        // disable minify to preserve inline source map
345 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
346
            return $this;
347
        }
348
349 1
        if ($this->minified) {
350
            return $this;
351
        }
352
353 1
        if ($this->data['ext'] == 'scss') {
354
            $this->compile();
355
        }
356
357 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
358
            return $this;
359
        }
360
361 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
362
            $this->minified;
363
364
            return $this;
365
        }
366
367 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
368 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
369 1
        if (!$cache->has($cacheKey)) {
370 1
            switch ($this->data['ext']) {
371 1
                case 'css':
372 1
                    $minifier = new Minify\CSS($this->data['content']);
373 1
                    break;
374 1
                case 'js':
375 1
                    $minifier = new Minify\JS($this->data['content']);
376 1
                    break;
377
                default:
378
                    throw new RuntimeException(sprintf('Not able to minify "%s"', $this->data['path']));
379
            }
380 1
            $this->data['path'] = preg_replace(
381 1
                '/\.' . $this->data['ext'] . '$/m',
382 1
                '.min.' . $this->data['ext'],
383 1
                $this->data['path']
384 1
            );
385 1
            $this->data['content'] = $minifier->minify();
386 1
            $this->data['size'] = \strlen($this->data['content']);
387 1
            $this->minified = true;
388 1
            $cache->set($cacheKey, $this->data);
389
        }
390 1
        $this->data = $cache->get($cacheKey);
391
392 1
        return $this;
393
    }
394
395
    /**
396
     * Optimizing an image.
397
     */
398 1
    public function optimize(string $filepath): self
399
    {
400 1
        if ($this->data['type'] != 'image') {
401 1
            return $this;
402
        }
403
404 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
405 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
406 1
        $tags = ["q$quality", 'optimized'];
407 1
        if ($this->data['width']) {
408 1
            array_unshift($tags, "{$this->data['width']}x");
409
        }
410 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
411 1
        if (!$cache->has($cacheKey)) {
412 1
            $message = $this->data['path'];
413 1
            $sizeBefore = filesize($filepath);
414 1
            Optimizer::create($quality)->optimize($filepath);
415 1
            $sizeAfter = filesize($filepath);
416 1
            if ($sizeAfter < $sizeBefore) {
417
                $message = sprintf(
418
                    '%s (%s Ko -> %s Ko)',
419
                    $message,
420
                    ceil($sizeBefore / 1000),
421
                    ceil($sizeAfter / 1000)
422
                );
423
            }
424 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
425 1
            $this->data['size'] = $sizeAfter;
426 1
            $cache->set($cacheKey, $this->data);
427 1
            $this->builder->getLogger()->debug(sprintf('Asset "%s" optimized', $message));
428
        }
429 1
        $this->data = $cache->get($cacheKey, $this->data);
430
431 1
        return $this;
432
    }
433
434
    /**
435
     * Resizes an image with a new $width.
436
     *
437
     * @throws RuntimeException
438
     */
439 1
    public function resize(int $width): self
440
    {
441 1
        if ($this->data['missing']) {
442
            throw new RuntimeException(sprintf('Not able to resize "%s": file not found', $this->data['path']));
443
        }
444 1
        if ($this->data['type'] != 'image') {
445
            throw new RuntimeException(sprintf('Not able to resize "%s": not an image', $this->data['path']));
446
        }
447 1
        if ($width >= $this->data['width']) {
448 1
            return $this;
449
        }
450
451 1
        $assetResized = clone $this;
452 1
        $assetResized->data['width'] = $width;
453
454 1
        if ($this->isImageInCdn()) {
455
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
456
        }
457
458 1
        $quality = $this->config->get('assets.images.quality');
459 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
460 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
461 1
        if (!$cache->has($cacheKey)) {
462 1
            if ($assetResized->data['type'] !== 'image') {
463
                throw new RuntimeException(sprintf('Not able to resize "%s"', $assetResized->data['path']));
464
            }
465 1
            if (!\extension_loaded('gd')) {
466
                throw new RuntimeException('GD extension is required to use images resize.');
467
            }
468
469
            try {
470 1
                $img = ImageManager::make($assetResized->data['content_source'])->encode($assetResized->data['ext']);
471 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
472 1
                    $constraint->aspectRatio();
473 1
                    $constraint->upsize();
474 1
                });
475
            } catch (\Exception $e) {
476
                throw new RuntimeException(sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
477
            }
478 1
            $assetResized->data['path'] = '/' . Util::joinPath(
479 1
                (string) $this->config->get('assets.target'),
480 1
                (string) $this->config->get('assets.images.resize.dir'),
481 1
                (string) $width,
482 1
                $assetResized->data['path']
483 1
            );
484
485
            try {
486 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
487
                    $img->interlace();
488
                }
489 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
490 1
                $img->destroy();
491 1
                $assetResized->data['height'] = $assetResized->getHeight();
492 1
                $assetResized->data['size'] = \strlen($assetResized->data['content']);
493
            } catch (\Exception $e) {
494
                throw new RuntimeException(sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
495
            }
496
497 1
            $cache->set($cacheKey, $assetResized->data);
498
        }
499 1
        $assetResized->data = $cache->get($cacheKey);
500
501 1
        return $assetResized;
502
    }
503
504
    /**
505
     * Converts an image asset to WebP format.
506
     *
507
     * @throws RuntimeException
508
     */
509 1
    public function webp(?int $quality = null): self
510
    {
511 1
        if ($this->data['type'] !== 'image') {
512
            throw new RuntimeException(sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
513
        }
514
515 1
        if ($quality === null) {
516 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
517
        }
518
519 1
        $assetWebp = clone $this;
520 1
        $format = 'webp';
521 1
        $assetWebp['ext'] = $format;
522
523 1
        if ($this->isImageInCdn()) {
524
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
525
        }
526
527 1
        $img = ImageManager::make($assetWebp['content']);
528 1
        $assetWebp['content'] = (string) $img->encode($format, $quality);
529
        $img->destroy();
530
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
531
        $assetWebp['subtype'] = "image/$format";
532
        $assetWebp['size'] = \strlen($assetWebp['content']);
0 ignored issues
show
Bug introduced by
It seems like $assetWebp['content'] can also be of type null; however, parameter $string of strlen() 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 ignore-type  annotation

532
        $assetWebp['size'] = \strlen(/** @scrutinizer ignore-type */ $assetWebp['content']);
Loading history...
533
534
        return $assetWebp;
535
    }
536
537
    /**
538
     * Implements \ArrayAccess.
539
     */
540 1
    #[\ReturnTypeWillChange]
541
    public function offsetSet($offset, $value): void
542
    {
543 1
        if (!\is_null($offset)) {
544 1
            $this->data[$offset] = $value;
545
        }
546
    }
547
548
    /**
549
     * Implements \ArrayAccess.
550
     */
551 1
    #[\ReturnTypeWillChange]
552
    public function offsetExists($offset): bool
553
    {
554 1
        return isset($this->data[$offset]);
555
    }
556
557
    /**
558
     * Implements \ArrayAccess.
559
     */
560
    #[\ReturnTypeWillChange]
561
    public function offsetUnset($offset): void
562
    {
563
        unset($this->data[$offset]);
564
    }
565
566
    /**
567
     * Implements \ArrayAccess.
568
     */
569 1
    #[\ReturnTypeWillChange]
570
    public function offsetGet($offset)
571
    {
572 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
573
    }
574
575
    /**
576
     * Hashing content of an asset with the specified algo, sha384 by default.
577
     * Used for SRI (Subresource Integrity).
578
     *
579
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
580
     */
581 1
    public function getIntegrity(string $algo = 'sha384'): string
582
    {
583 1
        return sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
584
    }
585
586
    /**
587
     * Returns MP3 file infos.
588
     *
589
     * @see https://github.com/wapmorgan/Mp3Info
590
     */
591 1
    public function getAudio(): Mp3Info
592
    {
593 1
        if ($this->data['type'] !== 'audio') {
594
            throw new RuntimeException(sprintf('Not able to get audio infos of "%s"', $this->data['path']));
595
        }
596
597 1
        return new Mp3Info($this->data['file']);
598
    }
599
600
    /**
601
     * Returns MP4 file infos.
602
     *
603
     * @see https://github.com/clwu88/php-read-mp4info
604
     */
605
    public function getVideo(): array
606
    {
607
        if ($this->data['type'] !== 'video') {
608
            throw new RuntimeException(sprintf('Not able to get video infos of "%s"', $this->data['path']));
609
        }
610
611
        return \Clwu\Mp4::getInfo($this->data['file']);
612
    }
613
614
    /**
615
     * Returns the data URL (encoded in Base64).
616
     *
617
     * @throws RuntimeException
618
     */
619 1
    public function dataurl(): string
620
    {
621 1
        if ($this->data['type'] == 'image' && !$this->isSVG()) {
622 1
            return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
623
        }
624
625 1
        return sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
626
    }
627
628
    /**
629
     * Saves file.
630
     * Note: a file from `static/` with the same name will NOT be overridden.
631
     *
632
     * @throws RuntimeException
633
     */
634 1
    public function save(): void
635
    {
636 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
637 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
638
            try {
639 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
640 1
                $this->builder->getLogger()->debug(sprintf('Asset "%s" saved', $filepath));
641 1
                if ($this->optimize) {
642 1
                    $this->optimize($filepath);
643
                }
644
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
645
                if (!$this->ignore_missing) {
646
                    throw new RuntimeException(sprintf('Can\'t save asset "%s"', $filepath));
647
                }
648
            }
649
        }
650
    }
651
652
    /**
653
     * Is Asset is an image in CDN.
654
     *
655
     * @return bool
656
     */
657 1
    public function isImageInCdn()
658
    {
659 1
        if ($this->data['type'] != 'image' || (bool) $this->config->get('assets.images.cdn.enabled') !== true || ($this->isSVG() && (bool) $this->config->get('assets.images.cdn.svg') !== true)) {
660 1
            return false;
661
        }
662
        // remote image?
663
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
664
            return false;
665
        }
666
667
        return true;
668
    }
669
670
    /**
671
     * Load file data.
672
     *
673
     * @throws RuntimeException
674
     */
675 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
676
    {
677 1
        $file = [
678 1
            'url' => null,
679 1
        ];
680
681
        try {
682 1
            $filePath = $this->findFile($path, $remote_fallback);
683 1
        } catch (\Exception $e) {
684 1
            if ($ignore_missing) {
685 1
                $file['path'] = $path;
686 1
                $file['missing'] = true;
687
688 1
                return $file;
689
            }
690
691
            throw new RuntimeException(sprintf('Can\'t load asset file "%s" (%s)', $path, $e->getMessage()));
692
        }
693
694 1
        if (Util\Url::isUrl($path)) {
695 1
            $file['url'] = $path;
696 1
            $path = Util::joinPath(
697 1
                (string) $this->config->get('assets.target'),
698 1
                Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath())
699 1
            );
700
            // remote_fallback in assets/ ont in cache/assets/remote/
701 1
            if (substr(Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath()), 0, 2) == '..') {
702
                $path = Util::joinPath(
703
                    (string) $this->config->get('assets.target'),
704
                    Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsPath())
705
                );
706
            }
707 1
            $force_slash = true;
708
        }
709 1
        if ($force_slash) {
710 1
            $path = '/' . ltrim($path, '/');
711
        }
712
713 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
714 1
        $content = Util\File::fileGetContents($filePath);
715
716 1
        $file['filepath'] = $filePath;
717 1
        $file['path'] = $path;
718 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
719 1
        $file['type'] = $type;
720 1
        $file['subtype'] = $subtype;
721 1
        $file['size'] = filesize($filePath);
722 1
        $file['content'] = $content;
723 1
        $file['missing'] = false;
724
725 1
        return $file;
726
    }
727
728
    /**
729
     * Try to find the file:
730
     *   1. remote (if $path is a valid URL)
731
     *   2. in static/
732
     *   3. in themes/<theme>/static/
733
     * Returns local file path or throw an exception.
734
     *
735
     * @throws RuntimeException
736
     */
737 1
    private function findFile(string $path, ?string $remote_fallback = null): string
738
    {
739
        // in case of remote file: save it and returns cached file path
740 1
        if (Util\Url::isUrl($path)) {
741 1
            $url = $path;
742 1
            $urlHost = parse_url($path, PHP_URL_HOST);
743 1
            $urlPath = parse_url($path, PHP_URL_PATH);
744 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
745 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
746
            // Google Fonts hack
747 1
            if (Util\Str::endsWith($urlPath, '/css') || Util\Str::endsWith($urlPath, '/css2')) {
748 1
                $extension = 'css';
749
            }
750 1
            $relativePath = Page::slugify(sprintf(
751 1
                '%s%s%s%s',
752 1
                $urlHost,
753 1
                $this->sanitize($urlPath),
754 1
                $urlQuery ? "-$urlQuery" : '',
755 1
                $urlQuery && $extension ? ".$extension" : ''
756 1
            ));
757 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
758
            // not already in cache
759 1
            if (!file_exists($filePath)) {
760
                try {
761 1
                    if (!Util\Url::isRemoteFileExists($url)) {
762
                        throw new RuntimeException(sprintf('File "%s" doesn\'t exists', $url));
763
                    }
764 1
                    if (false === $content = Util\File::fileGetContents($url, true)) {
765
                        throw new RuntimeException(sprintf('Can\'t get content of file "%s"', $url));
766
                    }
767 1
                    if (\strlen($content) <= 1) {
768 1
                        throw new RuntimeException(sprintf('File "%s" is empty', $url));
769
                    }
770
                } catch (RuntimeException $e) {
771
                    // is there a fallback in assets/
772
                    if ($remote_fallback) {
773
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
774
                        if (Util\File::getFS()->exists($filePath)) {
775
                            return $filePath;
776
                        }
777
                        throw new RuntimeException(sprintf('Fallback file "%s" doesn\'t exists', $filePath));
778
                    }
779
780
                    throw new RuntimeException($e->getMessage());
781
                }
782 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
783
                    throw new RuntimeException(sprintf('Can\'t get content of "%s"', $url));
784
                }
785 1
                if (\strlen($content) <= 1) {
786
                    throw new RuntimeException(sprintf('Asset at "%s" is empty', $url));
787
                }
788
                // put file in cache
789 1
                Util\File::getFS()->dumpFile($filePath, $content);
790
            }
791
792 1
            return $filePath;
793
        }
794
795
        // checks in assets/
796 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
797 1
        if (Util\File::getFS()->exists($filePath)) {
798 1
            return $filePath;
799
        }
800
801
        // checks in each themes/<theme>/assets/
802 1
        foreach ($this->config->getTheme() as $theme) {
803 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
804 1
            if (Util\File::getFS()->exists($filePath)) {
805 1
                return $filePath;
806
            }
807
        }
808
809
        // checks in static/
810 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
811 1
        if (Util\File::getFS()->exists($filePath)) {
812 1
            return $filePath;
813
        }
814
815
        // checks in each themes/<theme>/static/
816 1
        foreach ($this->config->getTheme() as $theme) {
817 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
818 1
            if (Util\File::getFS()->exists($filePath)) {
819 1
                return $filePath;
820
            }
821
        }
822
823 1
        throw new RuntimeException(sprintf('Can\'t find file "%s"', $path));
824
    }
825
826
    /**
827
     * Returns the width of an image/SVG.
828
     *
829
     * @throws RuntimeException
830
     */
831 1
    private function getWidth(): int
832
    {
833 1
        if ($this->data['type'] != 'image') {
834
            return 0;
835
        }
836 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
837 1
            return (int) $svg->width;
838
        }
839 1
        if (false === $size = $this->getImageSize()) {
840
            throw new RuntimeException(sprintf('Not able to get width of "%s"', $this->data['path']));
841
        }
842
843 1
        return $size[0];
844
    }
845
846
    /**
847
     * Returns the height of an image/SVG.
848
     *
849
     * @throws RuntimeException
850
     */
851 1
    private function getHeight(): int
852
    {
853 1
        if ($this->data['type'] != 'image') {
854
            return 0;
855
        }
856 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
857 1
            return (int) $svg->height;
858
        }
859 1
        if (false === $size = $this->getImageSize()) {
860
            throw new RuntimeException(sprintf('Not able to get height of "%s"', $this->data['path']));
861
        }
862
863 1
        return $size[1];
864
    }
865
866
    /**
867
     * Returns image size informations.
868
     *
869
     * @see https://www.php.net/manual/function.getimagesize.php
870
     *
871
     * @return array|false
872
     */
873 1
    private function getImageSize()
874
    {
875 1
        if (!$this->data['type'] == 'image') {
876
            return false;
877
        }
878
879
        try {
880 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
881 1
                return false;
882
            }
883
        } catch (\Exception $e) {
884
            throw new RuntimeException(sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
885
        }
886
887 1
        return $size;
888
    }
889
890
    /**
891
     * Returns true if asset is a SVG.
892
     */
893 1
    private function isSVG(): bool
894
    {
895 1
        return \in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
896
    }
897
898
    /**
899
     * Returns SVG attributes.
900
     *
901
     * @return \SimpleXMLElement|false
902
     */
903 1
    private function getSvgAttributes()
904
    {
905 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
906
            return false;
907
        }
908
909 1
        return $xml->attributes();
910
    }
911
912
    /**
913
     * Replaces some characters by '_'.
914
     */
915 1
    private function sanitize(string $string): string
916
    {
917 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
918
    }
919
920
    /**
921
     * Builds CDN image URL.
922
     */
923
    private function buildImageCdnUrl(): string
924
    {
925
        return str_replace(
926
            [
927
                '%account%',
928
                '%image_url%',
929
                '%width%',
930
                '%quality%',
931
                '%format%',
932
            ],
933
            [
934
                $this->config->get('assets.images.cdn.account'),
935
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
936
                $this->data['width'],
937
                $this->config->get('assets.images.quality') ?? 75,
938
                $this->data['ext'],
939
            ],
940
            (string) $this->config->get('assets.images.cdn.url')
941
        );
942
    }
943
}
944