Passed
Pull Request — master (#1648)
by Arnaud
08:17 queued 03:11
created

Asset::__construct()   F

Complexity

Conditions 25
Paths 166

Size

Total Lines 131
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 80
CRAP Score 27.4605

Importance

Changes 8
Bugs 5 Features 1
Metric Value
cc 25
eloc 93
c 8
b 5
f 1
nc 166
nop 3
dl 0
loc 131
ccs 80
cts 95
cp 0.8421
crap 27.4605
rs 3.6166

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\RuntimeException;
21
use Cecil\Util;
22
use Intervention\Image\ImageManagerStatic as ImageManager;
23
use MatthiasMullie\Minify;
24
use ScssPhp\ScssPhp\Compiler;
25
use wapmorgan\Mp3Info\Mp3Info;
26
27
class Asset implements \ArrayAccess
28
{
29
    /** @var Builder */
30
    protected $builder;
31
32
    /** @var Config */
33
    protected $config;
34
35
    /** @var array */
36
    protected $data = [];
37
38
    /** @var bool */
39
    protected $fingerprinted = false;
40
41
    /** @var bool */
42
    protected $compiled = false;
43
44
    /** @var bool */
45
    protected $minified = false;
46
47
    /** @var bool */
48
    protected $optimize = false;
49
    /** @var bool */
50
    protected $optimized = false;
51
52
    /** @var bool */
53
    protected $ignore_missing = false;
54
55
    /**
56
     * Creates an Asset from a file path, an array of files path or an URL.
57
     *
58
     * @param Builder      $builder
59
     * @param string|array $paths
60
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
61
     *
62
     * @throws RuntimeException
63
     */
64 1
    public function __construct(Builder $builder, $paths, array $options = null)
65
    {
66 1
        $this->builder = $builder;
67 1
        $this->config = $builder->getConfig();
68 1
        $paths = is_array($paths) ? $paths : [$paths];
69 1
        array_walk($paths, function ($path) {
70 1
            if (!is_string($path)) {
71
                throw new RuntimeException(\sprintf('The path to an asset must be a string (%s given).', gettype($path)));
72
            }
73 1
            if (empty($path)) {
74
                throw new RuntimeException('The path to an asset can\'t be empty.');
75
            }
76 1
            if (substr($path, 0, 2) == '..') {
77
                throw new RuntimeException(\sprintf('The path to asset "%s" is wrong: it must be directly relative to "assets" or "static" directory, or a remote URL.', $path));
78
            }
79 1
        });
80 1
        $this->data = [
81 1
            'file'           => '',    // absolute file path
82 1
            'files'          => [],    // array of files path (if bundle)
83 1
            'filename'       => '',    // filename
84 1
            'path_source'    => '',    // public path to the file, before transformations
85 1
            'path'           => '',    // public path to the file, after transformations
86 1
            'url'            => null,  // URL of a remote image
87 1
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
88 1
            'ext'            => '',    // file extension
89 1
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
90 1
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
91 1
            'size'           => 0,     // file size (in bytes)
92 1
            'content_source' => '',    // file content, before transformations
93 1
            'content'        => '',    // file content, after transformations
94 1
            'width'          => 0,     // width (in pixels) in case of an image
95 1
            'height'         => 0,     // height (in pixels) in case of an image
96 1
            'exif'           => [],    // exif data
97 1
        ];
98
99
        // handles options
100 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
101 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
102 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
103 1
        $filename = '';
104 1
        $ignore_missing = false;
105 1
        $remote_fallback = null;
106 1
        $force_slash = true;
107 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
108 1
        $this->ignore_missing = $ignore_missing;
109
110
        // fill data array with file(s) informations
111 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
112 1
        $cacheKey = \sprintf('%s__%s', implode('_', $paths), $this->builder->getVersion());
113 1
        if (!$cache->has($cacheKey)) {
114 1
            $pathsCount = count($paths);
115 1
            $file = [];
116 1
            for ($i = 0; $i < $pathsCount; $i++) {
117
                // loads file(s)
118 1
                $file[$i] = $this->loadFile($paths[$i], $ignore_missing, $remote_fallback, $force_slash);
119
                // bundle: same type/ext only
120 1
                if ($i > 0) {
121 1
                    if ($file[$i]['type'] != $file[$i - 1]['type']) {
122
                        throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $file[$i]['type'], $file[$i - 1]['type']));
123
                    }
124 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
125
                        throw new RuntimeException(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
126
                    }
127
                }
128
                // missing allowed = empty path
129 1
                if ($file[$i]['missing']) {
130 1
                    $this->data['missing'] = true;
131 1
                    $this->data['path'] = $file[$i]['path'];
132
133 1
                    continue;
134
                }
135
                // set data
136 1
                $this->data['size'] += $file[$i]['size'];
137 1
                $this->data['content_source'] .= $file[$i]['content'];
138 1
                $this->data['content'] .= $file[$i]['content'];
139 1
                if ($i == 0) {
140 1
                    $this->data['file'] = $file[$i]['filepath'];
141 1
                    $this->data['filename'] = $file[$i]['path'];
142 1
                    $this->data['path_source'] = $file[$i]['path'];
143 1
                    $this->data['path'] = $file[$i]['path'];
144 1
                    if (!empty($filename)) { /** @phpstan-ignore-line */
145 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
146
                    }
147 1
                    $this->data['url'] = $file[$i]['url'];
148 1
                    $this->data['ext'] = $file[$i]['ext'];
149 1
                    $this->data['type'] = $file[$i]['type'];
150 1
                    $this->data['subtype'] = $file[$i]['subtype'];
151 1
                    if ($this->data['type'] == 'image') {
152 1
                        $this->data['width'] = $this->getWidth();
153 1
                        $this->data['height'] = $this->getHeight();
154 1
                        if ($this->data['subtype'] == 'jpeg') {
155
                            $this->data['exif'] = Util\File::readExif($file[$i]['filepath']);
156
                        }
157
                    }
158
                }
159
                // bundle files path
160 1
                $this->data['files'][] = $file[$i]['filepath'];
161
            }
162
            // bundle: define path
163 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
164
                switch ($this->data['ext']) {
165
                    case 'scss':
166
                    case 'css':
167
                        $this->data['path'] = '/styles.' . $file[0]['ext'];
168
                        break;
169
                    case 'js':
170
                        $this->data['path'] = '/scripts.' . $file[0]['ext'];
171
                        break;
172
                    default:
173
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
174
                }
175
            }
176 1
            $cache->set($cacheKey, $this->data);
177
        }
