Passed
Push — asset-remote ( 3a1a12...5d4b72 )
by Arnaud
03:11
created

Asset::resize()   B

Complexity

Conditions 9
Paths 11

Size

Total Lines 53
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 10.5022

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 35
c 1
b 1
f 0
nc 11
nop 1
dl 0
loc 53
ccs 25
cts 34
cp 0.7352
crap 10.5022
rs 8.0555

How to fix   Long Method   

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
    protected $optimized = false;
50
51
    /** @var bool */
52
    protected $ignore_missing = false;
53
54
    /**
55
     * Creates an Asset from a file path or an array of files path.
56
     *
57
     * @param Builder      $builder
58
     * @param string|array $paths
59
     * @param array|null   $options e.g.: ['fingerprint' => true, 'minify' => true, 'filename' => '', 'ignore_missing' => false]
60
     *
61
     * @throws RuntimeException
62
     */
63 1
    public function __construct(Builder $builder, $paths, array $options = null)
64
    {
65 1
        $this->builder = $builder;
66 1
        $this->config = $builder->getConfig();
67 1
        $paths = is_array($paths) ? $paths : [$paths];
68 1
        array_walk($paths, function ($path) {
69 1
            if (empty($path)) {
70
                throw new RuntimeException('The path to an asset can\'t be empty.');
71
            }
72 1
            if (substr($path, 0, 2) == '..') {
73
                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));
74
            }
75
        });
76 1
        $this->data = [
77
            'file'           => '',    // absolute file path
78
            'files'          => [],    // bundle: files path
79
            'filename'       => '',    // filename
80
            'path_source'    => '',    // public path to the file, before transformations
81
            'path'           => '',    // public path to the file, after transformations
82
            'missing'        => false, // if file not found, but missing ollowed 'missing' is true
83
            'ext'            => '',    // file extension
84
            'type'           => '',    // file type (e.g.: image, audio, video, etc.)
85
            'subtype'        => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
86
            'size'           => 0,     // file size (in bytes)
87
            'content_source' => '',    // file content, before transformations
88
            'content'        => '',    // file content, after transformations
89
            'width'          => 0,     // width (in pixels) in case of an image
90
            'height'         => 0,     // height (in pixels) in case of an image
91
        ];
92
93
        // handles options
94 1
        $fingerprint = (bool) $this->config->get('assets.fingerprint.enabled');
95 1
        $minify = (bool) $this->config->get('assets.minify.enabled');
96 1
        $optimize = (bool) $this->config->get('assets.images.optimize.enabled');
97 1
        $filename = '';
98 1
        $ignore_missing = false;
99 1
        $remote_fallback = null;
100 1
        $force_slash = true;
101 1
        extract(is_array($options) ? $options : [], EXTR_IF_EXISTS);
102 1
        $this->ignore_missing = $ignore_missing;
103
104
        // fill data array with file(s) informations
105 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
106 1
        $cacheKey = \sprintf('%s__%s', 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/ext 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 1
                    if ($file[$i]['ext'] != $file[$i - 1]['ext']) {
119
                        throw new RuntimeException(\sprintf('Asset bundle extension error (%s != %s).', $file[$i]['ext'], $file[$i - 1]['ext']));
120
                    }
121
                }
122
                // missing allowed = empty path
123 1
                if ($file[$i]['missing']) {
124 1
                    $this->data['missing'] = true;
125 1
                    $this->data['path'] = $file[$i]['path'];
126
127 1
                    continue;
128
                }
129
                // set data
130 1
                $this->data['size'] += $file[$i]['size'];
131 1
                $this->data['content_source'] .= $file[$i]['content'];
132 1
                $this->data['content'] .= $file[$i]['content'];
133 1
                if ($i == 0) {
134 1
                    $this->data['file'] = $file[$i]['filepath'];
135 1
                    $this->data['filename'] = $file[$i]['path'];
136 1
                    $this->data['path_source'] = $file[$i]['path'];
137 1
                    $this->data['path'] = $file[$i]['path'];
138 1
                    if (!empty($filename)) { /** @phpstan-ignore-line */
139 1
                        $this->data['path'] = '/'.ltrim($filename, '/');
140
                    }
141 1
                    $this->data['ext'] = $file[$i]['ext'];
142 1
                    $this->data['type'] = $file[$i]['type'];
143 1
                    $this->data['subtype'] = $file[$i]['subtype'];
144 1
                    if ($this->data['type'] == 'image') {
145 1
                        $this->data['width'] = $this->getWidth();
146 1
                        $this->data['height'] = $this->getHeight();
147
                    }
148
                }
