Passed
Pull Request — master (#1649)
by Arnaud
09:30 queued 04:11
created

Asset::__construct()   F

Complexity

Conditions 25
Paths 166

Size

Total Lines 131
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 80
CRAP Score 27.4605

Importance

Changes 8
Bugs 5 Features 1
Metric Value
cc 25
eloc 93
c 8
b 5
f 1
nc 166
nop 3
dl 0
loc 131
ccs 80
cts 95
cp 0.8421
crap 27.4605
rs 3.6166

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
    /** @var bool */
50
    protected $optimized = false;
51
52
    /** @var bool */
53
    protected $ignore_missing = false;
54
55
    /**
56
     * Creates an Asset from a file path, an array of files path or an URL.
57
     *
58
     * @param Builder      $builder
59
     * @param string|array $paths
60
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
61
     *
62
     * @throws RuntimeException
63
     */
64 1
    public function __construct(Builder $builder, $paths, array $options = null)
65
    {
66 1
        $this->builder = $builder;
67 1
        $this->config = $builder->getConfig();
68 1
        $paths = is_array($paths) ? $paths : [$paths];
69 1
        array_walk($paths, function ($path) {
70 1
            if (!is_string($path)) {
71
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
72
            }
73 1
            if (empty($path)) {
74
                throw new RuntimeException('The path to an asset can\'t be empty.');
75
            }
76 1
            if (substr($path, 0, 2) == '..') {
77
                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));
78
            }
79 1
        });
80 1
        $this->data = [
81 1
            'file'           => '',    // absolute file path
82 1
            'files'          => [],    // array of files path (if bundle)
83 1
            'filename'       => '',    // filename
84 1
            'path_source'    => '',    // public path to the file, before transformations
85 1
            'path'           => '',    // public path to the file, after transformations
86 1
            'url'            => null,  // URL of a remote image
87 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
88 1
            'ext'            => '',    // file extension
89 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
90 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
91 1
            'size'           => 0,     // file size (in bytes)
92 1
            'content_source' => '',    // file content, before transformations
93 1
            'content'        => '',    // file content, after transformations
94 1
            'width'          => 0,     // width (in pixels) in case of an image
95 1
            'height'         => 0,     // height (in pixels) in case of an image
96 1
            'exif'           => [],    // exif data
97 1
        ];
98
99
        // handles options
100 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
101 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
102 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
103 1
        $filename = '';
104 1
        $ignore_missing = false;
105 1
        $remote_fallback = null;
106 1
        $force_slash = true;
107 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
108 1
        $this->ignore_missing = $ignore_missing;
109
110
        // fill data array with file(s) informations
111 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
112 1
        $cacheKey = \sprintf('%s__%s', implode('_', $paths), $this->builder->getVersion());
113 1
        if (!$cache->has($cacheKey)) {
114 1
            $pathsCount = count($paths);
115 1
            $file = [];
116 1
            for ($i = 0; $i < $pathsCount; $i++) {
117
                // loads file(s)
118 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
119
                // bundle: same type/ext only
120 1
                if ($i > 0) {
121 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
122
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
123
                    }
124 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
125
                        throw new RuntimeException(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
126
                    }
127
                }
128
                // missing allowed = empty path
129 1
                if ($file[$i]['missing']) {
130 1
                    $this->data['missing'] = true;
131 1
                    $this->data['path'] = $file[$i]['path'];
132
133 1
                    continue;
134
                }
135
                // set data
136 1
                $this->data['size'] += $file[$i]['size'];
137 1
                $this->data['content_source'] .= $file[$i]['content'];
138 1
                $this->data['content'] .= $file[$i]['content'];
139 1
                if ($i == 0) {
140 1
                    $this->data['file'] = $file[$i]['filepath'];
141 1
                    $this->data['filename'] = $file[$i]['path'];
142 1
                    $this->data['path_source'] = $file[$i]['path'];
143 1
                    $this->data['path'] = $file[$i]['path'];
144 1
                    if (!empty($filename)) { /** @phpstan-ignore-line */
145 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
146
                    }
147 1
                    $this->data['url'] = $file[$i]['url'];
148 1
                    $this->data['ext'] = $file[$i]['ext'];
149 1
                    $this->data['type'] = $file[$i]['type'];
150 1
                    $this->data['subtype'] = $file[$i]['subtype'];
151 1
                    if ($this->data['type'] == 'image') {
152 1
                        $this->data['width'] = $this->getWidth();
153 1
                        $this->data['height'] = $this->getHeight();
154 1
                        if ($this->data['subtype'] == 'jpeg') {
155
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
156
                        }
157
                    }
158
                }
159
                // bundle files path
160 1
                $this->data['files'][] = $file[$i]['filepath'];
161
            }
