Passed
Push — master ( 9ff4fa...fbf986 )
by
unknown
06:59
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\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
            $assetResized->data['height'] = round($this->data['height'] / ($this->data['width'] / $width));
424
425
            return $assetResized; // returns asset with the new dimensions only: CDN do the rest of the job
426
        }
427
428 1
        $quality = $this->config->get('assets.images.quality');
429 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
430 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
431 1
        if (!$cache->has($cacheKey)) {
432 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

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

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