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

Asset::findFile()   D

Complexity

Conditions 22
Paths 54

Size

Total Lines 87
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 24.5939

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 22
eloc 51
c 2
b 0
f 0
nc 54
nop 2
dl 0
loc 87
ccs 33
cts 40
cp 0.825
crap 24.5939
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
    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', 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
            // bundle: define path
177
            if ($pathsCount > 1 && empty($filename)) {
178 1
                switch ($this->data['ext']) {
179
                    case 'scss':
180
                    case 'css':
181 1
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
182 1
                        break;
183
                    case 'js':
184
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
185 1
                        break;
186 1
                    default:
187
                        throw new RuntimeException(sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
188
                }
189 1
            }
190 1
            $cache->set($cacheKey, $this->data);
191
        }
192
        $this->data = $cache->get($cacheKey);
193 1
194 1
        // fingerprinting
195
        if ($fingerprint) {
196
            $this->fingerprint();
197
        }
198
        // compiling (Sass files)
199
        if ((bool) $this->config->get('assets.compile.enabled')) {
200
            $this->compile();
201
        }
202
        // minifying (CSS and JavScript files)
203 1
        if ($minify) {
204
            $this->minify();
205
        }
206 1
        // optimizing (images files)
207
        if ($optimize) {
208
            $this->optimize = true;
209
        }
210
    }
211 1
212
    /**
213
     * Returns path.
214
     *
215 1
     * @throws RuntimeException
216
     */
217
    public function __toString(): string
218
    {
219 1
        try {
220
            $this->save();
221
        } catch (\Exception $e) {
222
            $this->builder->getLogger()->error($e->getMessage());
223
        }
224
225 1
        if ($this->isImageInCdn()) {
226
            return $this->buildImageCdnUrl();
227 1
        }
228 1
229
        if ($this->builder->getConfig()->get('canonicalurl')) {
230
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
231 1
        }
232 1
233 1
        return $this->data['path'];
234 1
    }
235 1
236 1
    /**
237
     * Fingerprints a file.
238 1
     */
239
    public function fingerprint(): self
240 1
    {
241
        if ($this->fingerprinted) {
242
            return $this;
243
        }
244
245
        $fingerprint = hash('md5', $this->data['content_source']);
246
        $this->data['path'] = preg_replace(
247
            '/\.' . $this->data['ext'] . '$/m',
248 1
            ".$fingerprint." . $this->data['ext'],
249
            $this->data['path']
250 1
        );
251 1
252
        $this->fingerprinted = true;
253
254 1
        return $this;
255 1
    }
256
257
    /**
258 1
     * Compiles a SCSS.
259 1
     *
260 1
     * @throws RuntimeException
261 1
     */
262 1
    public function compile(): self
263 1
    {
264 1
        if ($this->compiled) {
265 1
            return $this;
266 1
        }
267 1
268 1
        if ($this->data['ext'] != 'scss') {
269 1
            return $this;
270 1
        }
271 1
272 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
273 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
274
        if (!$cache->has($cacheKey)) {
275
            $scssPhp = new Compiler();
276 1
            $importDir = [];
277
            $importDir[] = Util::joinPath($this->config->getStaticPath());
278 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
279
            $scssDir = $this->config->get('assets.compile.import') ?? [];
280
            $themes = $this->config->getTheme() ?? [];
281
            foreach ($scssDir as $dir) {
282
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
283
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
284
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
285
                foreach ($themes as $theme) {
286
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
287
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
288
                }
289
            }
290
            $scssPhp->setImportPaths(array_unique($importDir));
291
            // source map
292
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
293
                $importDir = [];
294
                $assetDir = (string) $this->config->get('assets.dir');
295
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
296 1
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
297 1
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
298 1
                $importDir[] = \dirname($filePath);
299
                foreach ($scssDir as $dir) {
300
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
301 1
                }
302
                $scssPhp->setImportPaths(array_unique($importDir));
303 1
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
304 1
                $scssPhp->setSourceMapOptions([
305 1
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
306 1
                    'sourceRoot'        => '/',
307
                ]);
308
            }
309 1
            // output style
310 1
            $outputStyles = ['expanded', 'compressed'];
311 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
312 1
            if (!\in_array($outputStyle, $outputStyles)) {
313 1
                throw new RuntimeException(sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
314 1
            }
315 1
            $scssPhp->setOutputStyle($outputStyle);
316 1
            // variables
317
            $variables = $this->config->get('assets.compile.variables') ?? [];
318 1
            if (!empty($variables)) {
319
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
320 1
                $scssPhp->replaceVariables($variables);
321
            }
322
            // update data
323
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
324
            $this->data['ext'] = 'css';
325
            $this->data['type'] = 'text';
326
            $this->data['subtype'] = 'text/css';
327
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
328 1
            $this->data['size'] = \strlen($this->data['content']);
329
            $this->compiled = true;
330
            $cache->set($cacheKey, $this->data);
331 1
        }
332
        $this->data = $cache->get($cacheKey);
333
334
        return $this;
335 1
    }