162
            // bundle: define path
163 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
164
                switch ($this->data['ext']) {
165
                    case 'scss':
166
                    case 'css':
167
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
168
                        break;
169
                    case 'js':
170
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
171
                        break;
172
                    default:
173
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
174
                }
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 str_replace(
213
                [
214
                    '%account%',
215
                    '%image_url%',
216
                    '%width%',
217
                    '%quality%',
218
                    '%format%',
219
                ],
220
                [
221
                    $this->config->get('assets.images.cdn.account'),
222
                    $this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => true]),
223
                    $this->data['width'],
224
                    $this->config->get('assets.images.quality') ?? 75,
225
                    $this->data['ext'],
226
                ],
227
                (string) $this->config->get('assets.images.cdn.url')
228
            );
229
        }
230
231 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
232
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
233
        }
234
235 1
        return $this->data['path'];
236
    }
237
238
    /**
239
     * Fingerprints a file.
240
     */
241 1
    public function fingerprint(): self
242
    {
243 1
        if ($this->fingerprinted) {
244 1
            return $this;
245
        }
246
247 1
        $fingerprint = hash('md5', $this->data['content_source']);
248 1
        $this->data['path'] = preg_replace(
249 1
            '/\.' . $this->data['ext'] . '$/m',
250 1
            ".$fingerprint." . $this->data['ext'],
251 1
            $this->data['path']
252 1
        );
253
254 1
        $this->fingerprinted = true;
255
256 1
        return $this;
257
    }
258
259
    /**
260
     * Compiles a SCSS.
261
     *
262
     * @throws RuntimeException
263
     */
264 1
    public function compile(): self
265
    {
266 1
        if ($this->compiled) {
267 1
            return $this;
268
        }
269
270 1
        if ($this->data['ext'] != 'scss') {
271 1
            return $this;
272
        }
273
274 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
275 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
276 1
        if (!$cache->has($cacheKey)) {
277 1
            $scssPhp = new Compiler();
278 1
            $importDir = [];
279 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
280 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
281 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
282 1
            $themes = $this->config->getTheme() ?? [];
283 1
            foreach ($scssDir as $dir) {
284 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
285 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
286 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
287 1
                foreach ($themes as $theme) {
288 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
289 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
290
                }
291
            }
292 1
            $scssPhp->setImportPaths(array_unique($importDir));
293
            // source map
294 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
295
                $importDir = [];
296
                $assetDir = (string) $this->config->get('assets.dir');
297
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
298
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
299
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
300
                $importDir[] = dirname($filePath);
301
                foreach ($scssDir as $dir) {
302
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
303
                }
304
                $scssPhp->setImportPaths(array_unique($importDir));
305
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
306
                $scssPhp->setSourceMapOptions([
307
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
308
                    'sourceRoot'        => '/',
309
                ]);
310
            }
311
            // output style
312 1
            $outputStyles = ['expanded', 'compressed'];
313 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
314 1
            if (!in_array($outputStyle, $outputStyles)) {
315
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
316
            }
317 1
            $scssPhp->setOutputStyle($outputStyle);
318
            // variables
319 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
320 1
            if (!empty($variables)) {
321 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
322 1
                $scssPhp->replaceVariables($variables);
323
            }
324
            // update data
325 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
326 1
            $this->data['ext'] = 'css';
327 1
            $this->data['type'] = 'text';
328 1
            $this->data['subtype'] = 'text/css';
329 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
330 1
            $this->data['size'] = strlen($this->data['content']);
331 1
            $this->compiled = true;
332 1
            $cache->set($cacheKey, $this->data);
333
        }
334 1
        $this->data = $cache->get($cacheKey);
335
336 1
        return $this;
337
    }
338
339
    /**
340
     * Minifying a CSS or a JS.
341
     *
342
     * @throws RuntimeException
343
     */
344 1
    public function minify(): self
345
    {
346
        // disable minify to preserve inline source map
347 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
348
            return $this;
349
        }
350
351 1
        if ($this->minified) {
352 1
            return $this;
353
        }
354
355 1
        if ($this->data['ext'] == 'scss') {
356
            $this->compile();
357
        }
358
359 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
360 1
            return $this;
361
        }
362
363 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
364 1
            $this->minified;
365
366 1
            return $this;
367
        }
