Passed
Push — master ( 6ccce8...add370 )
by
unknown
08:15
created

Asset::compile()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 81
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 46
CRAP Score 15.5128

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 59
c 1
b 0
f 0
nc 13
nop 0
dl 0
loc 81
ccs 46
cts 61
cp 0.7541
crap 15.5128
rs 6.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\ConfigException;
21
use Cecil\Exception\RuntimeException;
22
use Cecil\Util;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use ScssPhp\ScssPhp\OutputStyle;
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'] == 'image/jpeg') {
148 1
                            $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 1
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
176
        }
177 1
        $this->data = $cache->get($cacheKey);
178
179
        // fingerprinting
180 1
        if ($fingerprint) {
181 1
            $this->fingerprint();
182
        }
183
        // compiling (Sass files)
184 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
185 1
            $this->compile();
186
        }
187
        // minifying (CSS and JavScript files)
188 1
        if ($minify) {
189 1
            $this->minify();
190
        }
191
        // optimizing (images files)
192 1
        if ($optimize) {
193 1
            $this->optimize = true;
194
        }
195
    }
196
197
    /**
198
     * Returns path.
199
     *
200
     * @throws RuntimeException
201
     */
202 1
    public function __toString(): string
203
    {
204
        try {
205 1
            $this->save();
206
        } catch (RuntimeException $e) {
207
            $this->builder->getLogger()->error($e->getMessage());
208
        }
209
210 1
        if ($this->isImageInCdn()) {
211
            return $this->buildImageCdnUrl();
212
        }
213
214 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
215
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
216
        }
217
218 1
        return $this->data['path'];
219
    }
220
221
    /**
222
     * Fingerprints a file.
223
     */
224 1
    public function fingerprint(): self
225
    {
226 1
        if ($this->fingerprinted) {
227 1
            return $this;
228
        }
229
230 1
        $fingerprint = hash('md5', $this->data['content_source']);
231 1
        $this->data['path'] = preg_replace(
232 1
            '/\.' . $this->data['ext'] . '$/m',
233 1
            ".$fingerprint." . $this->data['ext'],
234 1
            $this->data['path']
235 1
        );
236
237 1
        $this->fingerprinted = true;
238
239 1
        return $this;
240
    }
241
242
    /**
243
     * Compiles a SCSS.
244
     *
245
     * @throws RuntimeException
246
     */
247 1
    public function compile(): self
248
    {
249 1
        if ($this->compiled) {
250 1
            return $this;
251
        }
252
253 1
        if ($this->data['ext'] != 'scss') {
254 1
            return $this;
255
        }
256
257 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
258 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
259 1
        if (!$cache->has($cacheKey)) {
260 1
            $scssPhp = new Compiler();
261
            // import paths
262 1
            $importDir = [];
263 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
264 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
265 1
            $scssDir = (array) $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->setQuietDeps(true);
277 1
            $scssPhp->setImportPaths(array_unique($importDir));
278
            // source map
279 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
280
                $importDir = [];
281
                $assetDir = (string) $this->config->get('assets.dir');
282
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
283
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
284
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
285
                $importDir[] = \dirname($filePath);
286
                foreach ($scssDir as $dir) {
287
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
288
                }
289
                $scssPhp->setImportPaths(array_unique($importDir));
290
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
291
                $scssPhp->setSourceMapOptions([
292
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
293
                    'sourceRoot'        => '/',
294
                ]);
295
            }
296
            // output style
297 1
            $outputStyles = ['expanded', 'compressed'];
298 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
299 1
            if (!\in_array($outputStyle, $outputStyles)) {
300
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
301
            }
302 1
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
303
            // variables
304 1
            $variables = $this->config->get('assets.compile.variables');
305 1
            if (!empty($variables)) {
306 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
307 1
                $scssPhp->replaceVariables($variables);
308
            }
309
            // debug
310 1
            if ($this->builder->isDebug()) {
311 1
                $scssPhp->setQuietDeps(false);
312 1
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
313
            }
314
            // update data
315 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
316 1
            $this->data['ext'] = 'css';
317 1
            $this->data['type'] = 'text';
318 1
            $this->data['subtype'] = 'text/css';
319 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
320 1
            $this->data['size'] = \strlen($this->data['content']);
321 1
            $this->compiled = true;
322 1
            $cache->set($cacheKey, $this->data);
323 1
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
324
        }
325 1
        $this->data = $cache->get($cacheKey);
326
327 1
        return $this;
328
    }
