Passed
Pull Request — master (#1676)
by Arnaud
10:22 queued 03:30
created

Asset::findFile()   D

Complexity

Conditions 19
Paths 50

Size

Total Lines 81
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 45
CRAP Score 19.0881

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 19
eloc 47
c 3
b 1
f 0
nc 50
nop 2
dl 0
loc 81
ccs 45
cts 48
cp 0.9375
crap 19.0881
rs 4.5166

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', $filename ?: 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 1
            $cache->set($cacheKey, $this->data);
177
        }
178 1
        $this->data = $cache->get($cacheKey);
179
180
        // fingerprinting
181 1
        if ($fingerprint) {
182 1
            $this->fingerprint();
183
        }
184
        // compiling (Sass files)
185 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
186 1
            $this->compile();
187
        }
188
        // minifying (CSS and JavScript files)
189 1
        if ($minify) {
190 1
            $this->minify();
191
        }
192
        // optimizing (images files)
193 1
        if ($optimize) {
194 1
            $this->optimize = true;
195
        }
196
    }
197
198
    /**
199
     * Returns path.
200
     *
201
     * @throws RuntimeException
202
     */
203 1
    public function __toString(): string
204
    {
205
        try {
206 1
            $this->save();
207
        } catch (\Exception $e) {
208
            $this->builder->getLogger()->error($e->getMessage());
209
        }
210
211 1
        if ($this->isImageInCdn()) {
212
            return $this->buildImageCdnUrl();
213
        }
214
215 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
216
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
217
        }
218
219 1
        return $this->data['path'];
220
    }
221
222
    /**
223
     * Fingerprints a file.
224
     */
225 1
    public function fingerprint(): self
226
    {
227 1
        if ($this->fingerprinted) {
228 1
            return $this;
229
        }
230
231 1
        $fingerprint = hash('md5', $this->data['content_source']);
232 1
        $this->data['path'] = preg_replace(
233 1
            '/\.' . $this->data['ext'] . '$/m',
234 1
            ".$fingerprint." . $this->data['ext'],
235 1
            $this->data['path']
236 1
        );
237
238 1
        $this->fingerprinted = true;
239
240 1
        return $this;
241
    }
242
243
    /**
244
     * Compiles a SCSS.
245
     *
246
     * @throws RuntimeException
247
     */
248 1
    public function compile(): self
249
    {
250 1
        if ($this->compiled) {
251 1
            return $this;
252
        }
253
254 1
        if ($this->data['ext'] != 'scss') {
255 1
            return $this;
256
        }
257
258 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
259 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
260 1
        if (!$cache->has($cacheKey)) {
261 1
            $scssPhp = new Compiler();
262 1
            $importDir = [];
263 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
264 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
265 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
266 1
            $themes = $this->config->getTheme() ?? [];
267 1
            foreach ($scssDir as $dir) {
268 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
269 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
270 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
271 1
                foreach ($themes as $theme) {
272 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
273 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
274
                }
275
            }
276 1
            $scssPhp->setImportPaths(array_unique($importDir));
277
            // source map
278 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
279
                $importDir = [];
280
                $assetDir = (string) $this->config->get('assets.dir');
281
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
282
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
283
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
284
                $importDir[] = dirname($filePath);
285
                foreach ($scssDir as $dir) {
286
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
287
                }
288
                $scssPhp->setImportPaths(array_unique($importDir));
289
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
290
                $scssPhp->setSourceMapOptions([
291
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
292
                    'sourceRoot'        => '/',
293
                ]);
294
            }
295
            // output style
296 1
            $outputStyles = ['expanded', 'compressed'];
297 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
298 1
            if (!in_array($outputStyle, $outputStyles)) {
299
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
300
            }
301 1
            $scssPhp->setOutputStyle($outputStyle);
302
            // variables
303 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
304 1
            if (!empty($variables)) {
305 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
306 1
                $scssPhp->replaceVariables($variables);
307
            }
308
            // update data
309 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
310 1
            $this->data['ext'] = 'css';
311 1
            $this->data['type'] = 'text';
312 1
            $this->data['subtype'] = 'text/css';
313 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
314 1
            $this->data['size'] = strlen($this->data['content']);
315 1
            $this->compiled = true;
316 1
            $cache->set($cacheKey, $this->data);
317
        }
