Passed
Push — master ( a38123...f59840 )
by Arnaud
05:15
created

Asset::minify()   C

Complexity

Conditions 12
Paths 14

Size

Total Lines 52
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 13.152

Importance

Changes 0
Metric Value
cc 12
eloc 34
c 0
b 0
f 0
nc 14
nop 0
dl 0
loc 52
ccs 28
cts 35
cp 0.8
crap 13.152
rs 6.9666

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\Url;
23
use Cecil\Util;
24
use MatthiasMullie\Minify;
25
use ScssPhp\ScssPhp\Compiler;
26
use ScssPhp\ScssPhp\OutputStyle;
27
use wapmorgan\Mp3Info\Mp3Info;
28
29
class Asset implements \ArrayAccess
30
{
31
    /** @var Builder */
32
    protected $builder;
33
34
    /** @var Config */
35
    protected $config;
36
37
    /** @var array */
38
    protected $data = [];
39
40
    /** @var bool */
41
    protected $fingerprinted = false;
42
43
    /** @var bool */
44
    protected $compiled = false;
45
46
    /** @var bool */
47
    protected $minified = false;
48
49
    /** @var bool */
50
    protected $ignore_missing = false;
51
52
    /**
53
     * Creates an Asset from a file path, an array of files path or an URL.
54
     *
55
     * @param Builder      $builder
56
     * @param string|array $paths
57
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
58
     *
59
     * @throws RuntimeException
60
     */
61 1
    public function __construct(Builder $builder, string|array $paths, array|null $options = null)
62
    {
63 1
        $this->builder = $builder;
64 1
        $this->config = $builder->getConfig();
65 1
        $paths = \is_array($paths) ? $paths : [$paths];
66 1
        array_walk($paths, function ($path) {
67 1
            if (!\is_string($path)) {
68
                throw new RuntimeException(\sprintf('The path of an asset must be a string ("%s" given).', \gettype($path)));
69
            }
70 1
            if (empty($path)) {
71
                throw new RuntimeException('The path of an asset can\'t be empty.');
72
            }
73 1
            if (substr($path, 0, 2) == '..') {
74
                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));
75
            }
76 1
        });
77 1
        $this->data = [
78 1
            'file'           => '',    // absolute file path
79 1
            'files'          => [],    // bundle: array of files path
80 1
            'filename'       => '',    // bundle: filename
81 1
            'path'           => '',    // public path to the file
82 1
            'url'            => null,  // URL if it's a remote file
83 1
            'missing'        => false, // if file not found but missing allowed: 'missing' is true
84 1
            'ext'            => '',    // file extension
85 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
86 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
87 1
            'size'           => 0,     // file size (in bytes)
88 1
            'width'          => 0,     // image width (in pixels)
89 1
            'height'         => 0,     // image height (in pixels)
90 1
            'exif'           => [],    // exif data
91 1
            'content'        => '',    // file content
92 1
        ];
93
94
        // handles options
95 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
96 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
97 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
98 1
        $filename = '';
99 1
        $ignore_missing = false;
100 1
        $remote_fallback = null;
101 1
        $force_slash = true;
102 1
        extract(\is_array($options) ? $options : [], EXTR_IF_EXISTS);
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 1
            $cache->set($cacheKey, $this->data);
169 1
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
170
            // optimizing images files
171 1
            if ($optimize && $this->data['type'] == 'image' && !$this->isImageInCdn()) {
172 1
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
173
            }
174
        }
175 1
        $this->data = $cache->get($cacheKey);
176
        // fingerprinting
177 1
        if ($fingerprint) {
178 1
            $this->fingerprint();
179
        }
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 1
        $this->save();
198
199 1
        if ($this->isImageInCdn()) {
200
            return $this->buildImageCdnUrl();
201
        }
202
203 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
204
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
205
        }
206
207 1
        return $this->data['path'];
208
    }
209
210
    /**
211
     * Add hash to the file name.
212
     */
213 1
    public function fingerprint(): self
214
    {
215 1
        if ($this->fingerprinted) {
216 1
            return $this;
217
        }
218
219 1
        $cache = new Cache($this->builder, 'assets');
220 1
        $cacheKey = $cache->createKeyFromAsset($this, ['fingerprint']);
221 1
        if (!$cache->has($cacheKey)) {
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 1
            $cache->set($cacheKey, $this->data);
229 1
            $this->builder->getLogger()->debug(\sprintf('Asset fingerprinted: "%s"', $this->data['path']));
230
        }
231 1
        $this->data = $cache->get($cacheKey);
232
233 1
        $this->fingerprinted = true;
234
235 1
        return $this;
236
    }
