Passed
Push — master ( 4a60f8...5e68ca )
by Arnaud
05:15
created

Asset::minify()   C

Complexity

Conditions 12
Paths 14

Size

Total Lines 52
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 13.152

Importance

Changes 0
Metric Value
cc 12
eloc 34
c 0
b 0
f 0
nc 14
nop 0
dl 0
loc 52
ccs 28
cts 35
cp 0.8
crap 13.152
rs 6.9666

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\Url;
23
use Cecil\Util;
24
use MatthiasMullie\Minify;
25
use ScssPhp\ScssPhp\Compiler;
26
use ScssPhp\ScssPhp\OutputStyle;
27
use wapmorgan\Mp3Info\Mp3Info;
28
29
class Asset implements \ArrayAccess
30
{
31
    /** @var Builder */
32
    protected $builder;
33
34
    /** @var Config */
35
    protected $config;
36
37
    /** @var array */
38
    protected $data = [];
39
40
    /** @var bool */
41
    protected $fingerprinted = false;
42
43
    /** @var bool */
44
    protected $compiled = false;
45
46
    /** @var bool */
47
    protected $minified = false;
48
49
    /** @var bool */
50
    protected $ignore_missing = false;
51
52
    /**
53
     * Creates an Asset from a file path, an array of files path or an URL.
54
     *
55
     * @param Builder      $builder
56
     * @param string|array $paths
57
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
58
     *
59
     * @throws RuntimeException
60
     */
61 1
    public function __construct(Builder $builder, string|array $paths, array|null $options = null)
62
    {
63 1
        $this->builder = $builder;
64 1
        $this->config = $builder->getConfig();
65 1
        $paths = \is_array($paths) ? $paths : [$paths];
66 1
        array_walk($paths, function ($path) {
67 1
            if (!\is_string($path)) {
68
                throw new RuntimeException(\sprintf('The path of an asset must be a string ("%s" given).', \gettype($path)));
69
            }
70 1
            if (empty($path)) {
71
                throw new RuntimeException('The path of an asset can\'t be empty.');
72
            }
73 1
            if (substr($path, 0, 2) == '..') {
74
                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));
75
            }
76 1
        });
77 1
        $this->data = [
78 1
            'file'           => '',    // absolute file path
79 1
            'files'          => [],    // bundle: array of files path
80 1
            'filename'       => '',    // bundle: filename
81 1
            'path'           => '',    // public path to the file
82 1
            'url'            => null,  // URL if it's a remote file
83 1
            'missing'        => false, // if file not found but missing allowed: 'missing' is true
84 1
            'ext'            => '',    // file extension
85 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
86 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
87 1
            'size'           => 0,     // file size (in bytes)
88 1
            'width'          => 0,     // image width (in pixels)
89 1
            'height'         => 0,     // image height (in pixels)
90 1
            'exif'           => [],    // exif data
91 1
            'content'        => '',    // file content
92 1
        ];
93
94
        // handles options
95 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
96 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
97 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
98 1
        $filename = '';
99 1
        $ignore_missing = false;
100 1
        $remote_fallback = null;
101 1
        $force_slash = true;
102 1
        extract(\is_array($options) ? $options : [], EXTR_IF_EXISTS);
103
104
        // fill data array with file(s) informations
105 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
106 1
        $cacheKey = \sprintf('%s__%s', $filename ?: implode('_', $paths), $this->builder->getVersion());