178 1
        $this->data = $cache->get($cacheKey);
179
180
        // fingerprinting
181 1
        if ($fingerprint) {
182 1
            $this->fingerprint();
183
        }
184
        // compiling (Sass files)
185 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
186 1
            $this->compile();
187
        }
188
        // minifying (CSS and JavScript files)
189 1
        if ($minify) {
190 1
            $this->minify();
191
        }
192
        // optimizing (images files)
193 1
        if ($optimize) {
194 1
            $this->optimize = true;
195
        }
196
    }
197
198
    /**
199
     * Returns path.
200
     *
201
     * @throws RuntimeException
202
     */
203 1
    public function __toString(): string
204
    {
205
        try {
206 1
            $this->save();
207
        } catch (\Exception $e) {
208
            $this->builder->getLogger()->error($e->getMessage());
209
        }
210
211 1
        if ($this->data['type'] == 'image' && (bool) $this->config->get('assets.images.cdn.enabled')) {
212 1
            return str_replace(
213 1
                [
214 1
                    '%account%',
215 1
                    '%image_url%',
216 1
                    '%width%',
217 1
                    '%quality%',
218 1
                    '%format%',
219 1
                    '%default%',
220 1
                ],
221 1
                [
222 1
                    $this->config->get('assets.images.cdn.account'),
223 1
                    $this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => true]),
224 1
                    $this->data['width'],
225 1
                    $this->config->get('assets.images.quality') ?? 75,
226 1
                    $this->data['ext'],
227 1
                    $this->config->get('assets.images.cdn.default') ?? 'default.jpg',
228 1
                ],
229 1
                (string) $this->config->get('assets.images.cdn.url')
230 1
            );
231
        }
232
233 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
234
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
235
        }
236
237 1
        return $this->data['path'];
238
    }
239
240
    /**
241
     * Fingerprints a file.
242
     */
243 1
    public function fingerprint(): self
