Passed
Push — configuration ( fedd0e...6e95de )
by Arnaud
09:38 queued 05:27
created

Asset::compile()   C

Complexity

Conditions 13
Paths 13

Size

Total Lines 81
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 16.0813

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 42
cts 57
cp 0.7368
crap 16.0813
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
    /**
50
     * Creates an Asset from a file path, an array of files path or an URL.
51
     * Options:
52
     * [
53
     *     'fingerprint' => <bool>,
54
     *     'minify' => <bool>,
55
     *     'optimize' => <bool>,
56
     *     'filename' => <string>,
57
     *     'ignore_missing' => <bool>,
58
     *     'remote_fallback' => <string>,
59
     *     'force_slash' => <bool>
60
     * ]
61 1
     *
62
     * @param Builder      $builder
63 1
     * @param string|array $paths
64 1
     * @param array|null   $options
65 1
     *
66 1
     * @throws RuntimeException
67 1
     */
68
    public function __construct(Builder $builder, string|array $paths, array|null $options = null)
69
    {
70 1
        $this->builder = $builder;
71
        $this->config = $builder->getConfig();
72
        $paths = \is_array($paths) ? $paths : [$paths];
73 1
        array_walk($paths, function ($path) {
74
            if (!\is_string($path)) {
75
                throw new RuntimeException(\sprintf('The path of an asset must be a string ("%s" given).', \gettype($path)));
76 1
            }
77 1
            if (empty($path)) {
78 1
                throw new RuntimeException('The path of an asset can\'t be empty.');
79 1
            }
80 1
            if (substr($path, 0, 2) == '..') {
81 1
                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));
82 1
            }
83 1
        });
84 1
        $this->data = [
85 1
            'file'     => '',    // absolute file path
86 1
            'files'    => [],    // array of files path
87 1
            'filename' => '',    // file name
88 1
            'path'     => '',    // path to the file
89 1
            'url'      => null,  // URL if it's a remote file
90 1
            'missing'  => false, // if file not found but missing allowed: 'missing' is true
91 1
            'ext'      => '',    // file extension
92 1
            'type'     => '',    // file type (e.g.: image, audio, video, etc.)
93
            'subtype'  => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
94
            'size'     => 0,     // file size (in bytes)
95 1
            'width'    => 0,     // image width (in pixels)
96 1
            'height'   => 0,     // image height (in pixels)
97 1
            'exif'     => [],    // image exif data
98 1
            'content'  => '',    // file content
99 1
        ];
100 1
101 1
        // handles options
102 1
        $fingerprint = $this->config->isEnabled('assets.fingerprint');
103
        $minify = $this->config->isEnabled('assets.minify');
104
        $optimize = $this->config->isEnabled('assets.images.optimize');
0 ignored issues
show
Unused Code introduced by
The assignment to $optimize is dead and can be removed.
Loading history...
105 1
        $filename = '';
106 1
        $ignore_missing = false;
107 1
        $remote_fallback = null;
108 1
        $force_slash = true;
109 1
        extract(\is_array($options) ? $options : [], EXTR_IF_EXISTS);
110 1
111
        // locate file(s) and get content
112 1
        $pathsCount = \count($paths);
113
        for ($i = 0; $i < $pathsCount; $i++) {
114 1
            try {
115 1
                $filePath = $this->locateFile($paths[$i], $remote_fallback);
116
                $type = Util\File::getMediaType($filePath)[0];
117
                if ($i > 0) { // bundle
118
                    if ($type != $this->data['type']) {
119
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $type, $this->data['type']));
120 1
                    }
121 1
                    $this->data['size'] += filesize($filePath);
122 1
                    $this->data['content'] .= Util\File::fileGetContents($filePath);
123
                    break;
124 1
                }
125
                $this->data['file'] = $filePath;
126
                $this->data['files'][] = $filePath;
127 1
                $this->data['filename'] = $paths[$i];
128 1
                $this->data['path'] = $paths[$i];
129 1
                $this->data['url'] = $paths[$i];
130 1
                $this->data['ext'] = Util\File::getExtension($filePath);
131 1
                $this->data['type'] = $type;
132 1
                $this->data['subtype'] = Util\File::getMediaType($filePath)[0] . '/' . Util\File::getMediaType($filePath)[1];