107 1
        if (!$cache->has($cacheKey)) {
108 1
            $pathsCount = \count($paths);
109 1
            $file = [];
110 1
            for ($i = 0; $i < $pathsCount; $i++) {
111
                // loads file(s)
112 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
113
                // bundle: same type only
114 1
                if ($i > 0) {
115 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
116
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
117
                    }
118
                }
119
                // missing allowed = empty path
120 1
                if ($file[$i]['missing']) {
121 1
                    $this->data['missing'] = true;
122 1
                    $this->data['path'] = $file[$i]['path'];
123
124 1
                    continue;
125
                }
126
                // set data
127 1
                $this->data['content'] .= $file[$i]['content'];
128 1
                $this->data['size'] += $file[$i]['size'];
129 1
                if ($i == 0) {
130 1
                    $this->data['file'] = $file[$i]['filepath'];
131 1
                    $this->data['filename'] = $file[$i]['path'];
132 1
                    $this->data['path'] = $file[$i]['path'];
133 1
                    $this->data['url'] = $file[$i]['url'];
134 1
                    $this->data['ext'] = $file[$i]['ext'];
135 1
                    $this->data['type'] = $file[$i]['type'];
136 1
                    $this->data['subtype'] = $file[$i]['subtype'];
137
                    // image: width, height and exif
138 1
                    if ($this->data['type'] == 'image') {
139 1
                        $this->data['width'] = $this->getWidth();
140 1
                        $this->data['height'] = $this->getHeight();
141 1
                        if ($this->data['subtype'] == 'image/jpeg') {
142 1
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
143
                        }
144
                    }
145
                    // bundle default filename
146 1
                    if ($pathsCount > 1 && empty($filename)) {
147 1
                        switch ($this->data['ext']) {
148 1
                            case 'scss':
149 1
                            case 'css':
150 1
                                $filename = '/styles.css';
151 1
                                break;
152 1
                            case 'js':
153 1
                                $filename = '/scripts.js';
154 1
                                break;
155
                            default:
156
                                throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
157
                        }
158
                    }
159
                    // bundle filename and path
160 1
                    if (!empty($filename)) {
161 1
                        $this->data['filename'] = $filename;
162 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
163
                    }
164
                }
165
                // bundle files path
166 1
                $this->data['files'][] = $file[$i]['filepath'];
167
            }
168
            // fingerprinting
169 1
            if ($fingerprint) {
170
                $this->fingerprint();
171
            }
172 1
            $cache->set($cacheKey, $this->data);
173 1
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
174
            // optimizing images files
175 1
            if ($optimize && $this->data['type'] == 'image') {
176 1
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
177
            }
178
        }
179 1
        $this->data = $cache->get($cacheKey);
180
        // compiling (Sass files)
181 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
182 1
            $this->compile();
183
        }
184
        // minifying (CSS and JavScript files)
185 1
        if ($minify) {
186 1
            $this->minify();
187
        }
188
    }
189
190
    /**
191
     * Returns path.
192
     *
193
     * @throws RuntimeException
194
     */
195 1
    public function __toString(): string
196
    {
197 1
        $this->save();
198
199 1
        if ($this->isImageInCdn()) {
200
            return $this->buildImageCdnUrl();
201
        }
202
203 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
204
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
205
        }
206
207 1
        return $this->data['path'];
208
    }
209
210
    /**
211
     * Fingerprints a file.
212
     */
213 1
    public function fingerprint(): self
214
    {
215 1
        if ($this->fingerprinted) {
216
            return $this;
217
        }
218
219 1
        $fingerprint = hash('md5', $this->data['content']);
220 1
        $this->data['path'] = preg_replace(
221 1
            '/\.' . $this->data['ext'] . '$/m',
222 1
            ".$fingerprint." . $this->data['ext'],
223 1
            $this->data['path']
224 1
        );
225
226 1
        $this->fingerprinted = true;
227
228 1
        return $this;
229
    }
230
231
    /**
232
     * Compiles a SCSS.
233
     *
234
     * @throws RuntimeException
235
     */
236 1
    public function compile(): self
237
    {
238 1
        if ($this->compiled) {
239 1
            return $this;
240
        }
241
242 1
        if ($this->data['ext'] != 'scss') {
243 1
            return $this;
244
        }
245
246 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
247 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
248 1
        if (!$cache->has($cacheKey)) {
249 1
            $scssPhp = new Compiler();
250
            // import paths
251 1
            $importDir = [];
252 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
253 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
254 1
            $scssDir = (array) $this->config->get('assets.compile.import');
255 1
            $themes = $this->config->getTheme() ?? [];
256 1
            foreach ($scssDir as $dir) {
257 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
258 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
259 1
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
260 1
                foreach ($themes as $theme) {
261 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
262 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
263
                }
264
            }
265 1
            $scssPhp->setQuietDeps(true);
266 1
            $scssPhp->setImportPaths(array_unique($importDir));
267
            // source map
268 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
269
                $importDir = [];
270
                $assetDir = (string) $this->config->get('assets.dir');
271
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
272
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
273
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
274
                $importDir[] = \dirname($filePath);
275
                foreach ($scssDir as $dir) {
276
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
277
                }
278
                $scssPhp->setImportPaths(array_unique($importDir));
279
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
280
                $scssPhp->setSourceMapOptions([
281
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
282
                    'sourceRoot'        => '/',
283
                ]);
284
            }
285
            // output style
286 1
            $outputStyles = ['expanded', 'compressed'];
287 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
288 1
            if (!\in_array($outputStyle, $outputStyles)) {
289
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
290
            }
291 1
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
292
            // variables
293 1
            $variables = $this->config->get('assets.compile.variables');
294 1
            if (!empty($variables)) {
295 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
296 1
                $scssPhp->replaceVariables($variables);
297
            }
298
            // debug
299 1
            if ($this->builder->isDebug()) {
300 1
                $scssPhp->setQuietDeps(false);
301 1
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
302
            }
303
            // update data
304 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
305 1
            $this->data['ext'] = 'css';
306 1
            $this->data['type'] = 'text';
307 1
            $this->data['subtype'] = 'text/css';
308 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
309 1
            $this->data['size'] = \strlen($this->data['content']);
310 1
            $this->compiled = true;
311 1
            $cache->set($cacheKey, $this->data);
312 1
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
313
        }
