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

405
            Optimizer::create(/** @scrutinizer ignore-type */ $quality)->optimize($filepath);
Loading history...
406 1
            $sizeAfter = filesize($filepath);
407 1
            if ($sizeAfter < $sizeBefore) {
408
                $message = \sprintf(
409
                    '%s (%s Ko -> %s Ko)',
410
                    $message,
411
                    ceil($sizeBefore / 1000),
412
                    ceil($sizeAfter / 1000)
413
                );
414
            }
415 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
416 1
            $this->data['size'] = $sizeAfter;
417 1
            $cache->set($cacheKey, $this->data);
418 1
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
419
        }
420 1
        $this->data = $cache->get($cacheKey, $this->data);
421
422 1
        return $this;
423
    }
424
425
    /**
426
     * Resizes an image with a new $width.
427
     *
428
     * @throws RuntimeException
429
     */
430 1
    public function resize(int $width): self
431
    {
432 1
        if ($this->data['missing']) {
433
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
434
        }
435 1
        if ($this->data['type'] != 'image') {
436
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
437
        }
438 1
        if ($width >= $this->data['width']) {
439 1
            return $this;
440
        }
441
442 1
        $assetResized = clone $this;
443 1
        $assetResized->data['width'] = $width;
444
445 1
        if ($this->isImageInCdn()) {
446
            return $assetResized; // returns asset with the new width only: CDN do the rest of the job
447
        }
448
449 1
        $quality = $this->config->get('assets.images.quality');
450 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
451 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
452 1
        if (!$cache->has($cacheKey)) {
453 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

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

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