244
    {
245 1
        if ($this->fingerprinted) {
246 1
            return $this;
247
        }
248
249 1
        $fingerprint = hash('md5', $this->data['content_source']);
250 1
        $this->data['path'] = preg_replace(
251 1
            '/\.' . $this->data['ext'] . '$/m',
252 1
            ".$fingerprint." . $this->data['ext'],
253 1
            $this->data['path']
254 1
        );
255
256 1
        $this->fingerprinted = true;
257
258 1
        return $this;
259
    }
260
261
    /**
262
     * Compiles a SCSS.
263
     *
264
     * @throws RuntimeException
265
     */
266 1
    public function compile(): self
267
    {
268 1
        if ($this->compiled) {
269 1
            return $this;
270
        }
271
272 1
        if ($this->data['ext'] != 'scss') {
273 1
            return $this;
274
        }
275
276 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
277 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
278 1
        if (!$cache->has($cacheKey)) {
279 1
            $scssPhp = new Compiler();
280 1
            $importDir = [];
281 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
282 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
283 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
284 1
            $themes = $this->config->getTheme() ?? [];
285 1
            foreach ($scssDir as $dir) {
286 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
287 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
288 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
289 1
                foreach ($themes as $theme) {
290 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
291 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
292
                }
293
            }
294 1
            $scssPhp->setImportPaths(array_unique($importDir));
295
            // source map
296 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
297
                $importDir = [];
298
                $assetDir = (string) $this->config->get('assets.dir');
299
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
300
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
301
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
302
                $importDir[] = dirname($filePath);
303
                foreach ($scssDir as $dir) {
304
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
305
                }
306
                $scssPhp->setImportPaths(array_unique($importDir));
307
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
308
                $scssPhp->setSourceMapOptions([
309
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
310
                    'sourceRoot'        => '/',
311
                ]);
312
            }
313
            // output style
314 1
            $outputStyles = ['expanded', 'compressed'];
315 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
316 1
            if (!in_array($outputStyle, $outputStyles)) {
317
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
318
            }
319 1
            $scssPhp->setOutputStyle($outputStyle);
320
            // variables
321 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
322 1
            if (!empty($variables)) {
323 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
324 1
                $scssPhp->replaceVariables($variables);
325
            }
326
            // update data
327 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
328 1
            $this->data['ext'] = 'css';
329 1
            $this->data['type'] = 'text';
330 1
            $this->data['subtype'] = 'text/css';
331 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
332 1
            $this->data['size'] = strlen($this->data['content']);
333 1
            $this->compiled = true;
334 1
            $cache->set($cacheKey, $this->data);
335
        }
336 1
        $this->data = $cache->get($cacheKey);
337
338 1
        return $this;
339
    }
340
341
    /**
342
     * Minifying a CSS or a JS.
343
     *
344
     * @throws RuntimeException
345
     */
346 1
    public function minify(): self
347
    {
348
        // disable minify to preserve inline source map
349 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
350
            return $this;
351
        }
352
353 1
        if ($this->minified) {
354 1
            return $this;
355
        }
356
357 1
        if ($this->data['ext'] == 'scss') {
358
            $this->compile();
359
        }
360
361 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
362 1
            return $this;
363
        }
364
365 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
366 1
            $this->minified;
367
368 1
            return $this;
369
        }
370
371 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
372 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
373 1
        if (!$cache->has($cacheKey)) {
374 1
            switch ($this->data['ext']) {
375 1
                case 'css':
376 1
                    $minifier = new Minify\CSS($this->data['content']);
377 1
                    break;
378 1
                case 'js':
379 1
                    $minifier = new Minify\JS($this->data['content']);
380 1
                    break;
381
                default:
382
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
383
            }
384 1
            $this->data['path'] = preg_replace(
385 1
                '/\.' . $this->data['ext'] . '$/m',
386 1
                '.min.' . $this->data['ext'],
387 1
                $this->data['path']
388 1
            );
389 1
            $this->data['content'] = $minifier->minify();
390 1
            $this->data['size'] = strlen($this->data['content']);
391 1
            $this->minified = true;
392 1
            $cache->set($cacheKey, $this->data);
393
        }
394 1
        $this->data = $cache->get($cacheKey);
395
396 1
        return $this;
397
    }
398
399
    /**
400
     * Optimizing an image.
401
     */
402 1
    public function optimize(string $filepath): self