314 1
        $this->data = $cache->get($cacheKey);
315
316 1
        return $this;
317
    }
318
319
    /**
320
     * Minifying a CSS or a JS.
321
     *
322
     * @throws RuntimeException
323
     */
324 1
    public function minify(): self
325
    {
326
        // disable minify to preserve inline source map
327 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
328
            return $this;
329
        }
330
331 1
        if ($this->minified) {
332
            return $this;
333
        }
334
335 1
        if ($this->data['ext'] == 'scss') {
336
            $this->compile();
337
        }
338
339 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
340
            return $this;
341
        }
342
343 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
344
            $this->minified = true;
345
346
            return $this;
347
        }
348
349 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
350 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
351 1
        if (!$cache->has($cacheKey)) {
352 1
            switch ($this->data['ext']) {
353 1
                case 'css':
354 1
                    $minifier = new Minify\CSS($this->data['content']);
355 1
                    break;
356 1
                case 'js':
357 1
                    $minifier = new Minify\JS($this->data['content']);
358 1
                    break;
359
                default:
360
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
361
            }
362 1
            $this->data['path'] = preg_replace(
363 1
                '/\.' . $this->data['ext'] . '$/m',
364 1
                '.min.' . $this->data['ext'],
365 1
                $this->data['path']
366 1
            );
367 1
            $this->data['content'] = $minifier->minify();
368 1
            $this->data['size'] = \strlen($this->data['content']);
369 1
            $this->minified = true;
370 1
            $cache->set($cacheKey, $this->data);
371 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
372
        }
373 1
        $this->data = $cache->get($cacheKey);
374
375 1
        return $this;
376
    }
377
378
    /**
379
     * Optimizing $filepath image.
380
     * Returns the new file size.
381
     */
382 1
    public function optimize(string $filepath, string $path): int
383
    {
384 1
        $quality = $this->config->get('assets.images.quality');
385 1
        $message = \sprintf('Asset processed: "%s"', $path);
386 1
        $sizeBefore = filesize($filepath);
387 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

387
        Optimizer::create(/** @scrutinizer ignore-type */ $quality)->optimize($filepath);
Loading history...
388 1
        $sizeAfter = filesize($filepath);
389 1
        if ($sizeAfter < $sizeBefore) {
390
            $message = \sprintf(
391
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
392
                $path,
393
                ceil($sizeBefore / 1000),
394
                ceil($sizeAfter / 1000)
395
            );
396
        }
397 1
        $this->builder->getLogger()->debug($message);
398
399 1
        return $sizeAfter;
400
    }
401
402
    /**
403
     * Resizes an image with a new $width.
404
     *
405
     * @throws RuntimeException
406
     */
407 1
    public function resize(int $width): self
408
    {
409 1
        if ($this->data['missing']) {
410
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
411
        }
412 1
        if ($this->data['type'] != 'image') {
413
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
414
        }
415 1
        if ($width >= $this->data['width']) {
416 1
            return $this;
417
        }
418
419 1
        $assetResized = clone $this;
420 1
        $assetResized->data['width'] = $width;
421
422 1
        if ($this->isImageInCdn()) {
423
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
424
        }
425
426 1
        $quality = $this->config->get('assets.images.quality');
427 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
428 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
429 1
        if (!$cache->has($cacheKey)) {
430 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

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

594
            return Image::getDataUrl($this, /** @scrutinizer ignore-type */ $this->config->get('assets.images.quality'));
Loading history...
595
        }
596
597 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
598
    }