133 1
                // image: width, height and exif
134 1
                if ($this->data['type'] == 'image') {
135 1
                    $this->data['width'] = $this->getWidth();
136 1
                    $this->data['height'] = $this->getHeight();
137
                    if ($this->data['subtype'] == 'image/jpeg') {
138 1
                        $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
139 1
                    }
140 1
                }
141 1
                // bundle default filename
142 1
                if ($pathsCount > 1 && empty($filename)) {
143
                    switch ($this->data['ext']) {
144
                        case 'scss':
145
                        case 'css':
146 1
                            $filename = '/styles.css';
147 1
                            break;
148 1
                        case 'js':
149 1
                            $filename = '/scripts.js';
150 1
                            break;
151 1
                        default:
152 1
                            throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
153 1
                    }
154 1
                }
155
                // bundle filename and path
156
                if (!empty($filename)) {
157
                    $this->data['filename'] = $filename;
158
                    $this->data['path'] = '/' . ltrim($filename, '/');
159
                }
160 1
                // force leading slash
161 1
                if ($force_slash) {
162 1
                    $this->data['path'] = '/' . ltrim($paths[$i], '/');
163
                }
164
            } catch (RuntimeException $e) {
165
                if ($ignore_missing) {
166 1
                    $this->data['missing'] = true;
167
                    continue;
168 1
                }
169 1
                throw new RuntimeException(\sprintf('Can\'t get asset file "%s" (%s).', $paths[$i], $e->getMessage()));
170
            }
171 1
        }
172 1
173
        /*
174
        // cache
175 1
        $cache = new Cache($this->builder, 'assets');
176
        $cacheKey = \sprintf('%s__%s', $filename ?: implode('_', $paths), $cache->createKeyFromString($this->data['content']));
177 1
        if (!$cache->has($cacheKey)) {
178 1
            //
179
            $cache->set($cacheKey, $this->data);
180
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
181 1
            // optimizing images files
182 1
            if ($optimize && $this->data['type'] == 'image' && !$this->isImageInCdn()) {
183
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
184
            }
185 1
        }
186 1
        $this->data = $cache->get($cacheKey);
187
        */
188
189
        // fingerprinting
190
        if ($fingerprint && !$this->fingerprinted) {
191
            $this->fingerprint();
192
        }
193
        // compiling (Sass files)
194
        if ($this->config->isEnabled('assets.compile')) {
195 1
            $this->compile();
196
        }
197 1
        // minifying (CSS and JavScript files)
198
        if ($minify) {
199 1
            $this->minify();
200
        }
201
    }
202
203 1
    /**
204
     * Returns path.
205
     */
206
    public function __toString(): string
207 1
    {
208
        $this->save();
209
210
        if ($this->isImageInCdn()) {
211
            return $this->buildImageCdnUrl();
212
        }
213 1
214
        if ($this->builder->getConfig()->isEnabled('canonicalurl')) {
215 1
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
216 1
        }
217
218
        return $this->data['path'];
219 1
    }
220 1
221 1
    /**
222 1
     * Add hash to the file name.
223 1
     */
224 1
    public function fingerprint(): self
225
    {
226 1
        if ($this->fingerprinted) {
227
            return $this;
228 1
        }
229
230
        $fingerprint = hash('md5', $this->data['content']);
231
        $this->data['path'] = preg_replace(
232
            '/\.' . $this->data['ext'] . '$/m',
233
            ".$fingerprint." . $this->data['ext'],
234
            $this->data['path']
235
        );
236 1
237
        $this->fingerprinted = true;
238 1
239 1
        return $this;
240
    }
241
242 1
    /**
243 1
     * Compiles a SCSS.
244
     *
245
     * @throws RuntimeException
246 1
     */
247 1
    public function compile(): self
