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