599
600
    /**
601
     * Adds asset path to the list of assets to save.
602
     *
603
     * @throws RuntimeException
604
     */
605 1
    public function save(): void
606
    {
607 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
608 1
        if (Util\File::getFS()->exists($cache->getContentFilePathname($this->data['path']))) {
609 1
            $this->builder->addAsset($this->data['path']);
610
        }
611
    }
612
613
    /**
614
     * Is Asset is an image in CDN.
615
     *
616
     * @return bool
617
     */
618 1
    public function isImageInCdn()
619
    {
620 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)) {
621 1
            return false;
622
        }
623
        // remote image?
624
        if ($this->data['url'] !== null && (bool) $this->config->get('assets.images.cdn.remote') !== true) {
625
            return false;
626
        }
627
628
        return true;
629
    }
630
631
    /**
632
     * Load file data and store theme in $file array.
633
     *
634
     * @throws RuntimeException
635
     *
636
     * @return string[]
637
     */
638 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
639
    {
640 1
        $file = [
641 1
            'url'      => null,
642 1
            'filepath' => null,
643 1
            'path'     => null,
644 1
            'ext'      => null,
645 1
            'type'     => null,
646 1
            'subtype'  => null,
647 1
            'size'     => null,
648 1
            'content'  => null,
649 1
            'missing'  => false,
650 1
        ];
651
652
        // try to find file locally and returns the file path
653
        try {
654 1
            $filePath = $this->locateFile($path, $remote_fallback);
655 1
        } catch (RuntimeException $e) {
656 1
            if ($ignore_missing) {
657 1
                $file['path'] = $path;
658 1
                $file['missing'] = true;
659
660 1
                return $file;
661
            }
662
663
            throw new RuntimeException(\sprintf('Can\'t load asset file "%s" (%s).', $path, $e->getMessage()));
664
        }
665
666
        // in case of an URL, update $path
667 1
        if (Util\Url::isUrl($path)) {
668 1
            $file['url'] = $path;
669 1
            $path = Util::joinPath(
670 1
                (string) $this->config->get('assets.target'),
671 1
                Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsRemotePath())
672 1
            );
673
            // trick: the `remote_fallback` file is in assets/ dir (not in cache/assets/remote/
674 1
            if (substr(Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsRemotePath()), 0, 2) == '..') {
675
                $path = Util::joinPath(
676
                    (string) $this->config->get('assets.target'),
677
                    Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsPath())
678
                );
679
            }
680 1
            $force_slash = true;
681
        }
682
683
        // force leading slash?
684 1
        if ($force_slash) {
685 1
            $path = '/' . ltrim($path, '/');
686
        }
687
688
        // get content and content type
689 1
        $content = Util\File::fileGetContents($filePath);
