Passed
Pull Request — master (#2148)
by Arnaud
10:50 queued 04:56
created

Asset::locateFile()   B

Complexity

Conditions 11
Paths 89

Size

Total Lines 67
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 11.0013

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 11
eloc 40
c 6
b 0
f 0
nc 89
nop 2
dl 0
loc 67
ccs 44
cts 45
cp 0.9778
crap 11.0013
rs 7.3166

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
     *     '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 absolute files path
91 1
            'path'     => '',    // public path
92 1
            '_path'    => '',    // public path before any modification
93 1
            'url'      => null,  // URL if it's a remote file
94 1
            'missing'  => false, // if file not found but missing allowed: 'missing' is true
95 1
            'ext'      => '',    // file extension
96 1
            'type'     => '',    // file type (e.g.: image, audio, video, etc.)
97 1
            'subtype'  => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
98 1
            'size'     => 0,     // file size (in bytes)
99 1
            'width'    => 0,     // image width (in pixels)
100 1
            'height'   => 0,     // image height (in pixels)
101 1
            'exif'     => [],    // image exif data
102 1
            'content'  => '',    // file content
103 1
        ];
104
105
        // handles options
106 1
        $fingerprint = $this->config->isEnabled('assets.fingerprint');
107 1
        $minify = $this->config->isEnabled('assets.minify');
108 1
        $optimize = $this->config->isEnabled('assets.images.optimize');
109 1
        $filename = '';
110 1
        $ignore_missing = false;
111 1
        $fallback = null;
112 1
        $force_slash = true;
113 1
        extract(\is_array($options) ? $options : [], EXTR_IF_EXISTS);
114
115
        /*
116
        $options = array_merge(
117
            [
118
                'fingerprint'    => $this->config->isEnabled('assets.fingerprint'),
119
                'minify'         => $this->config->isEnabled('assets.minify'),
120
                'optimize'       => $this->config->isEnabled('assets.images.optimize'),
121
                'filename'       => '',
122
                'ignore_missing' => false,
123
                'fallback'       => null,
124
                'force_slash'    => true,
125
            ],
126
            \is_array($options) ? $options : []
127
        );
128
        $tags = [];
129
        foreach ($options as $key => $value) {
130
            if (\is_bool($value) && $value === true) {
131
                $tags[] = $key;
132
            }
133
            if (\is_string($value) && !empty($value)) {
134
                $tags[] = $value;
135
            }
136
        }
137
        // DEBUG
138
        echo implode('_', $tags) . "\n";
139
        echo hash('crc32', implode('_', $tags)) . "\n";
140
        die('debug');
141
        */
142
143
        // locate file(s) and get content
144 1
        $pathsCount = \count($paths);
145 1
        for ($i = 0; $i < $pathsCount; $i++) {
146
            try {
147 1
                $this->data['missing'] = false;
148 1
                $locate = $this->locateFile($paths[$i], $fallback);
149 1
                $file = $locate['file'];
150 1
                $path = $locate['path'];
151 1
                $type = Util\File::getMediaType($file)[0];
152 1
                if ($i > 0) { // bundle
153 1
                    if ($type != $this->data['type']) {
154
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $type, $this->data['type']));
155
                    }
156
                }
157 1
                $this->data['file'] = $file;
158 1
                $this->data['files'][] = $file;
159 1
                $this->data['path'] = $path;
160 1
                $this->data['url'] = Util\File::isRemote($paths[$i]) ? $paths[$i] : null;
161 1
                $this->data['ext'] = Util\File::getExtension($file);
162 1
                $this->data['type'] = $type;
163 1
                $this->data['subtype'] = Util\File::getMediaType($file)[1];
164 1
                $this->data['size'] += filesize($file);
165 1
                $this->data['content'] .= Util\File::fileGetContents($file);
166
                // bundle default filename