248 1
    {
249 1
        if ($this->compiled) {
250
            return $this;
251 1
        }
252 1
253 1
        if ($this->data['ext'] != 'scss') {
254 1
            return $this;
255 1
        }
256 1
257 1
        $cache = new Cache($this->builder, 'assets');
258 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
259 1
        if (!$cache->has($cacheKey)) {
260 1
            $scssPhp = new Compiler();
261 1
            // import paths
262 1
            $importDir = [];
263
            $importDir[] = Util::joinPath($this->config->getStaticPath());
264
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
265 1
            $scssDir = (array) $this->config->get('assets.compile.import');
266 1
            $themes = $this->config->getTheme() ?? [];
267
            foreach ($scssDir as $dir) {
268 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
269
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
270
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
271
                foreach ($themes as $theme) {
272
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
273
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
274
                }
275
            }
276
            $scssPhp->setQuietDeps(true);
277
            $scssPhp->setImportPaths(array_unique($importDir));
278
            // source map
279
            if ($this->builder->isDebug() && $this->config->isEnabled('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 1
                foreach ($scssDir as $dir) {
287 1
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
288 1
                }
289
                $scssPhp->setImportPaths(array_unique($importDir));
290
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
291 1
                $scssPhp->setSourceMapOptions([
292
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
293 1
                    'sourceRoot'        => '/',
294 1
                ]);
295 1
            }
296 1
            // output style
297
            $outputStyles = ['expanded', 'compressed'];
298
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
299 1
            if (!\in_array($outputStyle, $outputStyles)) {
300 1
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
301 1
            }
302
            $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 1
            }
309 1
            // 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 1
            // update data
315
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
316 1
            $this->data['ext'] = 'css';
317
            $this->data['type'] = 'text';
318
            $this->data['subtype'] = 'text/css';
319
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
320
            $this->data['size'] = \strlen($this->data['content']);
321
            $this->compiled = true;
322
            $cache->set($cacheKey, $this->data);
323
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
324 1
        }
325
        $this->data = $cache->get($cacheKey);
326
327 1
        return $this;
328
    }
329
330
    /**
331 1
     * 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
        if ($this->builder->isDebug() && $this->config->isEnabled('assets.compile.sourcemap')) {
339 1
            return $this;
340
        }
341
342
        if ($this->minified) {
343 1
            return $this;
344
        }
345
346
        if ($this->data['ext'] == 'scss') {
347
            $this->compile();
348
        }
349 1
350 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
351 1
            return $this;
352 1
        }
353 1
354 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
355 1
            $this->minified = true;
356 1
357 1
            return $this;
358 1
        }
359
360
        $cache = new Cache($this->builder, 'assets');
361
        $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 1
                default:
371 1
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
372
            }
373 1
            $this->data['path'] = preg_replace(
374
                '/\.' . $this->data['ext'] . '$/m',
375 1
                '.min.' . $this->data['ext'],
376
                $this->data['path']
377
            );
378
            $this->data['content'] = $minifier->minify();
379
            $this->data['size'] = \strlen($this->data['content']);
380
            $this->minified = true;
381
            $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 1
386 1
        return $this;
387 1
    }
388 1
389 1
    /**
390
     * Optimizing $filepath image.
391
     * Returns the new file size.
392
     */
393
    public function optimize(string $filepath, string $path): int
394
    {
395
        $quality = (int) $this->config->get('assets.images.quality');
396
        $message = \sprintf('Asset processed: "%s"', $path);
397 1
        $sizeBefore = filesize($filepath);
398
        Optimizer::create($quality)->optimize($filepath);
399 1
        $sizeAfter = filesize($filepath);
400
        if ($sizeAfter < $sizeBefore) {
401
            $message = \sprintf(
402
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
403
                $path,
404
                ceil($sizeBefore / 1000),
405
                ceil($sizeAfter / 1000)
406
            );
407 1
        }
408
        $this->builder->getLogger()->debug($message);
409 1
410
        return $sizeAfter;
411
    }
412 1
413
    /**
414
     * Resizes an image with a new $width.
415 1
     *
416 1
     * @throws RuntimeException
417
     */
418
    public function resize(int $width): self