336
337
    /**
338
     * Minifying a CSS or a JS.
339 1
     *
340
     * @throws RuntimeException
341
     */
342
    public function minify(): self
343 1
    {
344 1
        // disable minify to preserve inline source map
345
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
346
            return $this;
347 1
        }
348 1
349
        if ($this->minified) {
350 1
            return $this;
351
        }
352
353 1
        if ($this->data['ext'] == 'scss') {
354 1
            $this->compile();
355 1
        }
356 1
357 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
358 1
            return $this;
359 1
        }
360 1
361 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
362 1
            $this->minified;
363
364
            return $this;
365
        }
366 1
367 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
368 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
369 1
        if (!$cache->has($cacheKey)) {
370 1
            switch ($this->data['ext']) {
371 1
                case 'css':
372 1
                    $minifier = new Minify\CSS($this->data['content']);
373 1
                    break;
374 1
                case 'js':
375
                    $minifier = new Minify\JS($this->data['content']);
376 1
                    break;
377
                default:
378 1
                    throw new RuntimeException(sprintf('Not able to minify "%s"', $this->data['path']));
379
            }
380
            $this->data['path'] = preg_replace(
381
                '/\.' . $this->data['ext'] . '$/m',
382
                '.min.' . $this->data['ext'],
383
                $this->data['path']
384 1
            );
385
            $this->data['content'] = $minifier->minify();
386 1
            $this->data['size'] = \strlen($this->data['content']);
387 1
            $this->minified = true;
388
            $cache->set($cacheKey, $this->data);
389
        }
390 1
        $this->data = $cache->get($cacheKey);
391 1
392 1
        return $this;
393 1
    }
394 1
395
    /**
396 1
     * Optimizing an image.
397 1
     */
398 1
    public function optimize(string $filepath): self
399 1
    {
400 1
        if ($this->data['type'] != 'image') {
401 1
            return $this;
402 1
        }
403
404
        $quality = $this->config->get('assets.images.quality') ?? 75;
405
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
406
        $tags = ["q$quality", 'optimized'];
407
        if ($this->data['width']) {
408
            array_unshift($tags, "{$this->data['width']}x");
409
        }
410 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
411 1
        if (!$cache->has($cacheKey)) {
412 1
            $message = $this->data['path'];
413 1
            $sizeBefore = filesize($filepath);
414
            Optimizer::create($quality)->optimize($filepath);
415 1
            $sizeAfter = filesize($filepath);
416 1
            if ($sizeAfter < $sizeBefore) {
417
                $message = sprintf(
418 1
                    '%s (%s Ko -> %s Ko)',
419
                    $message,
420
                    ceil($sizeBefore / 1000),
421
                    ceil($sizeAfter / 1000)
422
                );
423
            }
424
            $this->data['content'] = Util\File::fileGetContents($filepath);
425
            $this->data['size'] = $sizeAfter;
426 1
            $cache->set($cacheKey, $this->data);
427
            $this->builder->getLogger()->debug(sprintf('Asset "%s" optimized', $message));
428 1
        }
429
        $this->data = $cache->get($cacheKey, $this->data);
430
431 1
        return $this;
432
    }
