Passed
Push — master ( 0b9670...5841ce )
by Arnaud
08:09
created

Asset::findFile()   D

Complexity

Conditions 22
Paths 54

Size

Total Lines 87
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 26.579

Importance

Changes 0
Metric Value
cc 22
eloc 51
c 0
b 0
f 0
nc 54
nop 2
dl 0
loc 87
ccs 41
cts 52
cp 0.7885
crap 26.579
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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