Test Failed
Pull Request — master (#2133)
by Arnaud
05:28 queued 15s
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 1
96 1
        // handles options
97
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
98
        $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 1
107 1
        // fill data array with file(s) informations
108
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
109
        $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 1
                // 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
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
119 1
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
120 1
                    }
121
                }
122
                // missing allowed = empty path
123
                if ($file[$i]['missing']) {
124
                    $this->data['missing'] = true;
125 1
                    $this->data['path'] = $file[$i]['path'];
126 1
127 1
                    continue;
128
                }
129 1
                // set data
130
                $this->data['content'] .= $file[$i]['content'];
131
                $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 1
                    // 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 1
                        }
147 1
                    }
148 1
                    // bundle default filename
149
                    if ($pathsCount > 1 && empty($filename)) {
150
                        switch ($this->data['ext']) {
151
                            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 1
                            default:
159 1
                                throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
160 1
                        }
161
                    }
162
                    // bundle filename and path
163
                    if (!empty($filename)) {
164
                        $this->data['filename'] = $filename;
165
                        $this->data['path'] = '/' . ltrim($filename, '/');
166 1
                    }
167 1
                }
168 1
                // bundle files path
169
                $this->data['files'][] = $file[$i]['filepath'];
170
            }
171
            $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 1
        }
178
        $this->data = $cache->get($cacheKey);
179
180 1
        // fingerprinting
181 1
        if ($fingerprint) {
182
            $this->fingerprint();
183
        }
184 1
        // compiling (Sass files)
185 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
186
            $this->compile();
187
        }
188 1
        // minifying (CSS and JavScript files)
189 1
        if ($minify) {
190
            $this->minify();
191
        }
192 1
    }
193 1
194
    /**
195
     * Returns path.
196
     *
197
     * @throws RuntimeException
198
     */
199
    public function __toString(): string
200
    {
201
        try {
202 1
            $this->save();
203
        } catch (RuntimeException $e) {
204
            $this->builder->getLogger()->error($e->getMessage());
205 1
        }
206
207
        if ($this->isImageInCdn()) {
208
            return $this->buildImageCdnUrl();
209
        }
210 1
211
        if ($this->builder->getConfig()->get('canonicalurl')) {
212
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
213
        }
214 1
215
        return $this->data['path'];
216
    }
217
218 1
    /**
219
     * Fingerprints a file.
220
     */
221
    public function fingerprint(): self
222
    {
223
        if ($this->fingerprinted) {
224 1
            return $this;
225
        }
226 1
227 1
        $fingerprint = hash('md5', $this->data['content']);
228
        $this->data['path'] = preg_replace(
229
            '/\.' . $this->data['ext'] . '$/m',
230 1
            ".$fingerprint." . $this->data['ext'],
231 1
            $this->data['path']
232 1
        );
233 1
234 1
        $this->fingerprinted = true;
235 1
236
        return $this;
237 1
    }
238
239 1
    /**
240
     * Compiles a SCSS.
241
     *
242
     * @throws RuntimeException
243
     */
244
    public function compile(): self
245
    {
246
        if ($this->compiled) {
247 1
            return $this;
248
        }
249 1
250 1
        if ($this->data['ext'] != 'scss') {
251
            return $this;
252
        }
253 1
254 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
255
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
256
        if (!$cache->has($cacheKey)) {
257 1
            $scssPhp = new Compiler();
258 1
            // import paths
259 1
            $importDir = [];
260 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
261
            $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 1
                }
272 1
            }
273 1
            $scssPhp->setQuietDeps(true);
274
            $scssPhp->setImportPaths(array_unique($importDir));
275
            // source map
276 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
277 1
                $importDir = [];
278
                $assetDir = (string) $this->config->get('assets.dir');
279 1
                $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
            $outputStyles = ['expanded', 'compressed'];
295
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
296
            if (!\in_array($outputStyle, $outputStyles)) {
297 1
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
298 1
            }