167 1
                if ($pathsCount > 1 && empty($filename)) {
168 1
                    switch ($this->data['ext']) {
169 1
                        case 'scss':
170 1
                        case 'css':
171 1
                            $filename = 'styles.css';
172 1
                            break;
173 1
                        case 'js':
174 1
                            $filename = 'scripts.js';
175 1
                            break;
176
                        default:
177
                            throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
178
                    }
179
                }
180
                // apply bundle filename to path
181 1
                if (!empty($filename)) {
182 1
                    $this->data['path'] = '/' . ltrim($filename, '/');
183
                }
184
                // force root slash
185 1
                if ($force_slash) {
186 1
                    $this->data['path'] = '/' . ltrim($this->data['path'], '/');
187
                }
188 1
                $this->data['_path'] = $this->data['path'];
189 1
            } catch (RuntimeException $e) {
190 1
                if ($ignore_missing) {
191 1
                    $this->data['missing'] = true;
192 1
                    continue;
193
                }
194
                throw new RuntimeException(\sprintf('Can\'t handle asset "%s" (%s).', $paths[$i], $e->getMessage()));
195
            }
196
        }
197
198
        // missing
199 1
        if ($this->data['missing']) {
200 1
            return;
201
        }
202
203
        // cache
204 1
        $cache = new Cache($this->builder, 'assets');
205 1
        $cacheKey = $cache->createKeyFromAsset($this, $fingerprint ? ['fingerprinted'] : []);
206 1
        if (!$cache->has($cacheKey)) {
207
            // image: width, height and exif
208 1
            if ($this->data['type'] == 'image') {
209 1
                $this->data['width'] = $this->getWidth();
210 1
                $this->data['height'] = $this->getHeight();
211 1
                if ($this->data['subtype'] == 'image/jpeg') {
212 1
                    $this->data['exif'] = Util\File::readExif($this->data['file']);
213
                }
214
            }
215
            // fingerprinting
216 1
            if ($fingerprint) {
217
                $this->fingerprint();
218
            }
219 1
            $cache->set($cacheKey, $this->data);
220 1
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
221
            // optimizing images files (in cache)
222 1
            if ($optimize && $this->data['type'] == 'image' && !$this->isImageInCdn()) {
223 1
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
224
            }
225
        }
226 1
        $this->data = $cache->get($cacheKey);
227
228
        // compiling Sass files
229 1
        $this->compile();
230
        // minifying (CSS and JavScript files)
231 1
        if ($minify) {
232 1
            $this->minify();
233
        }
234
    }
235
236
    /**
237
     * Returns path.
238
     */
239 1
    public function __toString(): string
240
    {
241 1
        $this->save();
242
243 1
        if ($this->isImageInCdn()) {
244
            return $this->buildImageCdnUrl();
245
        }
246
247 1
        if ($this->builder->getConfig()->isEnabled('canonicalurl')) {
248
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
249
        }
250
251 1
        return $this->data['path'];
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
            $cache->set($cacheKey, $this->data);
334 1
            $this->compiled = true;
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
        // in debug mode, 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->data['ext'] != 'css' && $this->data['ext'] != 'js') {
355
            return $this;
356
        }
357
358 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
359
            $this->minified = true;
360
        }
361
362 1
        if ($this->minified) {
363
            return $this;
364
        }
365
366 1
        if ($this->data['ext'] == 'scss') {
367
            $this->compile();
368
        }
369
370 1
        $cache = new Cache($this->builder, 'assets');
371 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
372 1
        if (!$cache->has($cacheKey)) {
373 1
            switch ($this->data['ext']) {
374 1
                case 'css':
375 1
                    $minifier = new Minify\CSS($this->data['content']);
376 1
                    break;
377 1
                case 'js':
378 1
                    $minifier = new Minify\JS($this->data['content']);
379 1
                    break;
380
                default:
381
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
382
            }
383 1
            $this->data['content'] = $minifier->minify();
384 1
            $this->data['size'] = \strlen($this->data['content']);
385 1
            $cache->set($cacheKey, $this->data);
386 1
            $this->minified = true;
387 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
388
        }