329
330
    /**
331
     * Minifying a CSS or a JS.
332
     *
333
     * @throws RuntimeException
334
     */
335 1
    public function minify(): self
336
    {
337
        // disable minify to preserve inline source map
338 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
339
            return $this;
340
        }
341
342 1
        if ($this->minified) {
343
            return $this;
344
        }
345
346 1
        if ($this->data['ext'] == 'scss') {
347
            $this->compile();
348
        }
349
350 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
351
            return $this;
352
        }
353
354 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
355
            $this->minified;
356
357
            return $this;
358
        }
359
360 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
361 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
362 1
        if (!$cache->has($cacheKey)) {
363 1
            switch ($this->data['ext']) {
364 1
                case 'css':
365 1
                    $minifier = new Minify\CSS($this->data['content']);
366 1
                    break;
367 1
                case 'js':
368 1
                    $minifier = new Minify\JS($this->data['content']);
369 1
                    break;
370
                default:
371
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
372
            }
373 1
            $this->data['path'] = preg_replace(
374 1
                '/\.' . $this->data['ext'] . '$/m',
375 1
                '.min.' . $this->data['ext'],
376 1
                $this->data['path']
377 1
            );
378 1
            $this->data['content'] = $minifier->minify();
379 1
            $this->data['size'] = \strlen($this->data['content']);
380 1
            $this->minified = true;
381 1
            $cache->set($cacheKey, $this->data);
382 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
383
        }
384 1
        $this->data = $cache->get($cacheKey);
385
386 1
        return $this;
387
    }
388
389
    /**
390
     * Optimizing an image.
391
     */
392 1
    public function optimize(string $filepath): self
393
    {
394 1
        if ($this->data['type'] != 'image') {
395 1
            return $this;
396
        }
397
398 1
        $quality = $this->config->get('assets.images.quality');
399 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
400 1
        $tags = ["q$quality", 'optimized'];
401 1
        if ($this->data['width']) {
402 1
            array_unshift($tags, "{$this->data['width']}x");
403
        }
404 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
405 1
        if (!$cache->has($cacheKey)) {
406 1
            $message = \sprintf('Asset processed: "%s"', $this->data['path']);
407 1
            $sizeBefore = filesize($filepath);
408 1
            Optimizer::create($quality)->optimize($filepath);
0 ignored issues
show
Bug introduced by
It seems like $quality can also be of type null; however, parameter $quality of Cecil\Assets\Image\Optimizer::create() does only seem to accept integer, 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

408
            Optimizer::create(/** @scrutinizer ignore-type */ $quality)->optimize($filepath);
Loading history...
409 1
            $sizeAfter = filesize($filepath);
410 1
            if ($sizeAfter < $sizeBefore) {
411
                $message = \sprintf(
412
                    'Asset optimized: "%s" (%s Ko -> %s Ko)',
413
                    $this->data['path'],
414
                    ceil($sizeBefore / 1000),
415
                    ceil($sizeAfter / 1000)
416
                );
417
            }
418 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
419 1
            $this->data['size'] = $sizeAfter;
420 1
            $cache->set($cacheKey, $this->data);
421 1
            $this->builder->getLogger()->debug($message);
422
        }
423 1
        $this->data = $cache->get($cacheKey, $this->data);
424
425 1
        return $this;
426
    }
427
428
    /**
429
     * Resizes an image with a new $width.
430
     *
431
     * @throws RuntimeException
432
     */
433 1
    public function resize(int $width): self
434
    {
435 1
        if ($this->data['missing']) {
436
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
437
        }
438 1
        if ($this->data['type'] != 'image') {
439
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
440
        }
441 1
        if ($width >= $this->data['width']) {
442 1
            return $this;
443
        }
444
445 1
        $assetResized = clone $this;
446 1
        $assetResized->data['width'] = $width;
447
448 1
        if ($this->isImageInCdn()) {
449
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
450
        }
451
452 1
        $quality = $this->config->get('assets.images.quality');
453 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
454 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
455 1
        if (!$cache->has($cacheKey)) {
456 1
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
0 ignored issues
show
Bug introduced by
It seems like $quality can also be of type null; however, parameter $quality of Cecil\Assets\Image::resize() does only seem to accept integer, 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

456
            $assetResized->data['content'] = Image::resize($assetResized, $width, /** @scrutinizer ignore-type */ $quality);
Loading history...
457 1
            $assetResized->data['path'] = '/' . Util::joinPath(
458 1
                (string) $this->config->get('assets.target'),
459 1
                (string) $this->config->get('assets.images.resize.dir'),
460 1
                (string) $width,
461 1
                $assetResized->data['path']
462 1
            );
463 1
            $assetResized->data['height'] = $assetResized->getHeight();
464 1
            $assetResized->data['size'] = \strlen($assetResized->data['content']);
465
466 1
            $cache->set($cacheKey, $assetResized->data);
467 1
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
468
        }
469 1
        $assetResized->data = $cache->get($cacheKey);
470
471 1
        return $assetResized;
472
    }