368
369 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
370 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
371 1
        if (!$cache->has($cacheKey)) {
372 1
            switch ($this->data['ext']) {
373 1
                case 'css':
374 1
                    $minifier = new Minify\CSS($this->data['content']);
375 1
                    break;
376 1
                case 'js':
377 1
                    $minifier = new Minify\JS($this->data['content']);
378 1
                    break;
379
                default:
380
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
381
            }
382 1
            $this->data['path'] = preg_replace(
383 1
                '/\.' . $this->data['ext'] . '$/m',
384 1
                '.min.' . $this->data['ext'],
385 1
                $this->data['path']
386 1
            );
387 1
            $this->data['content'] = $minifier->minify();
388 1
            $this->data['size'] = strlen($this->data['content']);
389 1
            $this->minified = true;
390 1
            $cache->set($cacheKey, $this->data);
391
        }
392 1
        $this->data = $cache->get($cacheKey);
393
394 1
        return $this;
395
    }
396
397
    /**
398
     * Optimizing an image.
399
     */
400 1
    public function optimize(string $filepath): self
401
    {
402 1
        if ($this->data['type'] != 'image') {
403 1
            return $this;
404
        }
405
406 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
407 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
408 1
        $tags = ["q$quality", 'optimized'];
409 1
        if ($this->data['width']) {
410 1
            array_unshift($tags, "{$this->data['width']}x");
411
        }
412 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
413 1
        if (!$cache->has($cacheKey)) {
414 1
            $message = $this->data['path'];
415 1
            $sizeBefore = filesize($filepath);
416 1
            Optimizer::create($quality)->optimize($filepath);
417 1
            $sizeAfter = filesize($filepath);
418 1
            if ($sizeAfter < $sizeBefore) {
419
                $message = \sprintf(
420
                    '%s (%s Ko -> %s Ko)',
421
                    $message,
422
                    ceil($sizeBefore / 1000),
423
                    ceil($sizeAfter / 1000)
424
                );
425
            }
426 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
427 1
            $this->data['size'] = $sizeAfter;
428 1
            $cache->set($cacheKey, $this->data);
429 1
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
430
        }
431 1
        $this->data = $cache->get($cacheKey);
432 1
        $this->optimized = true;
433
434 1
        return $this;
435
    }
436
437
    /**
438
     * Resizes an image with a new $width.
439
     *
440
     * @throws RuntimeException
441
     */
442 1
    public function resize(int $width): self
443
    {
444 1
        if ($this->data['missing']) {
445
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
446
        }
447 1
        if ($this->data['type'] != 'image') {
448
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
449
        }
450 1
        if ($width >= $this->data['width']) {
451
            return $this;
452
        }
453
454 1
        $assetResized = clone $this;
455 1
        $assetResized->data['width'] = $width;
456
457 1
        $quality = $this->config->get('assets.images.quality');
458 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
459 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
460 1
        if (!$cache->has($cacheKey)) {
461 1
            if ($assetResized->data['type'] !== 'image') {
462
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
463
            }
464 1
            if (!extension_loaded('gd')) {
465
                throw new RuntimeException('GD extension is required to use images resize.');
466
            }
467
468
            try {
469 1
                $img = ImageManager::make($assetResized->data['content_source']);
470 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
471 1
                    $constraint->aspectRatio();
472 1
                    $constraint->upsize();
473 1
                });
474
            } catch (\Exception $e) {
475
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
476
            }
477 1
            $assetResized->data['path'] = '/' . Util::joinPath(
478 1
                (string) $this->config->get('assets.target'),
479 1
                (string) $this->config->get('assets.images.resize.dir'),
480 1
                (string) $width,
481 1
                $assetResized->data['path']
482 1
            );
483
484
            try {
485 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
486 1
                $assetResized->data['height'] = $assetResized->getHeight();
487 1
                $assetResized->data['size'] = strlen($assetResized->data['content']);
488
            } catch (\Exception $e) {
489
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
490
            }
491
492 1
            $cache->set($cacheKey, $assetResized->data);
493
        }
494 1
        $assetResized->data = $cache->get($cacheKey);
495
496 1
        return $assetResized;
497
    }
498
499
    /**
500
     * Converts an image asset to WebP format.
501
     *
502
     * @throws RuntimeException
503
     */
504 1
    public function webp(?int $quality = null): self