403
    {
404 1
        if ($this->data['type'] != 'image') {
405 1
            return $this;
406
        }
407
408 1
        $quality = $this->config->get('assets.images.quality') ?? 75;
409 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
410 1
        $tags = ["q$quality", 'optimized'];
411 1
        if ($this->data['width']) {
412 1
            array_unshift($tags, "{$this->data['width']}x");
413
        }
414 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
415 1
        if (!$cache->has($cacheKey)) {
416 1
            $message = $this->data['path'];
417 1
            $sizeBefore = filesize($filepath);
418 1
            Optimizer::create($quality)->optimize($filepath);
419 1
            $sizeAfter = filesize($filepath);
420 1
            if ($sizeAfter < $sizeBefore) {
421
                $message = \sprintf(
422
                    '%s (%s Ko -> %s Ko)',
423
                    $message,
424
                    ceil($sizeBefore / 1000),
425
                    ceil($sizeAfter / 1000)
426
                );
427
            }
428 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
429 1
            $this->data['size'] = $sizeAfter;
430 1
            $cache->set($cacheKey, $this->data);
431 1
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
432
        }
433 1
        $this->data = $cache->get($cacheKey);
434 1
        $this->optimized = true;
435
436 1
        return $this;
437
    }
438
439
    /**
440
     * Resizes an image with a new $width.
441
     *
442
     * @throws RuntimeException
443
     */
444 1
    public function resize(int $width): self
445
    {
446 1
        if ($this->data['missing']) {
447
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
448
        }
449 1
        if ($this->data['type'] != 'image') {
450
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
451
        }
452 1
        if ($width >= $this->data['width']) {
453
            return $this;
454
        }
455
456 1
        $assetResized = clone $this;
457 1
        $assetResized->data['width'] = $width;
458
459 1
        $quality = $this->config->get('assets.images.quality');
460 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
461 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x", "q$quality"]);
462 1
        if (!$cache->has($cacheKey)) {
463 1
            if ($assetResized->data['type'] !== 'image') {
464
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
465
            }
466 1
            if (!extension_loaded('gd')) {
467
                throw new RuntimeException('GD extension is required to use images resize.');
468
            }
469
470
            try {
471 1
                $img = ImageManager::make($assetResized->data['content_source']);
472 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
473 1
                    $constraint->aspectRatio();
474 1
                    $constraint->upsize();
475 1
                });
476
            } catch (\Exception $e) {
477
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
478
            }
479 1
            $assetResized->data['path'] = '/' . Util::joinPath(
480 1
                (string) $this->config->get('assets.target'),
481 1
                (string) $this->config->get('assets.images.resize.dir'),
482 1
                (string) $width,
483 1
                $assetResized->data['path']
484 1
            );
485
486
            try {
487 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $quality);
488 1
                $assetResized->data['height'] = $assetResized->getHeight();
489 1
                $assetResized->data['size'] = strlen($assetResized->data['content']);
490
            } catch (\Exception $e) {
491
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
492
            }
493
494 1
            $cache->set($cacheKey, $assetResized->data);
495
        }
496 1
        $assetResized->data = $cache->get($cacheKey);
497
498 1
        return $assetResized;
499
    }
500
501
    /**
502
     * Converts an image asset to WebP format.
503
     *
504
     * @throws RuntimeException
505
     */
506 1
    public function webp(?int $quality = null): self
