Passed
Pull Request — master (#1676)
by Arnaud
11:07 queued 04:27
created

Asset::__construct()   D

Complexity

Conditions 26
Paths 80

Size

Total Lines 133
Code Lines 94

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 86
CRAP Score 26.7648

Importance

Changes 9
Bugs 5 Features 1
Metric Value
cc 26
eloc 94
c 9
b 5
f 1
nc 80
nop 3
dl 0
loc 133
ccs 86
cts 96
cp 0.8958
crap 26.7648
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Assets\Image\Optimizer;
17
use Cecil\Builder;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\RuntimeException;
21
use Cecil\Util;
22
use Intervention\Image\ImageManagerStatic as ImageManager;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use wapmorgan\Mp3Info\Mp3Info;
26
27
class Asset implements \ArrayAccess
28
{
29
    /** @var Builder */
30
    protected $builder;
31
32
    /** @var Config */
33
    protected $config;
34
35
    /** @var array */
36
    protected $data = [];
37
38
    /** @var bool */
39
    protected $fingerprinted = false;
40
41
    /** @var bool */
42
    protected $compiled = false;
43
44
    /** @var bool */
45
    protected $minified = false;
46
47
    /** @var bool */
48
    protected $optimize = false;
49
50
    /** @var bool */
51
    protected $ignore_missing = false;
52
53
    /**
54
     * Creates an Asset from a file path, an array of files path or an URL.
55
     *
56
     * @param Builder      $builder
57
     * @param string|array $paths
58
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
59
     *
60
     * @throws RuntimeException
61
     */
62 1
    public function __construct(Builder $builder, $paths, array $options = null)
63
    {
64 1
        $this->builder = $builder;
65 1
        $this->config = $builder->getConfig();
66 1
        $paths = is_array($paths) ? $paths : [$paths];
67 1
        array_walk($paths, function ($path) {
68 1
            if (!is_string($path)) {
69
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
70
            }
71 1
            if (empty($path)) {
72
                throw new RuntimeException('The path to an asset can\'t be empty.');
73
            }
74 1
            if (substr($path, 0, 2) == '..') {
75
                throw new RuntimeException(\sprintf('The path to asset "%s" is wrong: it must be directly relative to "assets" or "static" directory, or a remote URL.', $path));
76
            }
77 1
        });
78 1
        $this->data = [
79 1
            'file'           => '',    // absolute file path
80 1
            'files'          => [],    // array of files path (if bundle)
81 1
            'filename'       => '',    // filename
82 1
            'path_source'    => '',    // public path to the file, before transformations
83 1
            'path'           => '',    // public path to the file, after transformations
84 1
            'url'            => null,  // URL of a remote image
85 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
86 1
            'ext'            => '',    // file extension
87 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
88 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
89 1
            'size'           => 0,     // file size (in bytes)
90 1
            'content_source' => '',    // file content, before transformations
91 1
            'content'        => '',    // file content, after transformations
92 1
            'width'          => 0,     // width (in pixels) in case of an image
93 1
            'height'         => 0,     // height (in pixels) in case of an image
94 1
            'exif'           => [],    // exif data
95 1
        ];
96
97
        // handles options
98 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
99 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
100 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
101 1
        $filename = '';
102 1
        $ignore_missing = false;
103 1
        $remote_fallback = null;
104 1
        $force_slash = true;
105 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
106 1
        $this->ignore_missing = $ignore_missing;
107
108
        // fill data array with file(s) informations
109 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
110 1
        $cacheKey = \sprintf('%s__%s', $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
                            case 'js':
161
                                $filename = '/scripts.js';
162
                                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 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
            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
            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
    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
     *
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 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
658 1
            if ($ignore_missing) {
659 1
                $file['path'] = $path;
660 1
                $file['missing'] = true;
661
662 1
                return $file;
663
            }
664
665 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
666
        }
667
668 1
        if (Util\Url::isUrl($path)) {
669 1
            $file['url'] = $path;
670 1
            $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 1
        if ($force_slash) {
675 1
            $path = '/' . ltrim($path, '/');
676
        }
677
678 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
679 1
        $content = Util\File::fileGetContents($filePath);
680
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 1
        $file['size'] = filesize($filePath);
687 1
        $file['content'] = $content;
688 1
        $file['missing'] = false;
689
690 1
        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
     *
702
     * @return string|false
703
     */
704 1
    private function findFile(string $path, ?string $remote_fallback = null)
705
    {
706
        // 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 1
            $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
            }
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 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
725 1
            if (!file_exists($filePath)) {
726 1
                if (!Util\Url::isRemoteFileExists($url)) {
727
                    // is there a fallback in assets/
728 1
                    if ($remote_fallback) {
729 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
730 1
                        if (Util\File::getFS()->exists($filePath)) {
731
                            return $filePath;
732
                        }
733
                    }
734
735 1
                    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 1
            return $filePath;
747
        }
748
749
        // checks in assets/
750 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
751 1
        if (Util\File::getFS()->exists($filePath)) {
752 1
            return $filePath;
753
        }
754
755
        // checks in each themes/<theme>/assets/
756 1
        foreach ($this->config->getTheme() as $theme) {
757 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
758 1
            if (Util\File::getFS()->exists($filePath)) {
759
                return $filePath;
760
            }
761
        }
762
763
        // checks in static/
764 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
765 1
        if (Util\File::getFS()->exists($filePath)) {
766 1
            return $filePath;
767
        }
768
769
        // checks in each themes/<theme>/static/
770 1
        foreach ($this->config->getTheme() as $theme) {
771 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
772 1
            if (Util\File::getFS()->exists($filePath)) {
773 1
                return $filePath;
774
            }
775
        }
776
777 1
        return false;
778
    }
779
780
    /**
781
     * Returns the width of an image/SVG.
782
     *
783
     * @throws RuntimeException
784
     */
785 1
    private function getWidth(): int
786
    {
787 1
        if ($this->data['type'] != 'image') {
788
            return 0;
789
        }
790 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
791 1
            return (int) $svg->width;
792
        }
793 1
        if (false === $size = $this->getImageSize()) {
794
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
795
        }
796
797 1
        return $size[0];
798
    }
799
800
    /**
801
     * Returns the height of an image/SVG.
802
     *
803
     * @throws RuntimeException
804
     */
805 1
    private function getHeight(): int
806
    {
807 1
        if ($this->data['type'] != 'image') {
808
            return 0;
809
        }
810 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
811 1
            return (int) $svg->height;
812
        }
813 1
        if (false === $size = $this->getImageSize()) {
814
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
815
        }
816
817 1
        return $size[1];
818
    }
819
820
    /**
821
     * Returns image size informations.
822
     *
823
     * @see https://www.php.net/manual/function.getimagesize.php
824
     *
825
     * @return array|false
826
     */
827 1
    private function getImageSize()
828
    {
829 1
        if (!$this->data['type'] == 'image') {
830
            return false;
831
        }
832
833
        try {
834 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
835 1
                return false;
836
            }
837
        } catch (\Exception $e) {
838
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
839
        }
840
841 1
        return $size;
842
    }
843
844
    /**
845
     * Returns true if asset is a SVG.
846
     */
847 1
    private function isSVG(): bool
848
    {
849 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
850
    }
851
852
    /**
853
     * Returns SVG attributes.
854
     *
855
     * @return \SimpleXMLElement|false
856
     */
857 1
    private function getSvgAttributes()
858
    {
859 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
860
            return false;
861
        }
862
863 1
        return $xml->attributes();
864
    }
865
866
    /**
867
     * Replaces some characters by '_'.
868
     */
869 1
    private function sanitize(string $string): string
870
    {
871 1
        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