505
    {
506 1
        if ($this->data['type'] !== 'image') {
507
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
508
        }
509
510 1
        if ($quality === null) {
511 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
512
        }
513
514 1
        $assetWebp = clone $this;
515 1
        $format = 'webp';
516 1
        $image = ImageManager::make($assetWebp['content']);
517 1
        $assetWebp['content'] = (string) $image->encode($format, $quality);
518
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
519
        $assetWebp['ext'] = $format;
520
        $assetWebp['subtype'] = "image/$format";
521
        $assetWebp['size'] = strlen($assetWebp['content']);
0 ignored issues
show
Bug introduced by
It seems like $assetWebp['content'] can also be of type null; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

521
        $assetWebp['size'] = strlen(/** @scrutinizer ignore-type */ $assetWebp['content']);
Loading history...
522
523
        return $assetWebp;
524
    }
525
526
    /**
527
     * Implements \ArrayAccess.
528
     */
529
    #[\ReturnTypeWillChange]
530
    public function offsetSet($offset, $value): void
531
    {
532
        if (!is_null($offset)) {
533
            $this->data[$offset] = $value;
534
        }
535
    }
536
537
    /**
538
     * Implements \ArrayAccess.
539
     */
540
    #[\ReturnTypeWillChange]
541 1
    public function offsetExists($offset): bool
542
    {
543 1
        return isset($this->data[$offset]);
544
    }
545
546
    /**
547
     * Implements \ArrayAccess.
548
     */
549
    #[\ReturnTypeWillChange]
550
    public function offsetUnset($offset): void
551
    {
552
        unset($this->data[$offset]);
553
    }
554
555
    /**
556
     * Implements \ArrayAccess.
557
     */
558
    #[\ReturnTypeWillChange]
559 1
    public function offsetGet($offset)
560
    {
561 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
562
    }
563
564
    /**
565
     * Hashing content of an asset with the specified algo, sha384 by default.
566
     * Used for SRI (Subresource Integrity).
567
     *
568
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
569
     */
570 1
    public function getIntegrity(string $algo = 'sha384'): string
571
    {
572 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
573
    }
574
575
    /**
576
     * Returns MP3 file infos.
577
     *
578
     * @see https://github.com/wapmorgan/Mp3Info
579
     */
580
    public function getAudio(): Mp3Info
581
    {
582
        if ($this->data['type'] !== 'audio') {
583
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
584
        }
585
586
        return new Mp3Info($this->data['file']);
587
    }
588
589
    /**
590
     * Returns the data URL of an image.
591
     *
592
     * @throws RuntimeException
593
     */
594 1
    public function dataurl(): string
595
    {
596 1
        if ($this->data['type'] !== 'image') {
597
            throw new RuntimeException(\sprintf('Can\'t get data URL of "%s"', $this->data['path']));
598
        }
599
600 1
        return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
601
    }
602
603
    /**
604
     * Saves file.
605
     * Note: a file from `static/` with the same name will NOT be overridden.
606
     *
607
     * @throws RuntimeException
608
     */
609 1
    public function save(): void
610
    {
611 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
612 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
613
            try {
614 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
615 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $this->data['path']));
616 1
                if ($this->optimize) {
617 1
                    $this->optimize($filepath);
618
                }
619
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
620
                if (!$this->ignore_missing) {
621
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
622
                }
623
            }
624
        }
625
    }
626
627
    /**
628
     * Is Asset is an image in CDN.
629
     *
630
     * @return boolean
631
     */
632 1
    public function isImageInCdn()
633
    {
634 1
        return $this->data['type'] == 'image' && (bool) $this->config->get('assets.images.cdn.enabled');
635
    }
636
637
    /**
638
     * Load file data.
639
     *
640
     * @throws RuntimeException
641
     */
642 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
643
    {
644 1
        $file = [
645 1
            'url' => null,
646 1
        ];
647
648 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
649 1
            if ($ignore_missing) {
650 1
                $file['path'] = $path;
651 1
                $file['missing'] = true;
652
653 1
                return $file;
654
            }
655
656 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
657
        }
658
659 1
        if (Util\Url::isUrl($path)) {
660 1
            $file['url'] = $path;
661 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
662 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
663 1
            $force_slash = true;
664
        }
665 1
        if ($force_slash) {
666 1
            $path = '/' . ltrim($path, '/');
667
        }
668
669 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
670 1
        $content = Util\File::fileGetContents($filePath);
671
672 1
        $file['filepath'] = $filePath;
673 1
        $file['path'] = $path;
674 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
675 1
        $file['type'] = $type;
676 1
        $file['subtype'] = $subtype;
677 1
        $file['size'] = filesize($filePath);
678 1
        $file['content'] = $content;
679 1
        $file['missing'] = false;
680
681 1
        return $file;
682
    }
