Passed
Pull Request — master (#2133)
by Arnaud
11:48 queued 05:10
created

Asset::locateFile()   D

Complexity

Conditions 20
Paths 50

Size

Total Lines 82
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 22.6367

Importance

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

391
        Optimizer::create(/** @scrutinizer ignore-type */ $quality)->optimize($filepath);
Loading history...
392 1
        $sizeAfter = filesize($filepath);
393 1
        if ($sizeAfter < $sizeBefore) {
394
            $message = \sprintf(
395
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
396
                $path,
397
                ceil($sizeBefore / 1000),
398
                ceil($sizeAfter / 1000)
399
            );
400
        }
401 1
        $this->builder->getLogger()->debug($message);
402
403 1
        return $sizeAfter;
404
    }
405
406
    /**
407
     * Resizes an image with a new $width.
408
     *
409
     * @throws RuntimeException
410
     */
411 1
    public function resize(int $width): self
412
    {
413 1
        if ($this->data['missing']) {
414
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
415
        }
416 1
        if ($this->data['type'] != 'image') {
417
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
418
        }
419 1
        if ($width >= $this->data['width']) {
420 1
            return $this;
421
        }
422
423 1
        $assetResized = clone $this;
424 1
        $assetResized->data['width'] = $width;
425
426 1
        if ($this->isImageInCdn()) {
427
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
428
        }
429
430 1
        $quality = $this->config->get('assets.images.quality');
431 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
432 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
433 1
        if (!$cache->has($cacheKey)) {
434 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

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

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