507
    {
508 1
        if ($this->data['type'] !== 'image') {
509
            throw new RuntimeException(\sprintf('can\'t convert "%s" (%s) to WebP: it\'s not an image file.', $this->data['path'], $this->data['type']));
510
        }
511
512 1
        if ($quality === null) {
513 1
            $quality = (int) $this->config->get('assets.images.quality') ?? 75;
514
        }
515
516 1
        $assetWebp = clone $this;
517 1
        $format = 'webp';
518 1
        $image = ImageManager::make($assetWebp['content']);
519 1
        $assetWebp['content'] = (string) $image->encode($format, $quality);
520
        $assetWebp['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
521
        $assetWebp['ext'] = $format;
522
        $assetWebp['subtype'] = "image/$format";
523
        $assetWebp['size'] = strlen($assetWebp['content']);
0 ignored issues
show
Bug introduced by
It seems like $assetWebp['content'] can also be of type null; however, parameter $string of strlen() does only seem to accept string, 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

523
        $assetWebp['size'] = strlen(/** @scrutinizer ignore-type */ $assetWebp['content']);
Loading history...
524
525
        return $assetWebp;
526
    }
527
528
    /**
529
     * Implements \ArrayAccess.
530
     */
531
    #[\ReturnTypeWillChange]
532
    public function offsetSet($offset, $value): void
533
    {
534
        if (!is_null($offset)) {
535
            $this->data[$offset] = $value;
536
        }
537
    }
538
539
    /**
540
     * Implements \ArrayAccess.
541
     */
542
    #[\ReturnTypeWillChange]
543 1
    public function offsetExists($offset): bool
544
    {
545 1
        return isset($this->data[$offset]);
546
    }
547
548
    /**
549
     * Implements \ArrayAccess.
550
     */
551
    #[\ReturnTypeWillChange]
552
    public function offsetUnset($offset): void
553
    {
554
        unset($this->data[$offset]);
555
    }
556
557
    /**
558
     * Implements \ArrayAccess.
559
     */
560
    #[\ReturnTypeWillChange]
561 1
    public function offsetGet($offset)
562
    {
563 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
564
    }
565
566
    /**
567
     * Hashing content of an asset with the specified algo, sha384 by default.
568
     * Used for SRI (Subresource Integrity).
569
     *
570
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
571
     */
572 1
    public function getIntegrity(string $algo = 'sha384'): string
573
    {
574 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
575
    }
576
577
    /**
578
     * Returns MP3 file infos.
579
     *
580
     * @see https://github.com/wapmorgan/Mp3Info
581
     */
582
    public function getAudio(): Mp3Info
583
    {
584
        if ($this->data['type'] !== 'audio') {
585
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
586
        }
587
588
        return new Mp3Info($this->data['file']);
589
    }
590
591
    /**
592
     * Returns the data URL of an image.
593
     *
594
     * @throws RuntimeException
595
     */
596 1
    public function dataurl(): string
597
    {
598 1
        if ($this->data['type'] !== 'image') {
599
            throw new RuntimeException(\sprintf('Can\'t get data URL of "%s"', $this->data['path']));
600
        }
601
602 1
        return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
603
    }
604
605
    /**
606
     * Saves file.
607
     * Note: a file from `static/` with the same name will NOT be overridden.
608
     *
609
     * @throws RuntimeException
610
     */
611 1
    public function save(): void
612
    {
613 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
614 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
615
            try {
616 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
617 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $this->data['path']));
618 1
                if ($this->optimize) {
619 1
                    $this->optimize($filepath);
620
                }
621
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
622
                if (!$this->ignore_missing) {
623
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
624
                }
625
            }
626
        }
627
    }
628
629
    /**
630
     * Load file data.
631
     *
632
     * @throws RuntimeException
633
     */
634 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
635
    {
636 1
        $file = [
637 1
            'url' => null,
638 1
        ];
639
640 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
641 1
            if ($ignore_missing) {
642 1
                $file['path'] = $path;
643 1
                $file['missing'] = true;
644
645 1
                return $file;
646
            }
647
648 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
649
        }
650
651 1
        if (Util\Url::isUrl($path)) {
652 1
            $file['url'] = $path;
653 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
654 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
655 1
            $force_slash = true;
656
        }
657 1
        if ($force_slash) {
658 1
            $path = '/' . ltrim($path, '/');
659
        }
660
661 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
662 1
        $content = Util\File::fileGetContents($filePath);
663
664 1
        $file['filepath'] = $filePath;
665 1
        $file['path'] = $path;
666 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
667 1
        $file['type'] = $type;
668 1
        $file['subtype'] = $subtype;
669 1
        $file['size'] = filesize($filePath);
670 1
        $file['content'] = $content;
671 1
        $file['missing'] = false;
672
673 1
        return $file;
674
    }
675
676
    /**
677
     * Try to find the file:
678
     *   1. remote (if $path is a valid URL)
679
     *   2. in static/
680
     *   3. in themes/<theme>/static/
681
     * Returns local file path or false if file don't exists.
682
     *
683
     * @throws RuntimeException
684
     *
685
     * @return string|false
686
     */
687 1
    private function findFile(string $path, ?string $remote_fallback = null)
