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

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

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