389 1
        $this->data = $cache->get($cacheKey);
390
391 1
        return $this;
392
    }
393
394
    /**
395
     * Add hash to the file name.
396
     */
397 1
    public function fingerprint(): self
398
    {
399 1
        if ($this->fingerprinted) {
400
            return $this;
401
        }
402
403 1
        $cache = new Cache($this->builder, 'assets');
404 1
        $cacheKey = $cache->createKeyFromAsset($this, ['fingerprinted']);
405 1
        if (!$cache->has($cacheKey)) {
406 1
            $hash = hash('md5', $this->data['content']);
407 1
            $this->data['path'] = preg_replace(
408 1
                '/\.' . $this->data['ext'] . '$/m',
409 1
                ".$hash." . $this->data['ext'],
410 1
                $this->data['path']
411 1
            );
412 1
            $cache->set($cacheKey, $this->data);
413 1
            $this->fingerprinted = true;
414 1
            $this->builder->getLogger()->debug(\sprintf('Asset fingerprinted: "%s"', $this->data['path']));
415
        }
416 1
        $this->data = $cache->get($cacheKey);
417
418 1
        return $this;
419
    }
420
421
    /**
422
     * Optimizing $filepath image.
423
     * Returns the new file size.
424
     */
425 1
    public function optimize(string $filepath, string $path): int
426
    {
427 1
        $quality = (int) $this->config->get('assets.images.quality');
428 1
        $message = \sprintf('Asset processed: "%s"', $path);
429 1
        $sizeBefore = filesize($filepath);
430 1
        Optimizer::create($quality)->optimize($filepath);
431 1
        $sizeAfter = filesize($filepath);
432 1
        if ($sizeAfter < $sizeBefore) {
433
            $message = \sprintf(
434
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
435
                $path,
436
                ceil($sizeBefore / 1000),
437
                ceil($sizeAfter / 1000)
438
            );
439
        }
440 1
        $this->builder->getLogger()->debug($message);
441
442 1
        return $sizeAfter;
443
    }
444
445
    /**
446
     * Resizes an image with a new $width.
447
     *
448
     * @throws RuntimeException
449
     */
450 1
    public function resize(int $width): self
451
    {
452 1
        if ($this->data['missing']) {
453
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
454
        }
455 1
        if ($this->data['type'] != 'image') {
456
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
457
        }
458 1
        if ($width >= $this->data['width']) {
459 1
            return $this;
460
        }
461
462 1
        $assetResized = clone $this;
463 1
        $assetResized->data['width'] = $width;
464
465 1
        if ($this->isImageInCdn()) {
466
            $assetResized->data['height'] = round($this->data['height'] / ($this->data['width'] / $width));
467
468
            return $assetResized; // returns asset with the new dimensions only: CDN do the rest of the job
469
        }
470
471 1
        $quality = (int) $this->config->get('assets.images.quality');
472 1
        $cache = new Cache($this->builder, 'assets');
473 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
474 1
        if (!$cache->has($cacheKey)) {
475 1
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
476 1
            $assetResized->data['path'] = '/' . Util::joinPath(
477 1
                (string) $this->config->get('assets.target'),
478 1
                'thumbnails',
479 1
                (string) $width,
480 1
                $assetResized->data['path']
481 1
            );
482 1
            $assetResized->data['height'] = $assetResized->getHeight();
483 1
            $assetResized->data['size'] = \strlen($assetResized->data['content']);
484
485 1
            $cache->set($cacheKey, $assetResized->data);
486 1
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
487
        }
488 1
        $assetResized->data = $cache->get($cacheKey);
489
490 1
        return $assetResized;
491
    }
492
493
    /**
494
     * Converts an image asset to $format format.
495
     *
496
     * @throws RuntimeException
497
     */
498 1
    public function convert(string $format, ?int $quality = null): self