299 1
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
300
            // variables
301
            $variables = $this->config->get('assets.compile.variables');
302 1
            if (!empty($variables)) {
303
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
304 1
                $scssPhp->replaceVariables($variables);
305 1
            }
306 1
            // debug
307 1
            if ($this->builder->isDebug()) {
308
                $scssPhp->setQuietDeps(false);
309
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
310 1
            }
311 1
            // update data
312 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
313
            $this->data['ext'] = 'css';
314
            $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 1
        }
322 1
        $this->data = $cache->get($cacheKey);
323 1
324
        return $this;
325 1
    }
326
327 1
    /**
328
     * Minifying a CSS or a JS.
329
     *
330
     * @throws RuntimeException
331
     */
332
    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 1
339
        if ($this->minified) {
340
            return $this;
341
        }
342 1
343
        if ($this->data['ext'] == 'scss') {
344
            $this->compile();
345
        }
346 1
347
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
348
            return $this;
349
        }
350 1
351
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
352
            $this->minified = true;
353
354 1
            return $this;
355
        }
356
357
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
358
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
359
        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 1
                default:
368 1
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
369 1
            }
370
            $this->data['path'] = preg_replace(
371
                '/\.' . $this->data['ext'] . '$/m',
372
                '.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 1
        }
381 1
        $this->data = $cache->get($cacheKey);
382 1
383
        return $this;
384 1
    }
385
386 1
    /**
387
     * Optimizing $filepath image.
388
     * Returns the new file size.
389
     */
390
    public function optimize(string $filepath, string $path): int
391
    {
392 1
        $quality = $this->config->get('assets.images.quality');
393
        $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
        $sizeAfter = filesize($filepath);
397
        if ($sizeAfter < $sizeBefore) {
398 1
            $message = \sprintf(
399 1
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
400 1
                $path,
401 1
                ceil($sizeBefore / 1000),
402 1
                ceil($sizeAfter / 1000)
403
            );
404 1
        }
405 1
        $this->builder->getLogger()->debug($message);
406 1
407 1
        return $sizeAfter;
408 1
    }
409 1
410 1
    /**
411
     * Resizes an image with a new $width.
412
     *
413
     * @throws RuntimeException
414
     */
415
    public function resize(int $width): self
416
    {
417
        if ($this->data['missing']) {
418 1
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
419 1
        }
420 1
        if ($this->data['type'] != 'image') {
421 1
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
422
        }
423 1
        if ($width >= $this->data['width']) {
424
            return $this;
425 1
        }
426
427
        $assetResized = clone $this;
428
        $assetResized->data['width'] = $width;
429
430
        if ($this->isImageInCdn()) {
431
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
432
        }
433 1
434
        $quality = $this->config->get('assets.images.quality');
435 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
436
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
437
        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
            $assetResized->data['path'] = '/' . Util::joinPath(
440
                (string) $this->config->get('assets.target'),
441 1
                (string) $this->config->get('assets.images.resize.dir'),
442 1
                (string) $width,
443
                $assetResized->data['path']
444
            );
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
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
450
        }
451
        $assetResized->data = $cache->get($cacheKey);
452 1
453 1
        return $assetResized;
454 1
    }
455 1
456 1
    /**
457 1
     * Converts an image asset to $format format.
458 1
     *
459 1
     * @throws RuntimeException
460 1
     */
461 1
    public function convert(string $format, ?int $quality = null): self
462 1
    {
463 1
        if ($this->data['type'] != 'image') {
464 1
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
465
        }
466 1
467 1
        if ($quality === null) {
468
            $quality = (int) $this->config->get('assets.images.quality');
469 1
        }
470
471 1
        $asset = clone $this;
472
        $asset['ext'] = $format;
473
474
        if ($this->isImageInCdn()) {
475
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
476
        }
477
478
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
479 1
        $tags = ["q$quality"];
480
        if ($this->data['width']) {
481 1
            array_unshift($tags, "{$this->data['width']}x");
482
        }
483
        $cacheKey = $cache->createKeyFromAsset($asset, $tags);
484
        if (!$cache->has($cacheKey)) {
485 1
            $asset->data['content'] = Image::convert($asset, $format, $quality);
486 1
            $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 1
            $cache->set($cacheKey, $asset->data);
490 1
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
491
        }
492 1
        $asset->data = $cache->get($cacheKey);
493
494
        return $asset;
495
    }