473
474
    /**
475
     * Converts an image asset to $format format.
476
     *
477
     * @throws RuntimeException
478
     */
479 1
    public function convert(string $format, ?int $quality = null): self
480
    {
481 1
        if ($this->data['type'] != 'image') {
482
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
483
        }
484
485 1
        if ($quality === null) {
486 1
            $quality = (int) $this->config->get('assets.images.quality');
487
        }
488
489 1
        $asset = clone $this;
490 1
        $asset['ext'] = $format;
491
492 1
        if ($this->isImageInCdn()) {
493
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
494
        }
495
496 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
497 1
        $tags = ["q$quality"];
498 1
        if ($this->data['width']) {
499 1
            array_unshift($tags, "{$this->data['width']}x");
500
        }
501 1
        $cacheKey = $cache->createKeyFromAsset($asset, $tags);
502 1
        if (!$cache->has($cacheKey)) {
503 1
            $asset->data['content'] = Image::convert($asset, $format, $quality);
504
            $asset->data['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
505
            $asset->data['subtype'] = "image/$format";
506
            $asset->data['size'] = \strlen($asset->data['content']);
507
            $cache->set($cacheKey, $asset->data);
508
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
509
        }
510
        $asset->data = $cache->get($cacheKey);
511
512
        return $asset;
513
    }
514
515
    /**
516
     * Converts an image asset to WebP format.
517
     *
518
     * @throws RuntimeException
519
     */
520
    public function webp(?int $quality = null): self
521
    {
522
        return $this->convert('webp', $quality);
523
    }
524
525
    /**
526
     * Converts an image asset to AVIF format.
527
     *
528
     * @throws RuntimeException
529
     */
530 1
    public function avif(?int $quality = null): self
531
    {
532 1
        return $this->convert('avif', $quality);
533
    }
534
535
    /**
536
     * Implements \ArrayAccess.
537
     */
538 1
    #[\ReturnTypeWillChange]
539
    public function offsetSet($offset, $value): void
540
    {
541 1
        if (!\is_null($offset)) {
542 1
            $this->data[$offset] = $value;
543
        }
544
    }
545
546
    /**
547
     * Implements \ArrayAccess.
548
     */
549 1
    #[\ReturnTypeWillChange]
550
    public function offsetExists($offset): bool
551
    {
552 1
        return isset($this->data[$offset]);
553
    }
554
555
    /**
556
     * Implements \ArrayAccess.
557
     */
558
    #[\ReturnTypeWillChange]
559
    public function offsetUnset($offset): void
560
    {
561
        unset($this->data[$offset]);
562
    }
563
564
    /**
565
     * Implements \ArrayAccess.
566
     */
567 1
    #[\ReturnTypeWillChange]
568
    public function offsetGet($offset)
569
    {
570 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
571
    }
572
573
    /**
574
     * Hashing content of an asset with the specified algo, sha384 by default.
575
     * Used for SRI (Subresource Integrity).
576
     *
577
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
578
     */
579 1
    public function getIntegrity(string $algo = 'sha384'): string
580
    {
581 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
582
    }
583
584
    /**
585
     * Returns MP3 file infos.
586
     *
587
     * @see https://github.com/wapmorgan/Mp3Info
588
     */
589 1
    public function getAudio(): Mp3Info
590
    {
591 1
        if ($this->data['type'] !== 'audio') {
592
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
593
        }
594
595 1
        return new Mp3Info($this->data['file']);
596
    }
597
598
    /**
599
     * Returns MP4 file infos.
600
     *
601
     * @see https://github.com/clwu88/php-read-mp4info
602
     */
603 1
    public function getVideo(): array
604
    {
605 1
        if ($this->data['type'] !== 'video') {
606
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
607
        }
608
609 1
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
610
    }
611
612
    /**
613
     * Returns the Data URL (encoded in Base64).
614
     *
615
     * @throws RuntimeException
616
     */
617 1
    public function dataurl(): string
618
    {
619 1
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
620 1
            return Image::getDataUrl($this, $this->config->get('assets.images.quality'));
0 ignored issues
show
Bug introduced by
It seems like $this->config->get('assets.images.quality') can also be of type null; however, parameter $quality of Cecil\Assets\Image::getDataUrl() does only seem to accept integer, 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

620
            return Image::getDataUrl($this, /** @scrutinizer ignore-type */ $this->config->get('assets.images.quality'));
Loading history...
621
        }
622
623 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
624
    }