499
    {
500 1
        if ($this->data['type'] != 'image') {
501
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
502
        }
503
504 1
        if ($quality === null) {
505 1
            $quality = (int) $this->config->get('assets.images.quality');
506
        }
507
508 1
        $asset = clone $this;
509 1
        $asset['ext'] = $format;
510 1
        $asset->data['subtype'] = "image/$format";
511
512 1
        if ($this->isImageInCdn()) {
513
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
514
        }
515
516 1
        $cache = new Cache($this->builder, 'assets');
517 1
        $tags = ["q$quality"];
518 1
        if ($this->data['width']) {
519 1
            array_unshift($tags, "{$this->data['width']}x");
520
        }
521 1
        $cacheKey = $cache->createKeyFromAsset($asset, $tags);
522 1
        if (!$cache->has($cacheKey)) {
523 1
            $asset->data['content'] = Image::convert($asset, $format, $quality);
524
            $asset->data['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
525
            $asset->data['size'] = \strlen($asset->data['content']);
526
            $cache->set($cacheKey, $asset->data);
527
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
528
        }
529
        $asset->data = $cache->get($cacheKey);
530
531
        return $asset;
532
    }
533
534
    /**
535
     * Converts an image asset to WebP format.
536
     *
537
     * @throws RuntimeException
538
     */
539
    public function webp(?int $quality = null): self
540
    {
541
        return $this->convert('webp', $quality);
542
    }
543
544
    /**
545
     * Converts an image asset to AVIF format.
546
     *
547
     * @throws RuntimeException
548
     */
549 1
    public function avif(?int $quality = null): self
550
    {
551 1
        return $this->convert('avif', $quality);
552
    }
553
554
    /**
555
     * Implements \ArrayAccess.
556
     */
557 1
    #[\ReturnTypeWillChange]
558
    public function offsetSet($offset, $value): void
559
    {
560 1
        if (!\is_null($offset)) {
561 1
            $this->data[$offset] = $value;
562
        }
563
    }
564
565
    /**
566
     * Implements \ArrayAccess.
567
     */
568 1
    #[\ReturnTypeWillChange]
569
    public function offsetExists($offset): bool
570
    {
571 1
        return isset($this->data[$offset]);
572
    }
573
574
    /**
575
     * Implements \ArrayAccess.
576
     */
577
    #[\ReturnTypeWillChange]
578
    public function offsetUnset($offset): void
579
    {
580
        unset($this->data[$offset]);
581
    }
582
583
    /**
584
     * Implements \ArrayAccess.
585
     */
586 1
    #[\ReturnTypeWillChange]
587
    public function offsetGet($offset)
588
    {
589 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
590
    }
591
592
    /**
593
     * Hashing content of an asset with the specified algo, sha384 by default.
594
     * Used for SRI (Subresource Integrity).
595
     *
596
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
597
     */
598 1
    public function getIntegrity(string $algo = 'sha384'): string
599
    {
600 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
601
    }
602
603
    /**
604
     * Returns MP3 file infos.
605
     *
606
     * @see https://github.com/wapmorgan/Mp3Info
607
     */
608 1
    public function getAudio(): Mp3Info
609
    {
610 1
        if ($this->data['type'] !== 'audio') {
611
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
612
        }
613
614 1
        return new Mp3Info($this->data['file']);
615
    }
616
617
    /**
618
     * Returns MP4 file infos.
619
     *
620
     * @see https://github.com/clwu88/php-read-mp4info
621
     */
622 1
    public function getVideo(): array
623
    {
624 1
        if ($this->data['type'] !== 'video') {
625
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
626
        }
627
628 1
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
629
    }
630
631
    /**
632
     * Returns the Data URL (encoded in Base64).
633
     *
634
     * @throws RuntimeException
635
     */
636 1
    public function dataurl(): string
637
    {
638 1
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
639 1
            return Image::getDataUrl($this, (int) $this->config->get('assets.images.quality'));
640
        }
641
642 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
643
    }
644
645
    /**
646
     * Adds asset path to the list of assets to save.
647
     *
648
     * @throws RuntimeException
649
     */