496 1
497 1
    /**
498 1
     * Converts an image asset to WebP format.
499 1
     *
500
     * @throws RuntimeException
501 1
     */
502 1
    public function webp(?int $quality = null): self
503 1
    {
504
        return $this->convert('webp', $quality);
505
    }
506
507
    /**
508
     * Converts an image asset to AVIF format.
509
     *
510
     * @throws RuntimeException
511
     */
512
    public function avif(?int $quality = null): self
513
    {
514
        return $this->convert('avif', $quality);
515
    }
516
517
    /**
518
     * Implements \ArrayAccess.
519
     */
520
    #[\ReturnTypeWillChange]
521
    public function offsetSet($offset, $value): void
522
    {
523
        if (!\is_null($offset)) {
524
            $this->data[$offset] = $value;
525
        }
526
    }
527
528
    /**
529
     * Implements \ArrayAccess.
530 1
     */
531
    #[\ReturnTypeWillChange]
532 1
    public function offsetExists($offset): bool
533
    {
534
        return isset($this->data[$offset]);
535
    }
536
537
    /**
538 1
     * Implements \ArrayAccess.
539
     */
540
    #[\ReturnTypeWillChange]
541 1
    public function offsetUnset($offset): void
542 1
    {
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
    public function getIntegrity(string $algo = 'sha384'): string
562
    {
563
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
564
    }
565
566
    /**
567 1
     * Returns MP3 file infos.
568
     *
569
     * @see https://github.com/wapmorgan/Mp3Info
570 1
     */
571
    public function getAudio(): Mp3Info
572
    {
573
        if ($this->data['type'] !== 'audio') {
574
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
575
        }
576
577
        return new Mp3Info($this->data['file']);
578
    }
579 1
580
    /**
581 1
     * Returns MP4 file infos.
582
     *
583
     * @see https://github.com/clwu88/php-read-mp4info
584
     */
585
    public function getVideo(): array
586
    {
587
        if ($this->data['type'] !== 'video') {
588
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
589 1
        }
590
591 1
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
592
    }
593
594
    /**
595 1
     * Returns the Data URL (encoded in Base64).
596
     *
597
     * @throws RuntimeException
598
     */
599
    public function dataurl(): string
600
    {
601
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
602
            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 1
        }
604
605 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
606
    }
607
608
    /**
609 1
     * 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
    public function save(): void
615
    {
616
        $this->toSave[] = $this->data['path'];
617 1
618
        /*
619 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
620 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
621
            try {
622
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
623 1
            } 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 1
    /**
633
     * Is Asset is an image in CDN.
634 1
     *
635 1
     * @return bool
636
     */
637 1
    public function isImageInCdn()
638 1
    {
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
            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 1
     *
655
     * @return string[]
656 1
     */
657 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
658
    {
659
        $file = [
660
            'url'      => null,
661
            'filepath' => null,
662
            'path'     => null,
663
            'ext'      => null,
664
            'type'     => null,
665
            'subtype'  => null,
666
            'size'     => null,
667
            'content'  => null,
668
            'missing'  => false,
669
        ];
670
671
        // try to find file locally and returns the file path
672
        try {
673
            $filePath = $this->locateFile($path, $remote_fallback);
674 1
        } catch (RuntimeException $e) {
675
            if ($ignore_missing) {
676 1
                $file['path'] = $path;
677 1
                $file['missing'] = true;
678 1
679
                return $file;
680
            }
681 1
682 1
            throw new RuntimeException(\sprintf('Can\'t load asset file "%s" (%s).', $path, $e->getMessage()));
683 1
        }
684 1
685 1
        // in case of an URL, the file is already stored in cache
686
        if (Util\Url::isUrl($path)) {
687 1
            $file['url'] = $path;
688
            $path = Util::joinPath(
689
                (string) $this->config->get('assets.target'),
690
                Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsFilesPath())
691
            );
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 1
                $path = Util::joinPath(
695 1
                    (string) $this->config->get('assets.target'),
696 1
                    Util\File::getFS()->makePathRelative($filePath, $this->config->getAssetsPath())
697 1
                );
698 1
            }
699
            $force_slash = true;
700 1
        }