433
434 1
    /**
435
     * Resizes an image with a new $width.
436
     *
437
     * @throws RuntimeException
438 1
     */
439 1
    public function resize(int $width): self
440
    {
441 1
        if ($this->data['missing']) {
442
            throw new RuntimeException(sprintf('Not able to resize "%s": file not found', $this->data['path']));
443
        }
444
        if ($this->data['type'] != 'image') {
445 1
            throw new RuntimeException(sprintf('Not able to resize "%s": not an image', $this->data['path']));
446 1
        }
447 1
        if ($width >= $this->data['width']) {
448 1
            return $this;
449 1
        }
450
451
        $assetResized = clone $this;
452 1
        $assetResized->data['width'] = $width;
453
454
        if ($this->isImageInCdn()) {
455
            return $assetResized; // returns the asset with the new width only: CDN do the rest of the job
456
        }
457 1
458 1
        $quality = $this->config->get('assets.images.quality');
459 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
460 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
461 1
        if (!$cache->has($cacheKey)) {
462
            if ($assetResized->data['type'] !== 'image') {
463
                throw new RuntimeException(sprintf('Not able to resize "%s"', $assetResized->data['path']));
464
            }
465 1
            if (!\extension_loaded('gd')) {
466 1
                throw new RuntimeException('GD extension is required to use images resize.');
467 1
            }
468 1
469 1
            try {
470 1
                $img = ImageManager::make($assetResized->data['content_source'])->encode($assetResized->data['ext']);
471
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
472
                    $constraint->aspectRatio();
473 1
                    $constraint->upsize();
474
                });
475
            } catch (\Exception $e) {
476 1
                throw new RuntimeException(sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
477 1
            }
478 1
            $assetResized->data['path'] = '/' . Util::joinPath(
479 1
                (string) $this->config->get('assets.target'),
480
                (string) $this->config->get('assets.images.resize.dir'),
481
                (string) $width,
482
                $assetResized->data['path']
483
            );
484 1
485
            try {
486 1
                if ($assetResized->data['subtype'] == 'image/jpeg') {
487
                    $img->interlace();
488 1
                }
489
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
490
                $img->destroy();
491
                $assetResized->data['height'] = $assetResized->getHeight();
492
                $assetResized->data['size'] = \strlen($assetResized->data['content']);
493
            } catch (\Exception $e) {
494
                throw new RuntimeException(sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
495
            }
496 1
497
            $cache->set($cacheKey, $assetResized->data);
498 1
        }
499
        $assetResized->data = $cache->get($cacheKey);
500
501
        return $assetResized;
502 1
    }
503 1
504
    /**
505
     * Converts an image asset to WebP format.
506 1
     *
507 1
     * @throws RuntimeException
508 1
     */
509
    public function webp(?int $quality = null): self
510 1
    {
511
        if ($this->data['type'] !== 'image') {
512
            throw new RuntimeException(sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
513
        }
514 1
515 1
        if ($quality === null) {
516
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
517
        }
518
519
        $assetWebp = clone $this;
520
        $format = 'webp';
521
        $assetWebp['ext'] = $format;
522
523
        if ($this->isImageInCdn()) {
524
            return $assetWebp; // returns the asset with the new extension ('webp') only: CDN do the rest of the job
525
        }
526
527
        $img = ImageManager::make($assetWebp['content']);
528 1
        $assetWebp['content'] = (string) $img->encode($format, $quality);
529
        $img->destroy();
530 1
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
531 1
        $assetWebp['subtype'] = "image/$format";
532
        $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

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