Passed
Push — master ( 210c9d...28586c )
by Arnaud
06:27 queued 16s
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 1
                    $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
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
512 1
        $tags = ["q$quality"];
513 1
        if ($this->data['width']) {
514 1
            array_unshift($tags, "{$this->data['width']}x");
515
        }
516 1
        $cacheKey = $cache->createKeyFromAsset($assetWebp, $tags);
517 1
        if (!$cache->has($cacheKey)) {
518 1
            $img = ImageManager::make($assetWebp['content']);
519 1
            $assetWebp['content'] = (string) $img->encode($format, $quality);
520
            $img->destroy();
521
            $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
522
            $assetWebp['subtype'] = "image/$format";
523
            $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

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