237
238
    /**
239
     * Compiles a SCSS.
240
     *
241
     * @throws RuntimeException
242
     */
243 1
    public function compile(): self
244
    {
245 1
        if ($this->compiled) {
246 1
            return $this;
247
        }
248
249 1
        if ($this->data['ext'] != 'scss') {
250 1
            return $this;
251
        }
252
253 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
254 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
255 1
        if (!$cache->has($cacheKey)) {
256 1
            $scssPhp = new Compiler();
257
            // import paths
258 1
            $importDir = [];
259 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
260 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
261 1
            $scssDir = (array) $this->config->get('assets.compile.import');
262 1
            $themes = $this->config->getTheme() ?? [];
263 1
            foreach ($scssDir as $dir) {
264 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
265 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
266 1
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
267 1
                foreach ($themes as $theme) {
268 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
269 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
270
                }
271
            }
272 1
            $scssPhp->setQuietDeps(true);
273 1
            $scssPhp->setImportPaths(array_unique($importDir));
274
            // source map
275 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
276
                $importDir = [];
277
                $assetDir = (string) $this->config->get('assets.dir');
278
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
279
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
280
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
281
                $importDir[] = \dirname($filePath);
282
                foreach ($scssDir as $dir) {
283
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
284
                }
285
                $scssPhp->setImportPaths(array_unique($importDir));
286
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
287
                $scssPhp->setSourceMapOptions([
288
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
289
                    'sourceRoot'        => '/',
290
                ]);
291
            }
292
            // output style
293 1
            $outputStyles = ['expanded', 'compressed'];
294 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
295 1
            if (!\in_array($outputStyle, $outputStyles)) {
296
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
297
            }
298 1
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
299
            // variables
300 1
            $variables = $this->config->get('assets.compile.variables');
301 1
            if (!empty($variables)) {
302 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
303 1
                $scssPhp->replaceVariables($variables);
304
            }
305
            // debug
306 1
            if ($this->builder->isDebug()) {
307 1
                $scssPhp->setQuietDeps(false);
308 1
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
309
            }
310
            // update data
311 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
312 1
            $this->data['ext'] = 'css';
313 1
            $this->data['type'] = 'text';
314 1
            $this->data['subtype'] = 'text/css';
315 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
316 1
            $this->data['size'] = \strlen($this->data['content']);
317 1
            $this->compiled = true;
318 1
            $cache->set($cacheKey, $this->data);
319 1
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
320
        }
321 1
        $this->data = $cache->get($cacheKey);
322
323 1
        return $this;
324
    }
325
326
    /**
327
     * Minifying a CSS or a JS.
328
     *
329
     * @throws RuntimeException
330
     */
331 1
    public function minify(): self
332
    {
333
        // disable minify to preserve inline source map
334 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
335
            return $this;
336
        }
337
338 1
        if ($this->minified) {
339
            return $this;
340
        }
341
342 1
        if ($this->data['ext'] == 'scss') {
343
            $this->compile();
344
        }
345
346 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
347
            return $this;
348
        }
349
350 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
351
            $this->minified = true;
352
353
            return $this;
354
        }
355
356 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
357 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
358 1
        if (!$cache->has($cacheKey)) {
359 1
            switch ($this->data['ext']) {
360 1
                case 'css':
361 1
                    $minifier = new Minify\CSS($this->data['content']);
362 1
                    break;
363 1
                case 'js':
364 1
                    $minifier = new Minify\JS($this->data['content']);
365 1
                    break;
366
                default:
367
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
368
            }
369 1
            $this->data['path'] = preg_replace(
370 1
                '/\.' . $this->data['ext'] . '$/m',
371 1
                '.min.' . $this->data['ext'],
372 1
                $this->data['path']
373 1
            );
374 1
            $this->data['content'] = $minifier->minify();
375 1
            $this->data['size'] = \strlen($this->data['content']);
376 1
            $this->minified = true;
377 1
            $cache->set($cacheKey, $this->data);
378 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
379
        }
380 1
        $this->data = $cache->get($cacheKey);
381
382 1
        return $this;
383
    }
384
385
    /**
386
     * Optimizing $filepath image.
387
     * Returns the new file size.
388
     */
389 1
    public function optimize(string $filepath, string $path): int
390
    {
391 1
        $quality = $this->config->get('assets.images.quality');
392 1
        $message = \sprintf('Asset processed: "%s"', $path);
393 1
        $sizeBefore = filesize($filepath);
394 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

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

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

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