149
                // bundle files path
150 1
                $this->data['files'][] = $file[$i]['filepath'];
151
            }
152
            // bundle: define path
153 1
            if ($pathsCount > 1 && empty($filename)) { /** @phpstan-ignore-line */
154
                switch ($this->data['ext']) {
155
                    case 'scss':
156
                    case 'css':
157
                        $this->data['path'] = '/styles.'.$file[0]['ext'];
158
                        break;
159
                    case 'js':
160
                        $this->data['path'] = '/scripts.'.$file[0]['ext'];
161
                        break;
162
                    default:
163
                        throw new RuntimeException(\sprintf('Asset bundle supports "%s" files only.', '.scss, .css and .js'));
164
                }
165
            }
166 1
            $cache->set($cacheKey, $this->data);
167
        }
168 1
        $this->data = $cache->get($cacheKey);
169
170
        // fingerprinting
171 1
        if ($fingerprint) {
172 1
            $this->fingerprint();
173
        }
174
        // compiling
175 1
        if ((bool) $this->config->get('assets.compile.enabled')) {
176 1
            $this->compile();
177
        }
178
        // minifying
179 1
        if ($minify) {
180 1
            $this->minify();
181
        }
182
        // optimizing
183 1
        if ($optimize) {
184 1
            $this->optimize = true;
185
        }
186
    }
187
188
    /**
189
     * Returns path.
190
     *
191
     * @throws RuntimeException
192
     */
193 1
    public function __toString(): string
194
    {
195
        try {
196 1
            $this->save();
197
        } catch (\Exception $e) {
198
            $this->builder->getLogger()->error($e->getMessage());
199
        }
200
201 1
        if ($this->builder->getConfig()->get('canonicalurl')) {
202
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
203
        }
204
205 1
        return $this->data['path'];
206
    }
207
208
    /**
209
     * Fingerprints a file.
210
     */
211 1
    public function fingerprint(): self
212
    {
213 1
        if ($this->fingerprinted) {
214 1
            return $this;
215
        }
216
217 1
        $fingerprint = hash('md5', $this->data['content_source']);
218 1
        $this->data['path'] = preg_replace(
219 1
            '/\.'.$this->data['ext'].'$/m',
220 1
            ".$fingerprint.".$this->data['ext'],
221 1
            $this->data['path']
222
        );
223
224 1
        $this->fingerprinted = true;
225
226 1
        return $this;
227
    }
228
229
    /**
230
     * Compiles a SCSS.
231
     *
232
     * @throws RuntimeException
233
     */
234 1
    public function compile(): self
235
    {
236 1
        if ($this->compiled) {
237 1
            return $this;
238
        }
239
240 1
        if ($this->data['ext'] != 'scss') {
241 1
            return $this;
242
        }
243
244 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
245 1
        $cacheKey = $cache->createKeyFromAsset($this, ['compiled']);
246 1
        if (!$cache->has($cacheKey)) {
247 1
            $scssPhp = new Compiler();
248 1
            $importDir = [];
249 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
250 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
251 1
            $scssDir = $this->config->get('assets.compile.import') ?? [];
252 1
            $themes = $this->config->getTheme() ?? [];
253 1
            foreach ($scssDir as $dir) {
254 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
255 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
256 1
                $importDir[] = Util::joinPath(dirname($this->data['file']), $dir);
257 1
                foreach ($themes as $theme) {
258 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
259 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
260
                }
261
            }
262 1
            $scssPhp->setImportPaths(array_unique($importDir));
263
            // source map
264 1
            if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
265
                $importDir = [];
266
                $assetDir = (string) $this->config->get('assets.dir');
267
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR.$assetDir.DIRECTORY_SEPARATOR);
268
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
269
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
270
                $importDir[] = dirname($filePath);
271
                foreach ($scssDir as $dir) {
272
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
273
                }
274
                $scssPhp->setImportPaths(array_unique($importDir));
275
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
276
                $scssPhp->setSourceMapOptions([
277
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
278
                    'sourceRoot'        => '/',
279
                ]);
280
            }