625
626
    /**
627
     * Saves file.
628
     * Note: a file from `static/` with the same name will NOT be overridden.
629
     *
630
     * @throws RuntimeException
631
     */
632 1
    public function save(): void
633
    {
634 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
635 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
636
            try {
637 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
638 1
                if ($this->optimize) {
639 1
                    $this->optimize($filepath);
640
                }
641
            } catch (\Symfony\Component\Filesystem\Exception\IOException) {
642
                if (!$this->ignore_missing) {
643
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s" to output.', $filepath));
644
                }
645
            }
646
        }
647
    }
648
649
    /**
650
     * Is Asset is an image in CDN.
651
     *
652
     * @return bool
653
     */
654 1
    public function isImageInCdn()
655
    {
656 1
        if ($this->data['type'] != 'image' || (bool) $this->config->get('assets.images.cdn.enabled') !== true || (Image::isSVG($this) && (bool) $this->config->get('assets.images.cdn.svg') !== true)) {
657 1
            return false;
658
        }
659
        // remote image?
660
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
661
            return false;
662
        }
663
664
        return true;
665
    }
666
667
    /**
668
     * Load file data.
669
     *
670
     * @throws RuntimeException
671
     *
672
     * @return string[]
673
     */
674 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
675
    {
676 1
        $file = [
677 1
            'url' => null,
678 1
        ];
679
680
        try {
681 1
            $filePath = $this->findFile($path, $remote_fallback);
682 1
        } catch (RuntimeException $e) {
683 1
            if ($ignore_missing) {
684 1
                $file['path'] = $path;
685 1
                $file['missing'] = true;
686
687 1
                return $file;
688
            }
689
690
            throw new RuntimeException(\sprintf('Can\'t load asset file "%s" (%s).', $path, $e->getMessage()));
691
        }
692
693 1
        if (Util\Url::isUrl($path)) {
694 1
            $file['url'] = $path;
695 1
            $path = Util::joinPath(
696 1
                (string) $this->config->get('assets.target'),
697 1
                Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath())
698 1
            );
699
            // remote_fallback in assets/ ont in cache/assets/remote/
700 1
            if (substr(Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath()), 0, 2) == '..') {
701
                $path = Util::joinPath(
702
                    (string) $this->config->get('assets.target'),
703
                    Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsPath())
704
                );
705
            }
706 1
            $force_slash = true;
707
        }
708 1
        if ($force_slash) {
709 1
            $path = '/' . ltrim($path, '/');
710
        }
711
712 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
713 1
        $content = Util\File::fileGetContents($filePath);
714
715 1
        $file['filepath'] = $filePath;
716 1
        $file['path'] = $path;
717 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
718 1
        $file['type'] = $type;
719 1
        $file['subtype'] = $subtype;
720 1
        $file['size'] = filesize($filePath);
721 1
        $file['content'] = $content;
722 1
        $file['missing'] = false;
723
724 1
        return $file;
725
    }
726
727
    /**
728
     * Try to find the file:
729
     *   1. remote (if $path is a valid URL)
730
     *   2. in static/
731
     *   3. in themes/<theme>/static/
732
     * Returns local file path or throw an exception.
733
     *
734
     * @throws RuntimeException
735
     */
736 1
    private function findFile(string $path, ?string $remote_fallback = null): string