419 1
    {
420 1
        if ($this->data['missing']) {
421
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
422 1
        }
423
        if ($this->data['type'] != 'image') {
424
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
425
        }
426
        if ($width >= $this->data['width']) {
427
            return $this;
428 1
        }
429 1
430 1
        $assetResized = clone $this;
431 1
        $assetResized->data['width'] = $width;
432 1
433 1
        if ($this->isImageInCdn()) {
434 1
            $assetResized->data['height'] = round($this->data['height'] / ($this->data['width'] / $width));
435 1
436 1
            return $assetResized; // returns asset with the new dimensions only: CDN do the rest of the job
437 1
        }
438 1
439 1
        $quality = (int) $this->config->get('assets.images.quality');
440 1
        $cache = new Cache($this->builder, 'assets');
441
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
442 1
        if (!$cache->has($cacheKey)) {
443 1
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
444
            $assetResized->data['path'] = '/' . Util::joinPath(
445 1
                (string) $this->config->get('assets.target'),
446
                'thumbnails',
447 1
                (string) $width,
448
                $assetResized->data['path']
449
            );
450
            $assetResized->data['height'] = $assetResized->getHeight();
451
            $assetResized->data['size'] = \strlen($assetResized->data['content']);
452
453
            $cache->set($cacheKey, $assetResized->data);
454
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
455 1
        }
456
        $assetResized->data = $cache->get($cacheKey);
457 1
458
        return $assetResized;
459
    }
460
461 1
    /**
462 1
     * Converts an image asset to $format format.
463
     *
464
     * @throws RuntimeException
465 1
     */
466 1
    public function convert(string $format, ?int $quality = null): self
467 1
    {
468
        if ($this->data['type'] != 'image') {
469 1
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
470
        }
471
472
        if ($quality === null) {
473 1
            $quality = (int) $this->config->get('assets.images.quality');
474 1
        }
475 1
476 1
        $asset = clone $this;
477
        $asset['ext'] = $format;
478 1
        $asset->data['subtype'] = "image/$format";
479 1
480 1
        if ($this->isImageInCdn()) {
481
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
482
        }
483
484
        $cache = new Cache($this->builder, 'assets');
485
        $tags = ["q$quality"];
486
        if ($this->data['width']) {
487
            array_unshift($tags, "{$this->data['width']}x");
488
        }
489
        $cacheKey = $cache->createKeyFromAsset($asset, $tags);
490
        if (!$cache->has($cacheKey)) {
491
            $asset->data['content'] = Image::convert($asset, $format, $quality);
492
            $asset->data['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
493
            $asset->data['size'] = \strlen($asset->data['content']);
494
            $cache->set($cacheKey, $asset->data);
495
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
496
        }
497
        $asset->data = $cache->get($cacheKey);
498
499
        return $asset;
500
    }
501
502
    /**
503
     * Converts an image asset to WebP format.
504
     *
505
     * @throws RuntimeException
506 1
     */
507
    public function webp(?int $quality = null): self
508 1
    {
509
        return $this->convert('webp', $quality);
510
    }
511
512
    /**
513
     * Converts an image asset to AVIF format.
514 1
     *
515
     * @throws RuntimeException
516
     */
517 1
    public function avif(?int $quality = null): self
518 1
    {
519
        return $this->convert('avif', $quality);
520
    }
521
522
    /**
523
     * Implements \ArrayAccess.
524
     */
525 1
    #[\ReturnTypeWillChange]
526
    public function offsetSet($offset, $value): void
527
    {
528 1
        if (!\is_null($offset)) {
529
            $this->data[$offset] = $value;
530
        }
531
    }
532
533
    /**
534
     * Implements \ArrayAccess.
535
     */
536
    #[\ReturnTypeWillChange]
537
    public function offsetExists($offset): bool
538
    {
539
        return isset($this->data[$offset]);
540
    }
541
542
    /**
543 1
     * Implements \ArrayAccess.
544
     */
545
    #[\ReturnTypeWillChange]
546 1
    public function offsetUnset($offset): void
547
    {
548
        unset($this->data[$offset]);
549
    }
550
551
    /**
552
     * Implements \ArrayAccess.
553
     */
554
    #[\ReturnTypeWillChange]
555 1
    public function offsetGet($offset)
556
    {
557 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
558
    }
559
560
    /**
561
     * Hashing content of an asset with the specified algo, sha384 by default.
562
     * Used for SRI (Subresource Integrity).
563
     *
564
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
565 1
     */
566
    public function getIntegrity(string $algo = 'sha384'): string
567 1
    {
568
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
569
    }
570
571 1
    /**
572
     * Returns MP3 file infos.
573
     *
574
     * @see https://github.com/wapmorgan/Mp3Info
575
     */
576
    public function getAudio(): Mp3Info
577
    {
578
        if ($this->data['type'] !== 'audio') {
579 1
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
580
        }
581 1
582
        return new Mp3Info($this->data['file']);
583
    }
584
585 1
    /**
586
     * Returns MP4 file infos.
587
     *
588
     * @see https://github.com/clwu88/php-read-mp4info
589
     */
590
    public function getVideo(): array
591
    {
592
        if ($this->data['type'] !== 'video') {
593 1
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
594
        }
595 1
596 1
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
597
    }
598
599 1
    /**
600
     * Returns the Data URL (encoded in Base64).
601
     *
602
     * @throws RuntimeException
603
     */
604
    public function dataurl(): string
605
    {
606
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
607 1
            return Image::getDataUrl($this, (int) $this->config->get('assets.images.quality'));
608
        }
609 1
610 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
611 1
    }
