Test Failed
Pull Request — master (#1676)
by Arnaud
04:17
created

Asset::findFile()   D

Complexity

Conditions 18
Paths 24

Size

Total Lines 74
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 18.2433

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 40
cts 44
cp 0.9091
crap 18.2433
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
    public function __construct(Builder $builder, $paths, array $options = null)
63
    {
64 1
        $this->builder = $builder;
65
        $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 1
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
70 1
            }
71
            if (empty($path)) {
72
                throw new RuntimeException('The path to an asset can\'t be empty.');
73 1
            }
74
            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 1
            }
77
        });
78
        $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 1
97 1
        // handles options
98
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
99
        $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 1
108 1
        // fill data array with file(s) informations
109
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
110
        $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 1
                // 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
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
120 1
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
121 1
                    }
122
                    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 1
                    }
125
                }
126
                // missing allowed = empty path
127
                if ($file[$i]['missing']) {
128
                    $this->data['missing'] = true;
129 1
                    $this->data['path'] = $file[$i]['path'];
130 1
131 1
                    continue;
132
                }
133 1
                // set data
134
                $this->data['size'] += $file[$i]['size'];
135
                $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
                    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 1
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
151 1
                        }
152 1
                    }
153 1
                    // bundle: default filename
154 1
                    if ($pathsCount > 1 && empty($filename)) {
155
                        switch ($this->data['ext']) {
156
                            case 'scss':
157
                            case 'css':
158
                                $filename = '/styles.css';
159
                                break;
160 1
                            case 'js':
161
                                $filename = '/scripts.js';
162
                                break;
163 1
                            default:
164
                                throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
165
                        }
166
                    }
167
                    // bundle: filename and path
168
                    if (!empty($filename)) {
169
                        $this->data['filename'] = $filename;
170
                        $this->data['path'] = '/' . ltrim($filename, '/');
171
                    }
172
                }
173
                // bundle: files path
174
                $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 1
            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 1
            return $this;
345
        }
346
347 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
348 1
            $this->minified;
349
350 1
            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 1
            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
                if ($sizeAfter < $sizeBefore) {
404
                    $message = \sprintf(
405
                        '%s (%s Ko -> %s Ko)',
406
                        $message,
407
                        ceil($sizeBefore / 1000),
408
                        ceil($sizeAfter / 1000)
409
                    );
410 1
                }
411 1
                $this->data['content'] = Util\File::fileGetContents($filepath);
412 1
                $this->data['size'] = $sizeAfter;
413 1
                $cache->set($cacheKey, $this->data);
414
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
415 1
            } catch (\Exception $e) {
416 1
                $this->builder->getLogger()->error(\sprintf('Can\'t optimize image "%s": "%s"', $filepath, $e->getMessage()));
417
            }
418 1
        }
419
        $this->data = $cache->get($cacheKey, $this->data);
420
421
        return $this;
422
    }
423
424
    /**
425
     * Resizes an image with a new $width.
426 1
     *
427
     * @throws RuntimeException
428 1
     */
429
    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
        if ($width >= $this->data['width']) {
438 1
            return $this;
439 1
        }
440
441 1
        $assetResized = clone $this;
442
        $assetResized->data['width'] = $width;
443
444
        if ($this->isImageInCdn()) {
445 1
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
446 1
        }
447 1
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
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
451
        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
            if (!extension_loaded('gd')) {
456
                throw new RuntimeException('GD extension is required to use images resize.');
457 1
            }
458 1
459 1
            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
                    $constraint->aspectRatio();
463
                    $constraint->upsize();
464
                });
465 1
            } catch (\Exception $e) {
466 1
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
467 1
            }
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
                (string) $width,
472
                $assetResized->data['path']
473 1
            );
474
475
            try {
476 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
477 1
                    $img->interlace();
478 1
                }
479 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
480
                $img->destroy();
481
                $assetResized->data['height'] = $assetResized->getHeight();
482
                $assetResized->data['size'] = strlen($assetResized->data['content']);
483
            } catch (\Exception $e) {
484 1
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
485
            }
486 1
487
            $cache->set($cacheKey, $assetResized->data);
488 1
        }
489
        $assetResized->data = $cache->get($cacheKey);
490
491
        return $assetResized;
492
    }
493
494
    /**
495
     * Converts an image asset to WebP format.
496 1
     *
497
     * @throws RuntimeException
498 1
     */