737
    {
738
        // in case of remote file: save it and returns cached file path
739 1
        if (Util\Url::isUrl($path)) {
740 1
            $url = $path;
741 1
            $urlHost = parse_url($path, PHP_URL_HOST);
742 1
            $urlPath = parse_url($path, PHP_URL_PATH);
743 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
744 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
745
            // Google Fonts hack
746 1
            if (Util\Str::endsWith($urlPath, '/css') || Util\Str::endsWith($urlPath, '/css2')) {
747 1
                $extension = 'css';
748
            }
749 1
            $relativePath = Page::slugify(\sprintf(
750 1
                '%s%s%s%s',
751 1
                $urlHost,
752 1
                $this->sanitize($urlPath),
753 1
                $urlQuery ? "-$urlQuery" : '',
754 1
                $urlQuery && $extension ? ".$extension" : ''
755 1
            ));
756 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
757
            // not already in cache
758 1
            if (!file_exists($filePath)) {
759
                try {
760 1
                    if (!Util\Url::isRemoteFileExists($url)) {
761
                        throw new RuntimeException(\sprintf('File "%s" doesn\'t exists', $url));
762
                    }
763 1
                    if (false === $content = Util\File::fileGetContents($url, true)) {
764
                        throw new RuntimeException(\sprintf('Can\'t get content of file "%s".', $url));
765
                    }
766 1
                    if (\strlen($content) <= 1) {
767 1
                        throw new RuntimeException(\sprintf('File "%s" is empty.', $url));
768
                    }
769
                } catch (RuntimeException $e) {
770
                    // is there a fallback in assets/
771
                    if ($remote_fallback) {
772
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
773
                        if (Util\File::getFS()->exists($filePath)) {
774
                            return $filePath;
775
                        }
776
                        throw new RuntimeException(\sprintf('Fallback file "%s" doesn\'t exists.', $filePath));
777
                    }
778
779
                    throw new RuntimeException($e->getMessage());
780
                }
781 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
782
                    throw new RuntimeException(\sprintf('Can\'t get content of "%s"', $url));
783
                }
784 1
                if (\strlen($content) <= 1) {
785
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
786
                }
787
                // put file in cache
788 1
                Util\File::getFS()->dumpFile($filePath, $content);
789
            }
790
791 1
            return $filePath;
792
        }
793
794
        // checks in assets/
795 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
796 1
        if (Util\File::getFS()->exists($filePath)) {
797 1
            return $filePath;
798
        }
799
800
        // checks in each themes/<theme>/assets/
801 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
802 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
803 1
            if (Util\File::getFS()->exists($filePath)) {
804 1
                return $filePath;
805
            }
806
        }
807
808
        // checks in static/
809 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
810 1
        if (Util\File::getFS()->exists($filePath)) {
811 1
            return $filePath;
812
        }
813
814
        // checks in each themes/<theme>/static/
815 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
816 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
817 1
            if (Util\File::getFS()->exists($filePath)) {
818 1
                return $filePath;
819
            }
820
        }
821
822 1
        throw new RuntimeException(\sprintf('Can\'t find file "%s".', $path));
823
    }
824
825
    /**
826
     * Returns the width of an image/SVG.
827
     *
828
     * @throws RuntimeException
829
     */
830 1
    private function getWidth(): int
831
    {
832 1
        if ($this->data['type'] != 'image') {
833
            return 0;
834
        }
835 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
836 1
            return (int) $svg->width;
837
        }
838 1
        if (false === $size = $this->getImageSize()) {
839
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
840
        }
841
842 1
        return $size[0];
843
    }
844
845
    /**
846
     * Returns the height of an image/SVG.
847
     *
848
     * @throws RuntimeException
849
     */
850 1
    private function getHeight(): int
851
    {
852 1
        if ($this->data['type'] != 'image') {
853
            return 0;
854
        }
855 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
856 1
            return (int) $svg->height;
857
        }
858 1
        if (false === $size = $this->getImageSize()) {
859
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
860
        }
861
862 1
        return $size[1];
863
    }
864
865
    /**
866
     * Returns image size informations.
867
     *
868
     * @see https://www.php.net/manual/function.getimagesize.php
869
     *
870
     * @return array|false
871
     */
872 1
    private function getImageSize()
873
    {
874 1
        if (!$this->data['type'] == 'image') {
875
            return false;
876
        }
877
878
        try {
879 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
880 1
                return false;
881
            }
882
        } catch (\Exception $e) {
883
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
884
        }
885
886 1
        return $size;
887
    }
888
889
    /**
890
     * Replaces some characters by '_'.
891
     */
892 1
    private function sanitize(string $string): string
893
    {
894 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
895
    }
896
897
    /**
898
     * Builds CDN image URL.
899
     */
900
    private function buildImageCdnUrl(): string
901
    {
902
        return str_replace(
903
            [
904
                '%account%',
905
                '%image_url%',
906
                '%width%',
907
                '%quality%',
908
                '%format%',
909
            ],
910
            [
911
                $this->config->get('assets.images.cdn.account'),
912
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical')]), '/'),
913
                $this->data['width'],
914
                $this->config->get('assets.images.quality'),
915
                $this->data['ext'],
916
            ],
917
            (string) $this->config->get('assets.images.cdn.url')
918
        );
919
    }
920
}
921