318 1
        $this->data = $cache->get($cacheKey);
319
320 1
        return $this;
321
    }
322
323
    /**
324
     * Minifying a CSS or a JS.
325
     *
326
     * @throws RuntimeException
327
     */
328 1
    public function minify(): self
329
    {
330
        // disable minify to preserve inline source map
331 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
332
            return $this;
333
        }
334
335 1
        if ($this->minified) {
336
            return $this;
337
        }
338
339 1
        if ($this->data['ext'] == 'scss') {
340
            $this->compile();
341
        }
342
343 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
344
            return $this;
345
        }
346
347 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
348
            $this->minified;
349
350
            return $this;
351
        }
352
353 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
354 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
355 1
        if (!$cache->has($cacheKey)) {
356 1
            switch ($this->data['ext']) {
357 1
                case 'css':
358 1
                    $minifier = new Minify\CSS($this->data['content']);
359 1
                    break;
360 1
                case 'js':
361 1
                    $minifier = new Minify\JS($this->data['content']);
362 1
                    break;
363
                default:
364
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
365
            }
366 1
            $this->data['path'] = preg_replace(
367 1
                '/\.' . $this->data['ext'] . '$/m',
368 1
                '.min.' . $this->data['ext'],
369 1
                $this->data['path']
370 1
            );
371 1
            $this->data['content'] = $minifier->minify();
372 1
            $this->data['size'] = strlen($this->data['content']);
373 1
            $this->minified = true;
374 1
            $cache->set($cacheKey, $this->data);
375
        }
376 1
        $this->data = $cache->get($cacheKey);
377
378 1
        return $this;
379
    }
380
381
    /**
382
     * Optimizing an image.
383
     */
384 1
    public function optimize(string $filepath): self
385
    {
386 1
        if ($this->data['type'] != 'image') {
387 1
            return $this;
388
        }
389
390 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
391 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
392 1
        $tags = ["q$quality", 'optimized'];
393 1
        if ($this->data['width']) {
394 1
            array_unshift($tags, "{$this->data['width']}x");
395
        }
396 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
397 1
        if (!$cache->has($cacheKey)) {
398
            try {
399 1
                $message = $this->data['path'];
400 1
                $sizeBefore = filesize($filepath);
401 1
                Optimizer::create($quality)->optimize($filepath);
402 1
                $sizeAfter = filesize($filepath);
403 1
                if ($sizeAfter < $sizeBefore) {
404
                    $message = \sprintf(
405
                        '%s (%s Ko -> %s Ko)',
406
                        $message,
407
                        ceil($sizeBefore / 1000),
408
                        ceil($sizeAfter / 1000)
409
                    );
410
                }
411 1
                $this->data['content'] = Util\File::fileGetContents($filepath);
412 1
                $this->data['size'] = $sizeAfter;
413 1
                $cache->set($cacheKey, $this->data);
414 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
415
            } catch (\Exception $e) {
416
                $this->builder->getLogger()->error(\sprintf('Can\'t optimize image "%s": "%s"', $filepath, $e->getMessage()));
417
            }
418
        }
419 1
        $this->data = $cache->get($cacheKey, $this->data);
420
421 1
        return $this;
422
    }
423
424
    /**
425
     * Resizes an image with a new $width.
426
     *
427
     * @throws RuntimeException
428
     */
429 1
    public function resize(int $width): self
430
    {
431 1
        if ($this->data['missing']) {
432
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
433
        }
434 1
        if ($this->data['type'] != 'image') {
435
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
436
        }
437 1
        if ($width >= $this->data['width']) {
438 1
            return $this;
439
        }
440
441 1
        $assetResized = clone $this;
442 1
        $assetResized->data['width'] = $width;
443
444 1
        if ($this->isImageInCdn()) {
445
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
446
        }
447
448 1
        $quality = $this->config->get('assets.images.quality');
449 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
450 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
451 1
        if (!$cache->has($cacheKey)) {
452 1
            if ($assetResized->data['type'] !== 'image') {
453
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
454
            }
455 1
            if (!extension_loaded('gd')) {
456
                throw new RuntimeException('GD extension is required to use images resize.');
457
            }
458
459
            try {
460 1
                $img = ImageManager::make($assetResized->data['content_source'])->encode($assetResized->data['ext']);
461 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
462 1
                    $constraint->aspectRatio();
463 1
                    $constraint->upsize();
464 1
                });
465
            } catch (\Exception $e) {
466
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
467
            }