499
    public function webp(?int $quality = null): self
500
    {
501
        if ($this->data['type'] !== 'image') {
502 1
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
503 1
        }
504
505
        if ($quality === null) {
506 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
507 1
        }
508 1
509
        $assetWebp = clone $this;
510 1
        $format = 'webp';
511
        $assetWebp['ext'] = $format;
512
513
        if ($this->isImageInCdn()) {
514 1
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
515 1
        }
516
517
        $img = ImageManager::make($assetWebp['content']);
518
        $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 1
     * Implements \ArrayAccess.
529
     */
530 1
    #[\ReturnTypeWillChange]
531 1
    public function offsetSet($offset, $value): void
532
    {
533
        if (!is_null($offset)) {
534
            $this->data[$offset] = $value;
535
        }
536
    }
537
538
    /**
539 1
     * Implements \ArrayAccess.
540
     */
541 1
    #[\ReturnTypeWillChange]
542
    public function offsetExists($offset): bool
543
    {
544
        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 1
     * Implements \ArrayAccess.
558
     */
559 1
    #[\ReturnTypeWillChange]
560
    public function offsetGet($offset)
561
    {
562
        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 1
     *
569
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
570 1
     */
571
    public function getIntegrity(string $algo = 'sha384'): string
572
    {
573
        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
    public function getAudio(): Mp3Info
582
    {
583
        if ($this->data['type'] !== 'audio') {
584
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
585
        }
586
587
        return new Mp3Info($this->data['file']);
588
    }
589
590
    /**
591
     * Returns the data URL (encoded in Base64).
592 1
     *
593
     * @throws RuntimeException
594 1
     */
595 1
    public function dataurl(): string
596
    {
597
        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
        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 1
     *
608
     * @throws RuntimeException
609 1
     */
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 1
            try {
615 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
616
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $filepath));
617
                if ($this->optimize) {
618
                    $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 1
     *
631
     * @return boolean
632 1
     */
633 1
    public function isImageInCdn()
634
    {
635
        if ($this->data['type'] != 'image' || $this->isSVG() || (bool) $this->config->get('assets.images.cdn.enabled') !== true) {
636
            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 1
     *
649
     * @throws RuntimeException
650 1
     */
651 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
652 1
    {
653
        $file = [
654 1
            'url' => null,
655 1
        ];
656 1
657 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
658
            if ($ignore_missing) {
659 1
                $file['path'] = $path;
660
                $file['missing'] = true;
661
662 1
                return $file;
663
            }
664
665 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
666 1
        }
667 1
668 1
        if (Util\Url::isUrl($path)) {
669 1
            $file['url'] = $path;
670
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
671 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
672 1
            $force_slash = true;
673
        }
674
        if ($force_slash) {
675 1
            $path = '/' . ltrim($path, '/');
676 1
        }
677
678 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
679 1
        $content = Util\File::fileGetContents($filePath);
680 1
681 1
        $file['filepath'] = $filePath;
682 1
        $file['path'] = $path;
683 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
684 1
        $file['type'] = $type;
685 1
        $file['subtype'] = $subtype;
686
        $file['size'] = filesize($filePath);
687 1
        $file['content'] = $content;
688
        $file['missing'] = false;
689
690
        return $file;
691
    }
692
693
    /**
694
     * Try to find the file:
695
     *   1. remote (if $path is a valid URL)
696
     *   2. in static/
697
     *   3. in themes/<theme>/static/
698
     * Returns local file path or false if file don't exists.
699
     *
700
     * @throws RuntimeException
701 1
     *
702
     * @return string|false
703
     */
704 1
    private function findFile(string $path, ?string $remote_fallback = null)
705 1
    {
706 1
        // in case of remote file: save it and returns cached file path
707 1
        if (Util\Url::isUrl($path)) {
708 1
            $url = $path;
709 1
            $urlHost = parse_url($path, PHP_URL_HOST);
710
            $urlPath = parse_url($path, PHP_URL_PATH);
711 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
712 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
713
            // Google Fonts hack
714 1
            if (strpos($urlPath, '/css') !== false) {
715 1
                $extension = 'css';
716 1
            }
717 1
            $relativePath = Page::slugify(\sprintf(
718 1
                '%s%s%s%s',
719 1
                $urlHost,
720 1
                $this->sanitize($urlPath),
721 1
                $urlQuery ? "-$urlQuery" : '',
722 1
                $urlQuery && $extension ? ".$extension" : ''
723 1
            ));
724
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
725 1
            if (!file_exists($filePath)) {
726 1
                if (!Util\Url::isRemoteFileExists($url)) {
727 1
                    // is there a fallback in assets/
728
                    if ($remote_fallback) {
729
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
730
                        if (Util\File::getFS()->exists($filePath)) {
731
                            return $filePath;
732 1
                        }
733
                    }
734 1
735
                    return false;
736
                }
737 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
738
                    return false;
739
                }
740 1
                if (strlen($content) <= 1) {
741
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
742
                }
743 1
                Util\File::getFS()->dumpFile($filePath, $content);
744
            }
745
746
            return $filePath;
747 1
        }
748 1
749 1
        // checks in assets/
750
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
751
        if (Util\File::getFS()->exists($filePath)) {
752
            return $filePath;
753 1
        }
754 1
755 1
        // checks in each themes/<theme>/assets/
756
        foreach ($this->config->getTheme() as $theme) {
757
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
758
            if (Util\File::getFS()->exists($filePath)) {
759
                return $filePath;
760
            }
761 1
        }
762 1
763 1
        // checks in static/
764
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
765
        if (Util\File::getFS()->exists($filePath)) {
766
            return $filePath;
767 1
        }
768 1
769 1
        // checks in each themes/<theme>/static/
770 1
        foreach ($this->config->getTheme() as $theme) {
771
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
772
            if (Util\File::getFS()->exists($filePath)) {
773
                return $filePath;
774 1
            }
775
        }
776
777
        return false;
778
    }
779
780
    /**
781
     * Returns the width of an image/SVG.
782 1
     *
783
     * @throws RuntimeException
784 1
     */
785
    private function getWidth(): int
786
    {
787 1
        if ($this->data['type'] != 'image') {
788 1
            return 0;
789
        }
790 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
791
            return (int) $svg->width;
792
        }
793
        if (false === $size = $this->getImageSize()) {
794 1
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
795
        }
796
797
        return $size[0];
798
    }
799
800
    /**
801
     * Returns the height of an image/SVG.
802 1
     *
803
     * @throws RuntimeException
804 1
     */
805
    private function getHeight(): int
806
    {
807 1
        if ($this->data['type'] != 'image') {
808 1
            return 0;
809
        }
810 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
811
            return (int) $svg->height;
812
        }
813
        if (false === $size = $this->getImageSize()) {
814 1
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
815
        }
816
817
        return $size[1];
818
    }
819
820
    /**
821
     * Returns image size informations.
822
     *
823
     * @see https://www.php.net/manual/function.getimagesize.php
824 1
     *
825
     * @return array|false
826 1
     */
827
    private function getImageSize()
828
    {
829
        if (!$this->data['type'] == 'image') {
830
            return false;
831 1
        }
832 1
833
        try {
834
            if (false === $size = getimagesizefromstring($this->data['content'])) {
835
                return false;
836
            }
837
        } catch (\Exception $e) {
838 1
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
839
        }
840
841
        return $size;
842
    }
843
844 1
    /**
845
     * Returns true if asset is a SVG.
846 1
     */
847
    private function isSVG(): bool
848
    {
849
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
850
    }
851
852
    /**
853
     * Returns SVG attributes.
854 1
     *
855
     * @return \SimpleXMLElement|false
856 1
     */
857
    private function getSvgAttributes()
858
    {
859
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
860 1
            return false;
861
        }
862
863
        return $xml->attributes();
864
    }
865
866 1
    /**
867
     * Replaces some characters by '_'.
868 1
     */
869
    private function sanitize(string $string): string
870
    {
871
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
872
    }
873
874
    /**
875
     * Builds CDN image URL.
876
     */
877
    private function buildImageCdnUrl(): string
878
    {
879
        return str_replace(
880
            [
881
                '%account%',
882
                '%image_url%',
883
                '%width%',
884
                '%quality%',
885
                '%format%',
886
            ],
887
            [
888
                $this->config->get('assets.images.cdn.account'),
889
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
890
                $this->data['width'],
891
                $this->config->get('assets.images.quality') ?? 75,
892
                $this->data['ext'],
893
            ],
894
            (string) $this->config->get('assets.images.cdn.url')
895
        );
896
    }
897
}
898