612
613
    /**
614
     * Adds asset path to the list of assets to save.
615
     *
616
     * @throws RuntimeException
617
     */
618 1
    public function save(): void
619
    {
620
        if ($this->data['missing']) {
621 1
            return;
622 1
        }
623 1
624 1
        $cache = new Cache($this->builder, 'assets');
625
        if (!Util\File::getFS()->exists($cache->getContentFilePathname($this->data['path']))) {
626
            throw new RuntimeException(\sprintf('Can\'t add "%s" to assets list: file not found.', $this->data['path']));
627
        }
628
629 1
        $this->builder->addAsset($this->data['path']);
630
    }
631
632
    /**
633 1
     * Is the asset an image and is it in CDN?
634
     */
635
    public function isImageInCdn(): bool
636
    {
637
        if (
638
            $this->data['type'] == 'image'
639
            && $this->config->isEnabled('assets.images.cdn')
640
            && $this->data['ext'] != 'ico'
641
            && (Image::isSVG($this) && $this->config->isEnabled('assets.images.cdn.svg'))
642
        ) {
643 1
            return true;
644
        }
645 1
        // handle remote image?
646 1
        if ($this->data['url'] !== null && $this->config->isEnabled('assets.images.cdn.remote')) {
647 1
            return true;
648 1
        }
649 1
650 1
        return false;
651 1
    }
652 1
653 1
    /**
654 1
     * Builds a relative path from a URL.
655 1
     * Used for remote files.
656
     */
657
    public static function buildPathFromUrl(string $url): string
658
    {
659 1
        $host = parse_url($url, PHP_URL_HOST);
660 1
        $path = parse_url($url, PHP_URL_PATH);
661 1
        $query = parse_url($url, PHP_URL_QUERY);
662 1
        $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
663 1
664
        // Google Fonts hack
665 1
        if (Util\Str::endsWith($path, '/css') || Util\Str::endsWith($path, '/css2')) {
666
            $ext = 'css';
667
        }
668
669
        return Page::slugify(\sprintf('%s%s%s%s', $host, self::sanitize($path), $query ? "-$query" : '', $query && $ext ? ".$ext" : ''));
670
    }
671
672 1
    /**
673 1
     * Replaces some characters by '_'.
674 1
     */
675 1
    public static function sanitize(string $string): string
676 1
    {
677 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
678
    }
679 1
680
    /**
681
     * Returns local file path or throw an exception.
682
     * Try to locate the file in:
683
     *   (1. remote file)
684
     *   1. assets
685 1
     *   2. themes/<theme>/assets
686
     *   3. static
687
     *   4. themes/<theme>/static
688
     *
689 1
     * @throws RuntimeException
690 1
     */
691
    private function locateFile(string $path, ?string $remote_fallback = null): string
0 ignored issues
show
Unused Code introduced by
The parameter $remote_fallback is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

691
    private function locateFile(string $path, /** @scrutinizer ignore-unused */ ?string $remote_fallback = null): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
