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

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

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

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