468 1
            $assetResized->data['path'] = '/' . Util::joinPath(
469 1
                (string) $this->config->get('assets.target'),
470 1
                (string) $this->config->get('assets.images.resize.dir'),
471 1
                (string) $width,
472 1
                $assetResized->data['path']
473 1
            );
474
475
            try {
476 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
477
                    $img->interlace();
478
                }
479 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
480 1
                $img->destroy();
481 1
                $assetResized->data['height'] = $assetResized->getHeight();
482 1
                $assetResized->data['size'] = strlen($assetResized->data['content']);
483
            } catch (\Exception $e) {
484
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
485
            }
486
487 1
            $cache->set($cacheKey, $assetResized->data);
488
        }
489 1
        $assetResized->data = $cache->get($cacheKey);
490
491 1
        return $assetResized;
492
    }
493
494
    /**
495
     * Converts an image asset to WebP format.
496
     *
497
     * @throws RuntimeException
498
     */
499 1
    public function webp(?int $quality = null): self
500
    {
501 1
        if ($this->data['type'] !== 'image') {
502
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
503
        }
504
505 1
        if ($quality === null) {
506 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
507
        }
508
509 1
        $assetWebp = clone $this;
510 1
        $format = 'webp';
511 1
        $assetWebp['ext'] = $format;
512
513 1
        if ($this->isImageInCdn()) {
514
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
515
        }
516
517 1
        $img = ImageManager::make($assetWebp['content']);
518 1
        $assetWebp['content'] = (string) $img->encode($format, $quality);
519
        $img->destroy();
520
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
521
        $assetWebp['subtype'] = "image/$format";
522
        $assetWebp['size'] = strlen((string) $assetWebp['content']);
523
524
        return $assetWebp;
525
    }
526
527
    /**
528
     * Implements \ArrayAccess.
529
     */
530
    #[\ReturnTypeWillChange]
531 1
    public function offsetSet($offset, $value): void
532
    {
533 1
        if (!is_null($offset)) {
534 1
            $this->data[$offset] = $value;
535
        }
536
    }
537
538
    /**
539
     * Implements \ArrayAccess.
540
     */
541
    #[\ReturnTypeWillChange]
542 1
    public function offsetExists($offset): bool
543
    {
544 1
        return isset($this->data[$offset]);
545
    }
546
547
    /**
548
     * Implements \ArrayAccess.
549
     */
550
    #[\ReturnTypeWillChange]
551
    public function offsetUnset($offset): void
552
    {
553
        unset($this->data[$offset]);
554
    }
555
556
    /**
557
     * Implements \ArrayAccess.
558
     */
559
    #[\ReturnTypeWillChange]
560 1
    public function offsetGet($offset)
561
    {
562 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
563
    }
564
565
    /**
566
     * Hashing content of an asset with the specified algo, sha384 by default.
567
     * Used for SRI (Subresource Integrity).
568
     *
569
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
570
     */
571 1
    public function getIntegrity(string $algo = 'sha384'): string
572
    {
573 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
574
    }
575
576
    /**
577
     * Returns MP3 file infos.
578
     *
579
     * @see https://github.com/wapmorgan/Mp3Info
580
     */
581 1
    public function getAudio(): Mp3Info
582
    {
583 1
        if ($this->data['type'] !== 'audio') {
584
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
585
        }
586
587 1
        return new Mp3Info($this->data['file']);
588
    }
589
590
    /**
591
     * Returns the data URL (encoded in Base64).
592
     *
593
     * @throws RuntimeException
594
     */
595 1
    public function dataurl(): string
596
    {
597 1
        if ($this->data['type'] == 'image' && !$this->isSVG()) {
598 1
            return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
599
        }
600
601 1
        return sprintf("data:%s;base64,%s", $this->data['subtype'], base64_encode($this->data['content']));
602
    }
603
604
    /**
605
     * Saves file.
606
     * Note: a file from `static/` with the same name will NOT be overridden.
607
     *
608
     * @throws RuntimeException
609
     */
