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

Asset::findFile()   D

Complexity

Conditions 18
Paths 24

Size

Total Lines 74
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 18.2276

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 18
eloc 43
c 1
b 1
f 0
nc 24
nop 2
dl 0
loc 74
ccs 41
cts 45
cp 0.9111
crap 18.2276
rs 4.8666

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
                    if (!empty($filename)) { /** @phpstan-ignore-line */
143 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
144
                    }
145 1
                    $this->data['url'] = $file[$i]['url'];
146 1
                    $this->data['ext'] = $file[$i]['ext'];
147 1
                    $this->data['type'] = $file[$i]['type'];
148 1
                    $this->data['subtype'] = $file[$i]['subtype'];
149 1
                    if ($this->data['type'] == 'image') {
150 1
                        $this->data['width'] = $this->getWidth();
151 1
                        $this->data['height'] = $this->getHeight();
152 1
                        if ($this->data['subtype'] == 'jpeg') {
153
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
154
                        }
155
                    }
156
                }
157
                // bundle files path
158 1
                $this->data['files'][] = $file[$i]['filepath'];
159
            }
160
            // bundle: define path
161 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
162
                switch ($this->data['ext']) {
163
                    case 'scss':
164
                    case 'css':
165
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
166
                        break;
167
                    case 'js':
168
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
169
                        break;
170
                    default:
171
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
172
                }
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 (\Exception $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 RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
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 1
            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 1
            return $this;
343
        }
344
345 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
346 1
            $this->minified;
347
348 1
            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
            try {
397 1
                $message = $this->data['path'];
398 1
                $sizeBefore = filesize($filepath);
399 1
                Optimizer::create($quality)->optimize($filepath);
400 1
                $sizeAfter = filesize($filepath);
401 1
                if ($sizeAfter < $sizeBefore) {
402
                    $message = \sprintf(
403
                        '%s (%s Ko -> %s Ko)',
404
                        $message,
405
                        ceil($sizeBefore / 1000),
406
                        ceil($sizeAfter / 1000)
407
                    );
408
                }
409 1
                $this->data['content'] = Util\File::fileGetContents($filepath);
410 1
                $this->data['size'] = $sizeAfter;
411 1
                $cache->set($cacheKey, $this->data);
412 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
413
            } catch (\Exception $e) {
414
                $this->builder->getLogger()->error(\sprintf('Can\'t optimize image "%s": "%s"', $filepath, $e->getMessage()));
415
            }
416
        }
417 1
        $this->data = $cache->get($cacheKey, $this->data);
418
419 1
        return $this;
420
    }
421
422
    /**
423
     * Resizes an image with a new $width.
424
     *
425
     * @throws RuntimeException
426
     */
427 1
    public function resize(int $width): self
428
    {
429 1
        if ($this->data['missing']) {
430
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
431
        }
432 1
        if ($this->data['type'] != 'image') {
433
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
434
        }
435 1
        if ($width >= $this->data['width']) {
436
            return $this;
437
        }
438
439 1
        $assetResized = clone $this;
440 1
        $assetResized->data['width'] = $width;
441
442 1
        if ($this->isImageInCdn()) {
443
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
444
        }
445
446 1
        $quality = $this->config->get('assets.images.quality');
447 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
448 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
449 1
        if (!$cache->has($cacheKey)) {
450 1
            if ($assetResized->data['type'] !== 'image') {
451
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
452
            }
453 1
            if (!extension_loaded('gd')) {
454
                throw new RuntimeException('GD extension is required to use images resize.');
455
            }
456
457
            try {
458 1
                $img = ImageManager::make($assetResized->data['content_source'])->encode($assetResized->data['ext']);
459 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
460 1
                    $constraint->aspectRatio();
461 1
                    $constraint->upsize();
462 1
                });
463
            } catch (\Exception $e) {
464
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
465
            }
466 1
            $assetResized->data['path'] = '/' . Util::joinPath(
467 1
                (string) $this->config->get('assets.target'),
468 1
                (string) $this->config->get('assets.images.resize.dir'),
469 1
                (string) $width,
470 1
                $assetResized->data['path']
471 1
            );
472
473
            try {
474 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
475
                    $img->interlace();
476
                }
477 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
478 1
                $img->destroy();
479 1
                $assetResized->data['height'] = $assetResized->getHeight();
480 1
                $assetResized->data['size'] = strlen($assetResized->data['content']);
481
            } catch (\Exception $e) {
482
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
483
            }
484
485 1
            $cache->set($cacheKey, $assetResized->data);
486
        }
487 1
        $assetResized->data = $cache->get($cacheKey);
488
489 1
        return $assetResized;
490
    }
491
492
    /**
493
     * Converts an image asset to WebP format.
494
     *
495
     * @throws RuntimeException
496
     */
497 1
    public function webp(?int $quality = null): self