683
684
    /**
685
     * Try to find the file:
686
     *   1. remote (if $path is a valid URL)
687
     *   2. in static/
688
     *   3. in themes/<theme>/static/
689
     * Returns local file path or false if file don't exists.
690
     *
691
     * @throws RuntimeException
692
     *
693
     * @return string|false
694
     */
695 1
    private function findFile(string $path, ?string $remote_fallback = null)
696
    {
697
        // in case of remote file: save it and returns cached file path
698 1
        if (Util\Url::isUrl($path)) {
699 1
            $url = $path;
700 1
            $urlHost = parse_url($path, PHP_URL_HOST);
701 1
            $urlPath = parse_url($path, PHP_URL_PATH);
702 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
703 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
704
            // Google Fonts hack
705 1
            if (strpos($urlPath, '/css') !== false) {
706 1
                $extension = 'css';
707
            }
708 1
            $relativePath = Page::slugify(\sprintf(
709 1
                '%s%s%s%s',
710 1
                $urlHost,
711 1
                $this->sanitize($urlPath),
712 1
                $urlQuery ? "-$urlQuery" : '',
713 1
                $urlQuery && $extension ? " . $extension" : ''
714 1
            ));
715 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
716 1
            if (!file_exists($filePath)) {
717 1
                if (!Util\Url::isRemoteFileExists($url)) {
718
                    // is there a fallback in assets/
719 1
                    if ($remote_fallback) {
720 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
721 1
                        if (Util\File::getFS()->exists($filePath)) {
722
                            return $filePath;
723
                        }
724
                    }
725
726 1
                    return false;
727
                }
728 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
729
                    return false;
730
                }
731 1
                if (strlen($content) <= 1) {
732
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
733
                }
734 1
                Util\File::getFS()->dumpFile($filePath, $content);
735
            }
736
737 1
            return $filePath;
738
        }
739
740
        // checks in assets/
741 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
742 1
        if (Util\File::getFS()->exists($filePath)) {
743 1
            return $filePath;
744
        }
745
746
        // checks in each themes/<theme>/assets/
747 1
        foreach ($this->config->getTheme() as $theme) {
748 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
749 1
            if (Util\File::getFS()->exists($filePath)) {
750
                return $filePath;
751
            }
752
        }
753
754
        // checks in static/
755 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
756 1
        if (Util\File::getFS()->exists($filePath)) {
757 1
            return $filePath;
758
        }
759
760
        // checks in each themes/<theme>/static/
761 1
        foreach ($this->config->getTheme() as $theme) {
762 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
763 1
            if (Util\File::getFS()->exists($filePath)) {
764 1
                return $filePath;
765
            }
766
        }
767
768 1
        return false;
769
    }
770
771
    /**
772
     * Returns the width of an image/SVG.
773
     *
774
     * @throws RuntimeException
775
     */
776 1
    private function getWidth(): int
777
    {
778 1
        if ($this->data['type'] != 'image') {
779
            return 0;
780
        }
781 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
782 1
            return (int) $svg->width;
783
        }
784 1
        if (false === $size = $this->getImageSize()) {
785
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
786
        }
787
788 1
        return $size[0];
789
    }
790
791
    /**
792
     * Returns the height of an image/SVG.
793
     *
794
     * @throws RuntimeException
795
     */
796 1
    private function getHeight(): int
797
    {
798 1
        if ($this->data['type'] != 'image') {
799
            return 0;
800
        }
801 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
802 1
            return (int) $svg->height;
803
        }
804 1
        if (false === $size = $this->getImageSize()) {
805
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
806
        }
807
808 1
        return $size[1];
809
    }
810
811
    /**
812
     * Returns image size informations.
813
     *
814
     * @see https://www.php.net/manual/function.getimagesize.php
815
     *
816
     * @return array|false
817
     */
818 1
    private function getImageSize()
819
    {
820 1
        if (!$this->data['type'] == 'image') {
821
            return false;
822
        }
823
824
        try {
825 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
826 1
                return false;
827
            }
828
        } catch (\Exception $e) {
829
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
830
        }
831
832 1
        return $size;
833
    }
834
835
    /**
836
     * Returns true if asset is a SVG.
837
     */
838 1
    private function isSVG(): bool
839
    {
840 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
841
    }
842
843
    /**
844
     * Returns SVG attributes.
845
     *
846
     * @return \SimpleXMLElement|false
847
     */
848 1
    private function getSvgAttributes()
849
    {
850 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
851
            return false;
852
        }
853
854 1
        return $xml->attributes();
855
    }
856
857
    /**
858
     * Replaces some characters by '_'.
859
     */
860 1
    private function sanitize(string $string): string
861
    {
862 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
863
    }
864
}
865