281
            // output style
282 1
            $outputStyles = ['expanded', 'compressed'];
283 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
284 1
            if (!in_array($outputStyle, $outputStyles)) {
285
                throw new RuntimeException(\sprintf('Scss output style "%s" doesn\'t exists.', $outputStyle));
286
            }
287 1
            $scssPhp->setOutputStyle($outputStyle);
288
            // variables
289 1
            $variables = $this->config->get('assets.compile.variables') ?? [];
290 1
            if (!empty($variables)) {
291 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
292 1
                $scssPhp->replaceVariables($variables);
293
            }
294
            // update data
295 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
296 1
            $this->data['ext'] = 'css';
297 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
298 1
            $this->compiled = true;
299 1
            $cache->set($cacheKey, $this->data);
300
        }
301 1
        $this->data = $cache->get($cacheKey);
302
303 1
        return $this;
304
    }
305
306
    /**
307
     * Minifying a CSS or a JS.
308
     *
309
     * @throws RuntimeException
310
     */
311 1
    public function minify(): self
312
    {
313
        // disable minify to preserve inline source map
314 1
        if ($this->builder->isDebug() && (bool) $this->config->get('assets.compile.sourcemap')) {
315
            return $this;
316
        }
317
318 1
        if ($this->minified) {
319 1
            return $this;
320
        }
321
322 1
        if ($this->data['ext'] == 'scss') {
323
            $this->compile();
324
        }
325
326 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
327 1
            return $this;
328
        }
329
330 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
331 1
            $this->minified;
332
333 1
            return $this;
334
        }
335
336 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
337 1
        $cacheKey = $cache->createKeyFromAsset($this, ['minified']);
338 1
        if (!$cache->has($cacheKey)) {
339 1
            switch ($this->data['ext']) {
340 1
                case 'css':
341 1
                    $minifier = new Minify\CSS($this->data['content']);
342 1
                    break;
343 1
                case 'js':
344 1
                    $minifier = new Minify\JS($this->data['content']);
345 1
                    break;
346
                default:
347
                    throw new RuntimeException(\sprintf('Not able to minify "%s"', $this->data['path']));
348
            }
349 1
            $this->data['path'] = preg_replace(
350 1
                '/\.'.$this->data['ext'].'$/m',
351 1
                '.min.'.$this->data['ext'],
352 1
                $this->data['path']
353
            );
354 1
            $this->data['content'] = $minifier->minify();
355 1
            $this->minified = true;
356 1
            $cache->set($cacheKey, $this->data);
357
        }
358 1
        $this->data = $cache->get($cacheKey);
359
360 1
        return $this;
361
    }
362
363
    /**
364
     * Optimizing an image.
365
     */
366 1
    public function optimize(string $filepath): self
367
    {
368 1
        if ($this->data['type'] != 'image') {
369 1
            return $this;
370
        }
371
372 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
373 1
        $tags = ['optimized'];
374 1
        if ($this->data['width']) {
375 1
            array_unshift($tags, "{$this->data['width']}x");
376
        }
377 1
        $cacheKey = $cache->createKeyFromAsset($this, $tags);
378 1
        if (!$cache->has($cacheKey)) {
379 1
            $message = $this->data['path'];
380 1
            $sizeBefore = filesize($filepath);
381 1
            Optimizer::create($this->config->get('assets.images.quality') ?? 75)->optimize($filepath);
382 1
            $sizeAfter = filesize($filepath);
383 1
            if ($sizeAfter < $sizeBefore) {
384
                $message = \sprintf(
385
                    '%s (%s Ko -> %s Ko)',
386
                    $message,
387
                    ceil($sizeBefore / 1000),
388
                    ceil($sizeAfter / 1000)
389
                );
390
            }
391 1
            $this->data['content'] = Util\File::fileGetContents($filepath);
392 1
            $cache->set($cacheKey, $this->data);
393 1
            $this->builder->getLogger()->debug(\sprintf('Asset "%s" optimized', $message));
394
        }
395 1
        $this->data = $cache->get($cacheKey);
396 1
        $this->optimized = true;
397
398 1
        return $this;
399
    }