692
    {
693
        // remote file
694 1
        if (Util\File::isRemote($path)) {
695 1
            $cacheKey = self::buildPathFromUrl($path);
696
            $cache = new Cache($this->builder, 'assets/remote');
697 1
            if (!$cache->has($cacheKey)) {
698 1
                $cache->set($cacheKey, [
699 1
                    'content' => $this->getRemoteFileContent($path),
700 1
                    'path'    => $path,
701 1
                ]);
702 1
            }
703 1
            return $cache->getContentFilePathname($path);
704 1
        }
705
706 1
        // checks in assets/
707
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
708
        if (Util\File::getFS()->exists($filePath)) {
709
            return $filePath;
710
        }
711
712
        // checks in each themes/<theme>/assets/
713
        foreach ($this->config->getTheme() ?? [] as $theme) {
714
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
715
            if (Util\File::getFS()->exists($filePath)) {
716
                return $filePath;
717
            }
718
        }
719
720 1
        // checks in static/
721
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
722
        if (Util\File::getFS()->exists($filePath)) {
723 1
            return $filePath;
724 1
        }
725
726 1
        // checks in each themes/<theme>/static/
727 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
728 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
729 1
            if (Util\File::getFS()->exists($filePath)) {
730
                return $filePath;
731 1
            }
732 1
        }
733
734 1
        throw new RuntimeException(\sprintf('Can\'t find file "%s".', $path));
735 1
    }
736 1
737 1
    /**
738 1
     * Try to get remote file content.
739 1
     * Returns file content or throw an exception.
740 1
     *
741 1
     * @throws RuntimeException
742
     */
743 1
    private function getRemoteFileContent(string $path): string
744
    {
745
        $content = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $content is dead and can be removed.
Loading history...
746 1
        try {
747
            if (!Util\File::isRemoteExists($path)) {
748
                throw new RuntimeException(\sprintf('Remote file "%s" doesn\'t exists', $path));
749 1
            }
750
            if (false === $content = Util\File::fileGetContents($path, true)) {
751
                throw new RuntimeException(\sprintf('Can\'t get content of remote file "%s".', $path));
752 1
            }
753 1
            if (\strlen($content) <= 1) {
754
                throw new RuntimeException(\sprintf('Remote file "%s" is empty.', $path));
755
            }
756
        } catch (RuntimeException $e) {
757
            throw new RuntimeException($e->getMessage());
758
        }
759
760
        return $content;
761
    }
762
763
    /**
764
     * Returns the width of an image/SVG.
765
     *
766
     * @throws RuntimeException
767
     */
768 1
    private function getWidth(): int
769
    {
770
        if ($this->data['type'] != 'image') {
771 1
            return 0;
772
        }
773
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
774
            return (int) $svg->width;
775 1
        }
776 1
        if (false === $size = $this->getImageSize()) {
777 1
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
778
        }
779
780
        return $size[0];
781 1
    }
782 1
783 1
    /**
784 1
     * Returns the height of an image/SVG.
785
     *
786
     * @throws RuntimeException
787
     */
788
    private function getHeight(): int
789 1
    {
790 1
        if ($this->data['type'] != 'image') {
791 1
            return 0;
792
        }
793
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
794
            return (int) $svg->height;
795 1
        }
796 1
        if (false === $size = $this->getImageSize()) {
797 1
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
798 1
        }
799
800
        return $size[1];
801
    }
802 1
803
    /**
804
     * Returns image size informations.
805
     *
806
     * @see https://www.php.net/manual/function.getimagesize.php
807
     *
808
     * @return array|false
809
     */
810 1
    private function getImageSize()
811
    {
812 1
        if (!$this->data['type'] == 'image') {
813
            return false;
814
        }
815 1
816 1
        try {
817
            if (false === $size = getimagesizefromstring($this->data['content'])) {
818 1
                return false;
819
            }
820
        } catch (\Exception $e) {
821
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path'], $e->getMessage()));
822 1
        }
823
824
        return $size;
825
    }
826
827
    /**
828
     * Builds CDN image URL.
829
     */
830 1
    private function buildImageCdnUrl(): string
831
    {
832 1
        return str_replace(
833
            [
834
                '%account%',
835 1
                '%image_url%',
836 1
                '%width%',
837
                '%quality%',
838 1
                '%format%',
839
            ],
840
            [
841
                $this->config->get('assets.images.cdn.account') ?? '',
842 1
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
843
                $this->data['width'],
844
                (int) $this->config->get('assets.images.quality'),
845
                $this->data['ext'],
846
            ],
847
            (string) $this->config->get('assets.images.cdn.url')
848
        );
849
    }
850
}
851