498
    {
499 1
        if ($this->data['type'] !== 'image') {
500
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
501
        }
502
503 1
        if ($quality === null) {
504 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
505
        }
506
507 1
        $assetWebp = clone $this;
508 1
        $format = 'webp';
509 1
        $assetWebp['ext'] = $format;
510
511 1
        if ($this->isImageInCdn()) {
512
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
513
        }
514
515 1
        $img = ImageManager::make($assetWebp['content']);
516 1
        $assetWebp['content'] = (string) $img->encode($format, $quality);
517
        $img->destroy();
518
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
519
        $assetWebp['subtype'] = "image/$format";
520
        $assetWebp['size'] = strlen((string) $assetWebp['content']);
521
522
        return $assetWebp;
523
    }
524
525
    /**
526
     * Implements \ArrayAccess.
527
     */
528
    #[\ReturnTypeWillChange]
529 1
    public function offsetSet($offset, $value): void
530
    {
531 1
        if (!is_null($offset)) {
532 1
            $this->data[$offset] = $value;
533
        }
534
    }
535
536
    /**
537
     * Implements \ArrayAccess.
538
     */
539
    #[\ReturnTypeWillChange]
540 1
    public function offsetExists($offset): bool
541
    {
542 1
        return isset($this->data[$offset]);
543
    }
544
545
    /**
546
     * Implements \ArrayAccess.
547
     */
548
    #[\ReturnTypeWillChange]
549
    public function offsetUnset($offset): void
550
    {
551
        unset($this->data[$offset]);
552
    }
553
554
    /**
555
     * Implements \ArrayAccess.
556
     */
557
    #[\ReturnTypeWillChange]
558 1
    public function offsetGet($offset)
559
    {
560 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
561
    }
562
563
    /**
564
     * Hashing content of an asset with the specified algo, sha384 by default.
565
     * Used for SRI (Subresource Integrity).
566
     *
567
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
568
     */
569 1
    public function getIntegrity(string $algo = 'sha384'): string
570
    {
571 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
572
    }
573
574
    /**
575
     * Returns MP3 file infos.
576
     *
577
     * @see https://github.com/wapmorgan/Mp3Info
578
     */
579
    public function getAudio(): Mp3Info
580
    {
581
        if ($this->data['type'] !== 'audio') {
582
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
583
        }
584
585
        return new Mp3Info($this->data['file']);
586
    }
587
588
    /**
589
     * Returns the data URL (encoded in Base64).
590
     *
591
     * @throws RuntimeException
592
     */
593 1
    public function dataurl(): string
594
    {
595 1
        if ($this->data['type'] == 'image' && !$this->isSVG()) {
596 1
            return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
597
        }
598
599 1
        return sprintf("data:%s;base64,%s", $this->data['subtype'], base64_encode($this->data['content']));
600
    }
601
602
    /**
603
     * Saves file.
604
     * Note: a file from `static/` with the same name will NOT be overridden.
605
     *
606
     * @throws RuntimeException
607
     */
608 1
    public function save(): void
609
    {
610 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
611 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
612
            try {
613 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
614 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $filepath));
615 1
                if ($this->optimize) {
616 1
                    $this->optimize($filepath);
617
                }
618
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
619
                if (!$this->ignore_missing) {
620
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
621
                }
622
            }
623
        }
624
    }
625
626
    /**
627
     * Is Asset is an image in CDN.
628
     *
629
     * @return boolean
630
     */
631 1
    public function isImageInCdn()
632
    {
633 1
        if ($this->data['type'] != 'image' || $this->isSVG() || (bool) $this->config->get('assets.images.cdn.enabled') !== true) {
634 1
            return false;
635
        }
636
        // remote image?
637
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
638
            return false;
639
        }
640
641
        return true;
642
    }
643
644
    /**
645
     * Load file data.
646
     *
647
     * @throws RuntimeException
648
     */
649 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
650
    {
651 1
        $file = [
652 1
            'url' => null,
653 1
        ];
654
655 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
656 1
            if ($ignore_missing) {
657 1
                $file['path'] = $path;
658 1
                $file['missing'] = true;
659
660 1
                return $file;
661
            }
662
663 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
664
        }
665
666 1
        if (Util\Url::isUrl($path)) {
667 1
            $file['url'] = $path;
668 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
669 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
670 1
            $force_slash = true;
671
        }
672 1
        if ($force_slash) {
673 1
            $path = '/' . ltrim($path, '/');
674
        }
675
676 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
677 1
        $content = Util\File::fileGetContents($filePath);
678
679 1
        $file['filepath'] = $filePath;
680 1
        $file['path'] = $path;
681 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
682 1
        $file['type'] = $type;
683 1
        $file['subtype'] = $subtype;
684 1
        $file['size'] = filesize($filePath);
685 1
        $file['content'] = $content;
686 1
        $file['missing'] = false;
687
688 1
        return $file;
689
    }
690
691
    /**
692
     * Try to find the file:
693
     *   1. remote (if $path is a valid URL)
694
     *   2. in static/
695
     *   3. in themes/<theme>/static/
696
     * Returns local file path or false if file don't exists.
697
     *
698
     * @throws RuntimeException
699
     *
700
     * @return string|false
701
     */