701
702
        // force leading slash?
703
        if ($force_slash) {
704
            $path = '/' . ltrim($path, '/');
705
        }
706 1
707
        // get content and content type
708 1
        $content = Util\File::fileGetContents($filePath);
709 1
        list($type, $subtype) = Util\File::getMediaType($filePath);
710
711
        $file['filepath'] = $filePath;
712 1
        $file['path'] = $path;
713 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
714
        $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 1
720 1
        return $file;
721 1
    }
722 1
723
    /**
724 1
     * 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
    private function locateFile(string $path, ?string $remote_fallback = null): string
735
    {
736 1
        // in case of a remote file: save it locally and returns its path
737
        if (Util\Url::isUrl($path)) {
738
            $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 1
            // Google Fonts hack
744 1
            if (Util\Str::endsWith($urlPath, '/css') || Util\Str::endsWith($urlPath, '/css2')) {
745
                $extension = 'css';
746 1
            }
747 1
            $relativePath = Page::slugify(\sprintf(
748
                '%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 1
            // 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 1
                    }
761
                    if (false === $content = Util\File::fileGetContents($url, true)) {
762
                        throw new RuntimeException(\sprintf('Can\'t get content of file "%s".', $url));
763 1
                    }
764
                    if (\strlen($content) <= 1) {
765
                        throw new RuntimeException(\sprintf('File "%s" is empty.', $url));
766 1
                    }
767 1
                } 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
        if (Util\File::getFS()->exists($filePath)) {
790
            return $filePath;
791 1
        }
792
793
        // checks in each themes/<theme>/assets/
794
        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 1
        // 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
        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 1
    }
817 1
818 1
    /**
819
     * Returns the width of an image/SVG.
820
     *
821
     * @throws RuntimeException
822 1
     */
823
    private function getWidth(): int
824
    {
825
        if ($this->data['type'] != 'image') {
826
            return 0;
827
        }
828
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
829
            return (int) $svg->width;
830 1
        }
831
        if (false === $size = $this->getImageSize()) {
832 1
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
833
        }
834
835 1
        return $size[0];
836 1
    }
837
838 1
    /**
839
     * Returns the height of an image/SVG.
840
     *
841
     * @throws RuntimeException
842 1
     */
843
    private function getHeight(): int
844
    {
845
        if ($this->data['type'] != 'image') {
846
            return 0;
847
        }
848
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
849
            return (int) $svg->height;
850 1
        }
851
        if (false === $size = $this->getImageSize()) {
852 1
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
853
        }
854
855 1
        return $size[1];
856 1
    }
857
858 1
    /**
859
     * Returns image size informations.
860
     *
861
     * @see https://www.php.net/manual/function.getimagesize.php
862 1
     *
863
     * @return array|false
864
     */
865
    private function getImageSize()
866
    {
867
        if (!$this->data['type'] == 'image') {
868
            return false;
869
        }
870
871
        try {
872 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
873
                return false;
874 1
            }
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 1
    }
881
882
    /**
883
     * Replaces some characters by '_'.
884
     */
885
    private function sanitize(string $string): string
886 1
    {
887
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
888
    }
889
890
    /**
891
     * Builds CDN image URL.
892 1
     */
893
    private function buildImageCdnUrl(): string
894 1
    {
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