Passed
Push — feat/markdown-highlighter ( 1efac5...98434c )
by Arnaud
13:07 queued 08:45
created

Asset::findFile()   C

Complexity

Conditions 12
Paths 15

Size

Total Lines 52
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 12.1591

Importance

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