Passed
Push — master ( f74a88...2af518 )
by Arnaud
05:27
created

Asset::__construct()   D

Complexity

Conditions 25
Paths 78

Size

Total Lines 130
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 88
CRAP Score 25.1622

Importance

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

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