400
401
    /**
402
     * Resizes an image with a new $width.
403
     *
404
     * @throws RuntimeException
405
     */
406 1
    public function resize(int $width): self
407
    {
408 1
        if ($this->data['missing']) {
409
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found', $this->data['path']));
410
        }
411 1
        if ($this->data['type'] != 'image') {
412
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image', $this->data['path']));
413
        }
414 1
        if ($width >= $this->getWidth()) {
415
            return $this;
416
        }
417
418 1
        $assetResized = clone $this;
419 1
        $assetResized->data['width'] = $width;
420
421 1
        $cache = new Cache($this->builder, (string) $this->builder->getConfig()->get('cache.assets.dir'));
422 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, ["{$width}x"]);
423 1
        if (!$cache->has($cacheKey)) {
424 1
            if ($assetResized->data['type'] !== 'image') {
425
                throw new RuntimeException(\sprintf('Not able to resize "%s"', $assetResized->data['path']));
426
            }
427 1
            if (!extension_loaded('gd')) {
428
                throw new RuntimeException('GD extension is required to use images resize.');
429
            }
430
431
            try {
432 1
                $img = ImageManager::make($assetResized->data['content_source']);
433 1
                $img->resize($width, null, function (\Intervention\Image\Constraint $constraint) {
434 1
                    $constraint->aspectRatio();
435 1
                    $constraint->upsize();
436
                });
437
            } catch (\Exception $e) {
438
                throw new RuntimeException(\sprintf('Not able to resize image "%s": %s', $assetResized->data['path'], $e->getMessage()));
439
            }
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
            );
446
447
            try {
448 1
                $assetResized->data['content'] = (string) $img->encode($assetResized->data['ext'], $this->config->get('assets.images.quality'));
449 1
                $assetResized->data['height'] = $assetResized->getHeight();
450
            } catch (\Exception $e) {
451
                throw new RuntimeException(\sprintf('Not able to encode image "%s": %s', $assetResized->data['path'], $e->getMessage()));
452
            }
453
454 1
            $cache->set($cacheKey, $assetResized->data);
455
        }
456 1
        $assetResized->data = $cache->get($cacheKey);
457
458 1
        return $assetResized;
459
    }
460
461
    /**
462
     * Returns the data URL of an image.
463
     *
464
     * @throws RuntimeException
465
     */
466 1
    public function dataurl(): string
467
    {
468 1
        if ($this->data['type'] !== 'image') {
469
            throw new RuntimeException(\sprintf('Can\'t get data URL of "%s"', $this->data['path']));
470
        }
471
472 1
        return (string) ImageManager::make($this->data['content'])->encode('data-url', $this->config->get('assets.images.quality'));
473
    }
474
475
    /**
476
     * Implements \ArrayAccess.
477
     */
478
    #[\ReturnTypeWillChange]
479
    public function offsetSet($offset, $value): void
480
    {
481
        if (!is_null($offset)) {
482
            $this->data[$offset] = $value;
483
        }
484
    }
485
486
    /**
487
     * Implements \ArrayAccess.
488
     */
489
    #[\ReturnTypeWillChange]
490 1
    public function offsetExists($offset): bool
491
    {
492 1
        return isset($this->data[$offset]);
493
    }
494
495
    /**
496
     * Implements \ArrayAccess.
497
     */
498
    #[\ReturnTypeWillChange]
499
    public function offsetUnset($offset): void
500
    {
501
        unset($this->data[$offset]);
502
    }
503
504
    /**
505
     * Implements \ArrayAccess.
506
     */
507
    #[\ReturnTypeWillChange]
508 1
    public function offsetGet($offset)
509
    {
510 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
511
    }
512
513
    /**
514
     * Hashing content of an asset with the specified algo, sha384 by default.
515
     * Used for SRI (Subresource Integrity).
516
     *
517
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
518
     */
519 1
    public function getIntegrity(string $algo = 'sha384'): string
520
    {
521 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
522
    }
523
524
    /**
525
     * Returns the width of an image/SVG.
526
     *
527
     * @throws RuntimeException
528
     */
529 1
    public function getWidth(): int
530
    {
531 1
        if ($this->data['type'] != 'image') {
532
            return 0;
533
        }
534 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
535 1
            return (int) $svg->width;
536
        }