650 1
    public function save(): void
651
    {
652 1
        if ($this->data['missing']) {
653 1
            return;
654
        }
655
656 1
        $cache = new Cache($this->builder, 'assets');
657 1
        if (!Util\File::getFS()->exists($cache->getContentFilePathname($this->data['path']))) {
658
            throw new RuntimeException(
659
                \sprintf('Can\'t add "%s" to assets list. Please clear cache and retry.', $this->data['path'])
660
            );
661
        }
662
663 1
        $this->builder->addAsset($this->data['path']);
664
    }
665
666
    /**
667
     * Is the asset an image and is it in CDN?
668
     */
669 1
    public function isImageInCdn(): bool
670
    {
671
        if (
672 1
            $this->data['type'] == 'image'
673 1
            && $this->config->isEnabled('assets.images.cdn')
674 1
            && $this->data['ext'] != 'ico'
675 1
            && (Image::isSVG($this) && $this->config->isEnabled('assets.images.cdn.svg'))
676
        ) {
677
            return true;
678
        }
679
        // handle remote image?
680 1
        if ($this->data['url'] !== null && $this->config->isEnabled('assets.images.cdn.remote')) {
681
            return true;
682
        }
683
684 1
        return false;
685
    }
686
687
    /**
688
     * Builds a relative path from a URL.
689
     * Used for remote files.
690
     */
691 1
    public static function buildPathFromUrl(string $url): string
692
    {
693 1
        $host = parse_url($url, PHP_URL_HOST);
694 1
        $path = parse_url($url, PHP_URL_PATH);
695 1
        $query = parse_url($url, PHP_URL_QUERY);
696 1
        $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
697
698
        // Google Fonts hack
699 1
        if (Util\Str::endsWith($path, '/css') || Util\Str::endsWith($path, '/css2')) {
700 1
            $ext = 'css';
701
        }
702
703 1
        return Page::slugify(\sprintf('%s%s%s%s', $host, self::sanitize($path), $query ? "-$query" : '', $query && $ext ? ".$ext" : ''));
704
    }
705
706
    /**
707
     * Replaces some characters by '_'.
708
     */
709 1
    public static function sanitize(string $string): string
710
    {
711 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
712
    }
713
714
    /**
715
     * Returns local file path and updated path, or throw an exception.
716
     *
717
     * Try to locate the file in:
718
     *   (1. remote file)
719
     *   1. assets
720
     *   2. themes/<theme>/assets
721
     *   3. static
722
     *   4. themes/<theme>/static
723
     *
724
     * If $fallback is set, it will be used if the file is not found.
725
     *
726
     * @throws RuntimeException
727
     */
728 1
    private function locateFile(string $path, ?string $fallback = null): array
729
    {
730
        // remote file
731 1
        if (Util\File::isRemote($path)) {
732
            try {
733 1
                $content = $this->getRemoteFileContent($path);
734 1
                $path = self::buildPathFromUrl($path);
735 1
                $cache = new Cache($this->builder, 'assets/remote');
736 1
                if (!$cache->has($path)) {
737 1
                    $cache->set($path, [
738 1
                        'content' => $content,
739 1
                        'path'    => $path,
740 1
                    ], \DateInterval::createFromDateString('7 days'));
741
                }
742 1
                return [
743 1
                    'file' => $cache->getContentFilePathname($path),
744 1
                    'path' => $path,
745 1
                ];
746 1
            } catch (RuntimeException $e) {
747 1
                if ($fallback === null) {
748
                    throw new RuntimeException($e->getMessage(), previous: $e);
749
                }
750 1
                $path = $fallback;
751
            }
752
        }
753
754
        // checks in assets/
755 1
        $file = Util::joinFile($this->config->getAssetsPath(), $path);
756 1
        if (Util\File::getFS()->exists($file)) {
757 1
            return [
758 1
                'file' => $file,
759 1
                'path' => $path,
760 1
            ];
761
        }
762
763
        // checks in each themes/<theme>/assets/
764 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
765 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
766 1
            if (Util\File::getFS()->exists($file)) {
767 1
                return [
768 1
                    'file' => $file,
769 1
                    'path' => $path,
770 1
                ];
771
            }
772
        }