690 1
        list($type, $subtype) = Util\File::getMediaType($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 locate the file:
706
     *   1. remotely (if $path is a valid URL)
707
     *   2. in static|assets/
708
     *   3. in themes/<theme>/static|assets/
709
     * Returns local file path or throw an exception.
710
     *
711
     * @return string local file path
712
     *
713
     * @throws RuntimeException
714
     */
715 1
    private function locateFile(string $path, ?string $remote_fallback = null): string
716
    {
717
        // in case of a remote file: save it locally and returns its path
718 1
        if (Util\Url::isUrl($path)) {
719 1
            $url = $path;
720
            // create relative path
721 1
            $urlHost = parse_url($path, PHP_URL_HOST);
722 1
            $urlPath = parse_url($path, PHP_URL_PATH);
723 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
724 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
725
            // Google Fonts hack
726 1
            if (Util\Str::endsWith($urlPath, '/css') || Util\Str::endsWith($urlPath, '/css2')) {
727 1
                $extension = 'css';
728
            }
729 1
            $relativePath = Page::slugify(\sprintf(
730 1
                '%s%s%s%s',
731 1
                $urlHost,
732 1
                $this->sanitize($urlPath),
733 1
                $urlQuery ? "-$urlQuery" : '',
734 1
                $urlQuery && $extension ? ".$extension" : ''
735 1
            ));
736 1
            $filePath = Util::joinFile($this->config->getAssetsRemotePath(), $relativePath);
737
            // save file
738 1
            if (!file_exists($filePath)) {
739
                try {
740
                    // get content
741 1
                    if (!Util\Url::isRemoteFileExists($url)) {
742
                        throw new RuntimeException(\sprintf('File "%s" doesn\'t exists', $url));
743
                    }
744 1
                    if (false === $content = Util\File::fileGetContents($url, true)) {
745
                        throw new RuntimeException(\sprintf('Can\'t get content of file "%s".', $url));
746
                    }
747 1
                    if (\strlen($content) <= 1) {
748 1
                        throw new RuntimeException(\sprintf('File "%s" is empty.', $url));
749
                    }
750
                } catch (RuntimeException $e) {
751
                    // if there is a fallback in assets/ returns it
752
                    if ($remote_fallback) {
753
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
754
                        if (Util\File::getFS()->exists($filePath)) {
755
                            return $filePath;
756
                        }
757
758
                        throw new RuntimeException(\sprintf('Fallback file "%s" doesn\'t exists.', $filePath));
759
                    }
760
761
                    throw new RuntimeException($e->getMessage());
762
                }
763 1
                Util\File::getFS()->dumpFile($filePath, $content);
764
            }
765
766 1
            return $filePath;
767
        }
768
769
        // checks in assets/
770 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
771 1
        if (Util\File::getFS()->exists($filePath)) {
772 1
            return $filePath;
773
        }
774
775
        // checks in each themes/<theme>/assets/
776 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
777 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
778 1
            if (Util\File::getFS()->exists($filePath)) {
779 1
                return $filePath;
780
            }
781
        }
782
783
        // checks in static/
784 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
785 1
        if (Util\File::getFS()->exists($filePath)) {
786 1
            return $filePath;
787
        }
788
789
        // checks in each themes/<theme>/static/
790 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
791 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
792 1
            if (Util\File::getFS()->exists($filePath)) {
793 1
                return $filePath;
794
            }
795
        }
796
797 1
        throw new RuntimeException(\sprintf('Can\'t find file "%s".', $path));
798
    }
799
800
    /**
801
     * Returns the width of an image/SVG.
802
     *
803
     * @throws RuntimeException
804
     */
805 1
    private function getWidth(): int
806
    {
807 1
        if ($this->data['type'] != 'image') {
808
            return 0;
809
        }
810 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
811 1
            return (int) $svg->width;
812
        }
813 1
        if (false === $size = $this->getImageSize()) {
814
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
815
        }
816
817 1
        return $size[0];
818
    }
819
820
    /**
821
     * Returns the height of an image/SVG.
822
     *
823
     * @throws RuntimeException
824
     */
825 1
    private function getHeight(): int
826
    {
827 1
        if ($this->data['type'] != 'image') {
828
            return 0;
829
        }
830 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
831 1
            return (int) $svg->height;
832
        }
833 1
        if (false === $size = $this->getImageSize()) {
834
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
835
        }
836
837 1
        return $size[1];
838
    }
839
840
    /**
841
     * Returns image size informations.
842
     *
843
     * @see https://www.php.net/manual/function.getimagesize.php
844
     *
845
     * @return array|false
846
     */
847 1
    private function getImageSize()
848
    {
849 1
        if (!$this->data['type'] == 'image') {
850
            return false;
851
        }
852
853
        try {
854 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
855 1
                return false;
856
            }
857
        } catch (\Exception $e) {
858
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path'], $e->getMessage()));
859
        }
860
861 1
        return $size;
862
    }
863
864
    /**
865
     * Replaces some characters by '_'.
866
     */
867 1
    private function sanitize(string $string): string
868
    {
869 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
870
    }
871
872
    /**
873
     * Builds CDN image URL.
874
     */
875
    private function buildImageCdnUrl(): string
876
    {
877
        return str_replace(
878
            [
879
                '%account%',
880
                '%image_url%',
881
                '%width%',
882
                '%quality%',
883
                '%format%',
884
            ],
885
            [
886
                $this->config->get('assets.images.cdn.account'),
887
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical')]), '/'),
888
                $this->data['width'],
889
                $this->config->get('assets.images.quality'),
890
                $this->data['ext'],
891
            ],
892
            (string) $this->config->get('assets.images.cdn.url')
893
        );
894
    }
895
}
896