537 1
        if (false === $size = $this->getImageSize()) {
538
            throw new RuntimeException(\sprintf('Not able to get width of "%s"', $this->data['path']));
539
        }
540
541 1
        return $size[0];
542
    }
543
544
    /**
545
     * Returns the height of an image/SVG.
546
     *
547
     * @throws RuntimeException
548
     */
549 1
    public function getHeight(): int
550
    {
551 1
        if ($this->data['type'] != 'image') {
552
            return 0;
553
        }
554 1
        if ($this->isSVG() && false !== $svg = $this->getSvgAttributes()) {
555 1
            return (int) $svg->height;
556
        }
557 1
        if (false === $size = $this->getImageSize()) {
558
            throw new RuntimeException(\sprintf('Not able to get height of "%s"', $this->data['path']));
559
        }
560
561 1
        return $size[1];
562
    }
563
564
    /**
565
     * Returns MP3 file infos.
566
     *
567
     * @see https://github.com/wapmorgan/Mp3Info
568
     */
569
    public function getAudio(): Mp3Info
570
    {
571
        if ($this->data['type'] !== 'audio') {
572
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s"', $this->data['path']));
573
        }
574
575
        return new Mp3Info($this->data['file']);
576
    }
577
578
    /**
579
     * Saves file.
580
     * Note: a file from `static/` with the same name will NOT be overridden.
581
     *
582
     * @throws RuntimeException
583
     */
584 1
    public function save(): void
585
    {
586 1
        $filepath = Util::joinFile($this->config->getOutputPath(), $this->data['path']);
587 1
        if (!$this->builder->getBuildOptions()['dry-run'] && !Util\File::getFS()->exists($filepath)) {
588
            try {
589 1
                Util\File::getFS()->dumpFile($filepath, $this->data['content']);
590 1
                $this->builder->getLogger()->debug(\sprintf('Asset "%s" saved', $this->data['path']));
591 1
                if ($this->optimize) {
592 1
                    $this->optimize($filepath);
593
                }
594
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
595
                if (!$this->ignore_missing) {
596
                    throw new RuntimeException(\sprintf('Can\'t save asset "%s"', $filepath));
597
                }
598
            }
599
        }
600
    }
601
602
    /**
603
     * Load file data.
604
     *
605
     * @throws RuntimeException
606
     */
607 1
    private function loadFile(string $path, bool $ignore_missing = false, ?string $remote_fallback = null, bool $force_slash = true): array
608
    {
609 1
        $file = [];
610
611 1
        if (false === $filePath = $this->findFile($path, $remote_fallback)) {
612 1
            if ($ignore_missing) {
613 1
                $file['path'] = $path;
614 1
                $file['missing'] = true;
615
616 1
                return $file;
617
            }
618
619 1
            throw new RuntimeException(\sprintf('Asset file "%s" doesn\'t exist', $path));
620
        }
621
622 1
        if (Util\Url::isUrl($path)) {
623 1
            $path = Util\File::getFS()->makePathRelative($filePath, $this->config->getCacheAssetsRemotePath());
624 1
            $path = Util::joinPath((string) $this->config->get('assets.target'), $path);
625 1
            $force_slash = true;
626
        }
627 1
        if ($force_slash) {
628 1
            $path = '/'.ltrim($path, '/');
629
        }
630
631 1
        list($type, $subtype) = Util\File::getMimeType($filePath);
632 1
        $content = Util\File::fileGetContents($filePath);
633
634 1
        $file['filepath'] = $filePath;
635 1
        $file['path'] = $path;
636 1
        $file['ext'] = pathinfo($path)['extension'] ?? '';
637 1
        $file['type'] = $type;
638 1
        $file['subtype'] = $subtype;
639 1
        $file['size'] = filesize($filePath);
640 1
        $file['content'] = $content;
641 1
        $file['missing'] = false;
642
643 1
        return $file;
644
    }
645
646
    /**
647
     * Try to find the file:
648
     *   1. remote (if $path is a valid URL)
649
     *   2. in static/
650
     *   3. in themes/<theme>/static/
651
     * Returns local file path or false if file don't exists.
652
     *
653
     * @throws RuntimeException
654
     *
655
     * @return string|false
656
     */
657 1
    private function findFile(string $path, ?string $remote_fallback = null)
