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

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