610 1
    public function save(): void
611
    {
612 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
613 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
614
            try {
615 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
616 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $filepath));
617 1
                if ($this->optimize) {
618 1
                    $this->optimize($filepath);
619
                }
620
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
621
                if (!$this->ignore_missing) {
622
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
623
                }
624
            }
625
        }
626
    }
627
628
    /**
629
     * Is Asset is an image in CDN.
630
     *
631
     * @return boolean
632
     */
633 1
    public function isImageInCdn()
634
    {
635 1
        if ($this->data['type'] != 'image' || $this->isSVG() || (bool) $this->config->get('assets.images.cdn.enabled') !== true) {
636 1
            return false;
637
        }
638
        // remote image?
639
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
640
            return false;
641
        }
642
643
        return true;
644
    }
645
646
    /**
647
     * Load file data.
648
     *
649
     * @throws RuntimeException
650
     */
651 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
652
    {
653 1
        $file = [
654 1
            'url' => null,
655 1
        ];
656
657
        try {
658 1
            $filePath = $this->findFile($path, $remote_fallback);
659 1
        } catch (\Exception $e) {
660 1
            if ($ignore_missing) {
661 1
                $file['path'] = $path;
662 1
                $file['missing'] = true;
663
664 1
                return $file;
665
            }
666
667
            throw new RuntimeException(\sprintf('Can\'t load Asset file "%s" (%s)', $path, $e->getMessage()));
668
        }
669
670 1
        if (Util\Url::isUrl($path)) {
671 1
            $file['url'] = $path;
672 1
            $path = Util::joinPath(
673 1
                (string) $this->config->get('assets.target'),
674 1
                Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath())
675 1
            );
676 1
            if ($remote_fallback) {
677 1
                $path = Util::joinPath(
678 1
                    (string) $this->config->get('assets.target'),
679 1
                    Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsPath())
680 1
                );
681
            }
682 1
            $force_slash = true;
683
        }
684 1
        if ($force_slash) {
685 1
            $path = '/' . ltrim($path, '/');
686
        }
687
688 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
689 1
        $content = Util\File::fileGetContents($filePath);
690
691 1
        $file['filepath'] = $filePath;
692 1
        $file['path'] = $path;
693 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
694 1
        $file['type'] = $type;
695 1
        $file['subtype'] = $subtype;
696 1
        $file['size'] = filesize($filePath);
697 1
        $file['content'] = $content;
698 1
        $file['missing'] = false;
699
700 1
        return $file;
701
    }
702
703
    /**
704
     * Try to find the file:
705
     *   1. remote (if $path is a valid URL)
706
     *   2. in static/
707
     *   3. in themes/<theme>/static/
708
     * Returns local file path or throw an exception.
709
     *
710
     * @throws RuntimeException
711
     */
712 1
    private function findFile(string $path, ?string $remote_fallback = null): string