658
    {
659
        // in case of remote file: save it and returns cached file path
660 1
        if (Util\Url::isUrl($path)) {
661 1
            $url = $path;
662 1
            $urlHost = parse_url($path, PHP_URL_HOST);
663 1
            $urlPath = parse_url($path, PHP_URL_PATH);
664 1
            $urlQuery = parse_url($path, PHP_URL_QUERY);
665 1
            $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
666 1
            // Google Fonts hack
667
            if (strpos($urlPath, '/css') !== false) {
668
                $extension = 'css';
669 1
            }
670 1
            $relativePath = Page::slugify(\sprintf(
671 1
                '%s%s%s%s',
672
                $urlHost,
673 1
                $this->sanitize($urlPath),
674 1
                $urlQuery ? "-$urlQuery" : '',
675 1
                $urlQuery && $extension ? ".$extension" : ''
676
            ));
677 1
            $filePath = Util::joinFile($this->config->getCacheAssetsRemotePath(), $relativePath);
678
            if (!file_exists($filePath)) {
679
                if (!Util\Url::isRemoteFileExists($url)) {
680
                    // is there a fallback in assets/
681
                    if ($remote_fallback) {
682
                        $filePath = Util::joinFile($this->config->getAssetsPath(), $remote_fallback);
683
                        if (Util\File::getFS()->exists($filePath)) {
684 1
                            return $filePath;
685
                        }
686 1
                    }
687
688
                    return false;
689 1
                }
690
                if (false === $content = Util\File::fileGetContents($url, true)) {
691
                    return false;
692 1
                }
693
                if (strlen($content) <= 1) {
694
                    throw new RuntimeException(\sprintf('Asset at "%s" is empty', $url));
695 1
                }
696
                Util\File::getFS()->dumpFile($filePath, $content);
697
            }
698
699 1
            return $filePath;
700 1
        }
701 1
702
        // checks in assets/
703
        $filePath = Util::joinFile($this->config->getAssetsPath(), $path);
704
        if (Util\File::getFS()->exists($filePath)) {
705 1
            return $filePath;
706 1
        }
707 1
708
        // checks in each themes/<theme>/assets/
709
        foreach ($this->config->getTheme() as $theme) {
710
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
711
            if (Util\File::getFS()->exists($filePath)) {
712
                return $filePath;
713 1
            }
714 1
        }
715 1
716
        // checks in static/
717
        $filePath = Util::joinFile($this->config->getStaticTargetPath(), $path);
718
        if (Util\File::getFS()->exists($filePath)) {
719 1
            return $filePath;
720 1
        }
721 1
722 1
        // checks in each themes/<theme>/static/
723
        foreach ($this->config->getTheme() as $theme) {
724
            $filePath = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
725
            if (Util\File::getFS()->exists($filePath)) {
726 1
                return $filePath;
727
            }
728
        }
729
730
        return false;
731
    }
732
733
    /**
734
     * Returns image size informations.
735
     *
736 1
     * @see https://www.php.net/manual/function.getimagesize.php
737
     *
738 1
     * @return array|false
739
     */
740
    private function getImageSize()
741
    {
742
        if (!$this->data['type'] == 'image') {
743 1
            return false;
744 1
        }
745
746
        try {
747
            if (false === $size = getimagesizefromstring($this->data['content'])) {
748
                return false;
749
            }
750 1
        } catch (\Exception $e) {
751
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path_source'], $e->getMessage()));
752
        }
753
754
        return $size;
755
    }
756 1
757
    /**
758 1
     * Returns true if asset is a SVG.
759
     */
760
    private function isSVG(): bool
761
    {
762
        return in_array($this->data['subtype'], ['image/svg', 'image/svg+xml']) || $this->data['ext'] == 'svg';
763
    }
764
765
    /**
766 1
     * Returns SVG attributes.
767
     *
768 1
     * @return \SimpleXMLElement|false
769
     */
770
    private function getSvgAttributes()
771
    {
772 1
        if (false === $xml = simplexml_load_string($this->data['content_source'])) {
773
            return false;
774
        }
775
776
        return $xml->attributes();
777
    }
778 1
779
    /**
780 1
     * Replaces some characters by '_'.
781
     */
782
    private function sanitize(string $string): string
783
    {
784
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
785
    }
786
}
787