688
    {
689
        // in case of remote file: save it and returns cached file path
690 1
        if (Util\Url::isUrl($path)) {
691 1
            $url = $path;
692 1
            $urlHost = parse_url($path, PHP_URL_HOST);
693 1
            $urlPath = parse_url($path, PHP_URL_PATH);
694 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
695 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
696
            // Google Fonts hack
697 1
            if (strpos($urlPath, '/css') !== false) {
698 1
                $extension = 'css';
699
            }
700 1
            $relativePath = Page::slugify(\sprintf(
701 1
                '%s%s%s%s',
702 1
                $urlHost,
703 1
                $this->sanitize($urlPath),
704 1
                $urlQuery ? "-$urlQuery" : '',
705 1
                $urlQuery && $extension ? " . $extension" : ''
706 1
            ));
707 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
708 1
            if (!file_exists($filePath)) {
709 1
                if (!Util\Url::isRemoteFileExists($url)) {
710
                    // is there a fallback in assets/
711 1
                    if ($remote_fallback) {
712 1
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
713 1
                        if (Util\File::getFS()->exists($filePath)) {
714
                            return $filePath;
715
                        }
716
                    }
717
718 1
                    return false;
719
                }
720 1
                if (false === $content = Util\File::fileGetContents($url, true)) {
721
                    return false;
722
                }
723 1
                if (strlen($content) <= 1) {
724
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
725
                }
726 1
                Util\File::getFS()->dumpFile($filePath, $content);
727
            }
728
729 1
            return $filePath;
730
        }
731
732
        // checks in assets/
733 1
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
734 1
        if (Util\File::getFS()->exists($filePath)) {
735 1
            return $filePath;
736
        }
737
738
        // checks in each themes/<theme>/assets/
739 1
        foreach ($this->config->getTheme() as $theme) {
740 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
741 1
            if (Util\File::getFS()->exists($filePath)) {
742
                return $filePath;
743
            }
744
        }
745
746
        // checks in static/
747 1
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
748 1
        if (Util\File::getFS()->exists($filePath)) {
749 1
            return $filePath;
750
        }
751
752
        // checks in each themes/<theme>/static/
753 1
        foreach ($this->config->getTheme() as $theme) {
754 1
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
755 1
            if (Util\File::getFS()->exists($filePath)) {
756 1
                return $filePath;
757
            }
758
        }
759
760 1
        return false;
761
    }
762
763
    /**
764
     * Returns the width of an image/SVG.
765
     *
766
     * @throws RuntimeException
767
     */
768 1
    private function getWidth(): int
769
    {
770 1
        if ($this->data['type'] != 'image') {
771
            return 0;
772
        }
773 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
774 1
            return (int) $svg->width;
775
        }
776 1
        if (false === $size = $this->getImageSize()) {
777
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
778
        }
779
780 1
        return $size[0];
781
    }
782
783
    /**
784
     * Returns the height of an image/SVG.
785
     *
786
     * @throws RuntimeException
787
     */
788 1
    private function getHeight(): int
789
    {
790 1
        if ($this->data['type'] != 'image') {
791
            return 0;
792
        }
793 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
794 1
            return (int) $svg->height;
795
        }
796 1
        if (false === $size = $this->getImageSize()) {
797
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
798
        }
799
800 1
        return $size[1];
801
    }
802
803
    /**
804
     * Returns image size informations.
805
     *
806
     * @see https://www.php.net/manual/function.getimagesize.php
807
     *
808
     * @return array|false
809
     */
810 1
    private function getImageSize()
811
    {
812 1
        if (!$this->data['type'] == 'image') {
813
            return false;
814
        }
815
816
        try {
817 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
818 1
                return false;
819
            }
820
        } catch (\Exception $e) {
821
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
822
        }
823
824 1
        return $size;
825
    }
826
827
    /**
828
     * Returns true if asset is a SVG.
829
     */
830 1
    private function isSVG(): bool
831
    {
832 1
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
833
    }
834
835
    /**
836
     * Returns SVG attributes.
837
     *
838
     * @return \SimpleXMLElement|false
839
     */
840 1
    private function getSvgAttributes()
841
    {
842 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
843
            return false;
844
        }
845
846 1
        return $xml->attributes();
847
    }
848
849
    /**
850
     * Replaces some characters by '_'.
851
     */
852 1
    private function sanitize(string $string): string
853
    {
854 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
855
    }
856
}
857