702 1
    private function findFile(string $path, ?string $remote_fallback = null)
703
    {
704
        // in case of remote file: save it and returns cached file path
705 1
        if (Util\Url::isUrl($path)) {
706 1
            $url = $path;
707 1
            $urlHost = parse_url($path, PHP_URL_HOST);
708 1
            $urlPath = parse_url($path, PHP_URL_PATH);
709 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
710 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
711
            // Google Fonts hack
712 1
            if (strpos($urlPath, '/css') !== false) {
713 1
                $extension = 'css';
714
            }
715 1
            $relativePath = Page::slugify(\sprintf(
716 1
                '%s%s%s%s',
717 1
                $urlHost,
718 1
                $this->sanitize($urlPath),
719 1
                $urlQuery ? "-$urlQuery" : '',
720 1
                $urlQuery && $extension ? ".$extension" : ''
721 1
            ));
722 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
723 1
            if (!file_exists($filePath)) {
724 1
                if (!Util\Url::isRemoteFileExists($url)) {
725
                    // is there a fallback in assets/
726 1
                    if ($remote_fallback) {
727 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
728 1
                        if (Util\File::getFS()->exists($filePath)) {
729
                            return $filePath;
730
                        }
731
                    }
732
733 1
                    return false;
734
                }
735 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
736
                    return false;
737
                }
738 1
                if (strlen($content) <= 1) {
739
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
740
                }
741 1
                Util\File::getFS()->dumpFile($filePath, $content);
742
            }
743
744 1
            return $filePath;
745
        }
746
747
        // checks in assets/
748 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
749 1
        if (Util\File::getFS()->exists($filePath)) {
750 1
            return $filePath;
751
        }
752
753
        // checks in each themes/<theme>/assets/
754 1
        foreach ($this->config->getTheme() as $theme) {
755 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
756 1
            if (Util\File::getFS()->exists($filePath)) {
757
                return $filePath;
758
            }
759
        }
760
761
        // checks in static/
762 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
763 1
        if (Util\File::getFS()->exists($filePath)) {
764 1
            return $filePath;
765
        }
766
767
        // checks in each themes/<theme>/static/
768 1
        foreach ($this->config->getTheme() as $theme) {
769 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
770 1
            if (Util\File::getFS()->exists($filePath)) {
771 1
                return $filePath;
772
            }
773
        }
774
775 1
        return false;
776
    }
777
778
    /**
779
     * Returns the width of an image/SVG.
780
     *
781
     * @throws RuntimeException
782
     */
783 1
    private function getWidth(): int
784
    {
785 1
        if ($this->data['type'] != 'image') {
786
            return 0;
787
        }
788 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
789 1
            return (int) $svg->width;
790
        }
791 1
        if (false === $size = $this->getImageSize()) {
792
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
793
        }
794
795 1
        return $size[0];
796
    }
797
798
    /**
799
     * Returns the height of an image/SVG.
800
     *
801
     * @throws RuntimeException
802
     */
803 1
    private function getHeight(): int
804
    {
805 1
        if ($this->data['type'] != 'image') {
806
            return 0;
807
        }
808 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
809 1
            return (int) $svg->height;
810
        }
811 1
        if (false === $size = $this->getImageSize()) {
812
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
813
        }
814
815 1
        return $size[1];
816
    }
817
818
    /**
819
     * Returns image size informations.
820
     *
821
     * @see https://www.php.net/manual/function.getimagesize.php
822
     *
823
     * @return array|false
824
     */
825 1
    private function getImageSize()
826
    {
827 1
        if (!$this->data['type'] == 'image') {
828
            return false;
829
        }
830
831
        try {
832 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
833 1
                return false;
834
            }
835
        } catch (\Exception $e) {
836
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
837
        }
838
839 1
        return $size;
840
    }
841
842
    /**
843
     * Returns true if asset is a SVG.
844
     */
845 1
    private function isSVG(): bool
846
    {
847 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
848
    }
849
850
    /**
851
     * Returns SVG attributes.
852
     *
853
     * @return \SimpleXMLElement|false
854
     */
855 1
    private function getSvgAttributes()
856
    {
857 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
858
            return false;
859
        }
860
861 1
        return $xml->attributes();
862
    }
863
864
    /**
865
     * Replaces some characters by '_'.
866
     */
867 1
    private function sanitize(string $string): string
868
    {
869 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
870
    }
871
872
    /**
873
     * Builds CDN image URL.
874
     */
875
    private function buildImageCdnUrl(): string
876
    {
877
        return str_replace(
878
            [
879
                '%account%',
880
                '%image_url%',
881
                '%width%',
882
                '%quality%',
883
                '%format%',
884
            ],
885
            [
886
                $this->config->get('assets.images.cdn.account'),
887
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
888
                $this->data['width'],
889
                $this->config->get('assets.images.quality') ?? 75,
890
                $this->data['ext'],
891
            ],
892
            (string) $this->config->get('assets.images.cdn.url')
893
        );
894
    }
895
}
896