713
    {
714
        // in case of remote file: save it and returns cached file path
715 1
        if (Util\Url::isUrl($path)) {
716 1
            $url = $path;
717 1
            $urlHost = parse_url($path, PHP_URL_HOST);
718 1
            $urlPath = parse_url($path, PHP_URL_PATH);
719 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
720 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
721
            // Google Fonts hack
722 1
            if (strpos($urlPath, '/css') !== false) {
723 1
                $extension = 'css';
724
            }
725 1
            $relativePath = Page::slugify(\sprintf(
726 1
                '%s%s%s%s',
727 1
                $urlHost,
728 1
                $this->sanitize($urlPath),
729 1
                $urlQuery ? "-$urlQuery" : '',
730 1
                $urlQuery && $extension ? ".$extension" : ''
731 1
            ));
732 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
733
            // not already in cache
734 1
            if (!file_exists($filePath)) {
735
                try {
736 1
                    if (!Util\Url::isRemoteFileExists($url)) {
737 1
                        throw new RuntimeException(\sprintf('File "%s" doesn\'t exists', $url));
738
                    }
739 1
                    if (false === $content = Util\File::fileGetContents($url, true)) {
740
                        throw new RuntimeException(\sprintf('Can\'t get content of file "%s"', $url));
741
                    }
742 1
                    if (strlen($content) <= 1) {
743 1
                        throw new RuntimeException(\sprintf('File "%s" is empty', $url));
744
                    }
745 1
                } catch (RuntimeException $e) {
746
                    // is there a fallback in assets/
747 1
                    if ($remote_fallback) {
748 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
749 1
                        if (Util\File::getFS()->exists($filePath)) {
750 1
                            return $filePath;
751
                        }
752
                        throw new RuntimeException(\sprintf('Fallback file "%s" doesn\'t exists', $filePath));
753
                    }
754
755
                    throw new RuntimeException($e->getMessage());
756
                }
757
                // put file in cache
758 1
                Util\File::getFS()->dumpFile($filePath, $content);
759
            }
760
761 1
            return $filePath;
762
        }
763
764
        // checks in assets/
765 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
766 1
        if (Util\File::getFS()->exists($filePath)) {
767 1
            return $filePath;
768
        }
769
770
        // checks in each themes/<theme>/assets/
771 1
        foreach ($this->config->getTheme() as $theme) {
772 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
773 1
            if (Util\File::getFS()->exists($filePath)) {
774 1
                return $filePath;
775
            }
776
        }
777
778
        // checks in static/
779 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
780 1
        if (Util\File::getFS()->exists($filePath)) {
781 1
            return $filePath;
782
        }
783
784
        // checks in each themes/<theme>/static/
785 1
        foreach ($this->config->getTheme() as $theme) {
786 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
787 1
            if (Util\File::getFS()->exists($filePath)) {
788 1
                return $filePath;
789
            }
790
        }
791
792 1
        throw new RuntimeException(\sprintf('Can\'t find file "%s"', $path));
793
    }
794
795
    /**
796
     * Returns the width of an image/SVG.
797
     *
798
     * @throws RuntimeException
799
     */
800 1
    private function getWidth(): int
801
    {
802 1
        if ($this->data['type'] != 'image') {
803
            return 0;
804
        }
805 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
806 1
            return (int) $svg->width;
807
        }
808 1
        if (false === $size = $this->getImageSize()) {
809
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
810
        }
811
812 1
        return $size[0];
813
    }
814
815
    /**
816
     * Returns the height of an image/SVG.
817
     *
818
     * @throws RuntimeException
819
     */
820 1
    private function getHeight(): int
821
    {
822 1
        if ($this->data['type'] != 'image') {
823
            return 0;
824
        }
825 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
826 1
            return (int) $svg->height;
827
        }
828 1
        if (false === $size = $this->getImageSize()) {
829
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
830
        }
831
832 1
        return $size[1];
833
    }
834
835
    /**
836
     * Returns image size informations.
837
     *
838
     * @see https://www.php.net/manual/function.getimagesize.php
839
     *
840
     * @return array|false
841
     */
842 1
    private function getImageSize()
843
    {
844 1
        if (!$this->data['type'] == 'image') {
845
            return false;
846
        }
847
848
        try {
849 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
850 1
                return false;
851
            }
852
        } catch (\Exception $e) {
853
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
854
        }
855
856 1
        return $size;
857
    }
858
859
    /**
860
     * Returns true if asset is a SVG.
861
     */
862 1
    private function isSVG(): bool
863
    {
864 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
865
    }
866
867
    /**
868
     * Returns SVG attributes.
869
     *
870
     * @return \SimpleXMLElement|false
871
     */
872 1
    private function getSvgAttributes()
873
    {
874 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
875
            return false;
876
        }
877
878 1
        return $xml->attributes();
879
    }
880
881
    /**
882
     * Replaces some characters by '_'.
883
     */
884 1
    private function sanitize(string $string): string
885
    {
886 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
887
    }
888
889
    /**
890
     * Builds CDN image URL.
891
     */
892
    private function buildImageCdnUrl(): string
893
    {
894
        return str_replace(
895
            [
896
                '%account%',
897
                '%image_url%',
898
                '%width%',
899
                '%quality%',
900
                '%format%',
901
            ],
902
            [
903
                $this->config->get('assets.images.cdn.account'),
904
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
905
                $this->data['width'],
906
                $this->config->get('assets.images.quality') ?? 75,
907
                $this->data['ext'],
908
            ],
909
            (string) $this->config->get('assets.images.cdn.url')
910
        );
911
    }
912
}
913