Passed
Pull Request — master (#2133)
by Arnaud
09:42 queued 04:08
created

Asset::findFile()   D

Complexity

Conditions 22
Paths 54

Size

Total Lines 87
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 26.579

Importance

Changes 0
Metric Value
cc 22
eloc 51
c 0
b 0
f 0
nc 54
nop 2
dl 0
loc 87
ccs 41
cts 52
cp 0.7885
crap 26.579
rs 4.1666

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

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

395
        Optimizer::create(/** @scrutinizer ignore-type */ $quality)->optimize($filepath);
Loading history...
396 1
        $sizeAfter = filesize($filepath);
397 1
        if ($sizeAfter < $sizeBefore) {
398
            $message = \sprintf(
399
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
400
                $path,
401
                ceil($sizeBefore / 1000),
402
                ceil($sizeAfter / 1000)
403
            );
404
        }
405 1
        $this->builder->getLogger()->debug($message);
406
407 1
        return $sizeAfter;
408
    }
409
410
    /**
411
     * Resizes an image with a new $width.
412
     *
413
     * @throws RuntimeException
414
     */
415 1
    public function resize(int $width): self
416
    {
417 1
        if ($this->data['missing']) {
418
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
419
        }
420 1
        if ($this->data['type'] != 'image') {
421
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
422
        }
423 1
        if ($width >= $this->data['width']) {
424 1
            return $this;
425
        }
426
427 1
        $assetResized = clone $this;
428 1
        $assetResized->data['width'] = $width;
429
430 1
        if ($this->isImageInCdn()) {
431
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
432
        }
433
434 1
        $quality = $this->config->get('assets.images.quality');
435 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
436 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
437 1
        if (!$cache->has($cacheKey)) {
438 1
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
0 ignored issues
show
Bug introduced by
It seems like $quality can also be of type null; however, parameter $quality of Cecil\Assets\Image::resize() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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