773
774
        // checks in static/
775 1
        $file = Util::joinFile($this->config->getStaticTargetPath(), $path);
776 1
        if (Util\File::getFS()->exists($file)) {
777 1
            return [
778 1
                'file' => $file,
779 1
                'path' => $path,
780 1
            ];
781
        }
782
783
        // checks in each themes/<theme>/static/
784 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
785 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
786 1
            if (Util\File::getFS()->exists($file)) {
787 1
                return [
788 1
                    'file' => $file,
789 1
                    'path' => $path,
790 1
                ];
791
            }
792
        }
793
794 1
        throw new RuntimeException(\sprintf('Can\'t locate file "%s".', $path));
795
    }
796
797
    /**
798
     * Try to get remote file content.
799
     * Returns file content or throw an exception.
800
     *
801
     * @throws RuntimeException
802
     */
803 1
    private function getRemoteFileContent(string $path): string
804
    {
805
        try {
806 1
            if (!Util\File::isRemoteExists($path)) {
807 1
                throw new RuntimeException(\sprintf('Remote file "%s" doesn\'t exists', $path));
808
            }
809 1
            if (false === $content = Util\File::fileGetContents($path, true)) {
810
                throw new RuntimeException(\sprintf('Can\'t get content of remote file "%s".', $path));
811
            }
812 1
            if (\strlen($content) <= 1) {
813 1
                throw new RuntimeException(\sprintf('Remote file "%s" is empty.', $path));
814
            }
815 1
        } catch (RuntimeException $e) {
816 1
            throw new RuntimeException($e->getMessage());
817
        }
818
819 1
        return $content;
820
    }
821
822
    /**
823
     * Returns the width of an image/SVG.
824
     *
825
     * @throws RuntimeException
826
     */
827 1
    private function getWidth(): int
828
    {
829 1
        if ($this->data['type'] != 'image') {
830
            return 0;
831
        }
832 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
833 1
            return (int) $svg->width;
834
        }
835 1
        if (false === $size = $this->getImageSize()) {
836
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
837
        }
838
839 1
        return $size[0];
840
    }
841
842
    /**
843
     * Returns the height of an image/SVG.
844
     *
845
     * @throws RuntimeException
846
     */
847 1
    private function getHeight(): int
848
    {
849 1
        if ($this->data['type'] != 'image') {
850
            return 0;
851
        }
852 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
853 1
            return (int) $svg->height;
854
        }
855 1
        if (false === $size = $this->getImageSize()) {
856
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
857
        }
858
859 1
        return $size[1];
860
    }
861
862
    /**
863
     * Returns image size informations.
864
     *
865
     * @see https://www.php.net/manual/function.getimagesize.php
866
     *
867
     * @return array|false
868
     */
869 1
    private function getImageSize()
870
    {
871 1
        if (!$this->data['type'] == 'image') {
872
            return false;
873
        }
874
875
        try {
876 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
877 1
                return false;
878
            }
879
        } catch (\Exception $e) {
880
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path'], $e->getMessage()));
881
        }
882
883 1
        return $size;
884
    }
885
886
    /**
887
     * Builds CDN image URL.
888
     */
889
    private function buildImageCdnUrl(): string
890
    {
891
        return str_replace(
892
            [
893
                '%account%',
894
                '%image_url%',
895
                '%width%',
896
                '%quality%',
897
                '%format%',
898
            ],
899
            [
900
                $this->config->get('assets.images.cdn.account') ?? '',
901
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
902
                $this->data['width'],
903
                (int) $this->config->get('assets.images.quality'),
904
                $this->data['ext'],
905
            ],
906
            (string) $this->config->get('assets.images.cdn.url')
907
        );
908
    }
909
}
910