Passed
Pull Request — master (#2148)
by Arnaud
09:31 queued 04:12
created

Asset::compile()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 79
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 14.3653

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 12
eloc 57
c 5
b 0
f 0
nc 12
nop 0
dl 0
loc 79
ccs 44
cts 59
cp 0.7458
crap 14.3653
rs 6.5115

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Cecil\Assets;
15
16
use Cecil\Assets\Image\Optimizer;
17
use Cecil\Builder;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Config;
20
use Cecil\Exception\ConfigException;
21
use Cecil\Exception\RuntimeException;
22
use Cecil\Url;
23
use Cecil\Util;
24
use MatthiasMullie\Minify;
25
use ScssPhp\ScssPhp\Compiler;
26
use ScssPhp\ScssPhp\OutputStyle;
27
use wapmorgan\Mp3Info\Mp3Info;
28
29
class Asset implements \ArrayAccess
30
{
31
    /** @var Builder */
32
    protected $builder;
33
34
    /** @var Config */
35
    protected $config;
36
37
    /** @var array */
38
    protected $data = [];
39
40
    /** @var array Cache tags */
41
    protected $cacheTags = [];
42
43
    /**
44
     * Creates an Asset from a file path, an array of files path or an URL.
45
     * Options:
46
     * [
47
     *     'fingerprint' => <bool>,
48
     *     'minify' => <bool>,
49
     *     'optimize' => <bool>,
50
     *     'filename' => <string>,
51
     *     'ignore_missing' => <bool>,
52
     *     'fallback' => <string>,
53
     *     'leading_slash' => <bool>
54
     * ]
55
     *
56
     * @param Builder      $builder
57
     * @param string|array $paths
58
     * @param array|null   $options
59
     *
60
     * @throws RuntimeException
61
     */
62 1
    public function __construct(Builder $builder, string|array $paths, array|null $options = null)
63
    {
64 1
        $this->builder = $builder;
65 1
        $this->config = $builder->getConfig();
66 1
        $paths = \is_array($paths) ? $paths : [$paths];
67
        // checks path(s)
68 1
        array_walk($paths, function ($path) {
69
            // must be a string
70 1
            if (!\is_string($path)) {
71
                throw new RuntimeException(\sprintf('The path of an asset must be a string ("%s" given).', \gettype($path)));
72
            }
73
            // can't be empty
74 1
            if (empty($path)) {
75
                throw new RuntimeException('The path of an asset can\'t be empty.');
76
            }
77
            // can't be relative
78 1
            if (substr($path, 0, 2) == '..') {
79
                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));
80
            }
81 1
        });
82 1
        $this->data = [
83 1
            'file'     => '',    // absolute file path
84 1
            'files'    => [],    // array of absolute files path
85 1
            'path'     => '',    // public path
86 1
            '_path'    => '',    // original path
87 1
            'url'      => null,  // URL if it's a remote file
88 1
            'missing'  => false, // if file not found but missing allowed: 'missing' is true
89 1
            'ext'      => '',    // file extension
90 1
            'type'     => '',    // file type (e.g.: image, audio, video, etc.)
91 1
            'subtype'  => '',    // file media type (e.g.: image/png, audio/mp3, etc.)
92 1
            'size'     => 0,     // file size (in bytes)
93 1
            'width'    => 0,     // image width (in pixels)
94 1
            'height'   => 0,     // image height (in pixels)
95 1
            'exif'     => [],    // image exif data
96 1
            'content'  => '',    // file content
97 1
        ];
98
99
        // handles options
100 1
        $options = array_merge(
101 1
            [
102 1
                'fingerprint'    => $this->config->isEnabled('assets.fingerprint'),
103 1
                'minify'         => $this->config->isEnabled('assets.minify'),
104 1
                'optimize'       => $this->config->isEnabled('assets.images.optimize'),
105 1
                'filename'       => '',
106 1
                'ignore_missing' => false,
107 1
                'fallback'       => '',
108 1
                'leading_slash'  => true,
109 1
            ],
110 1
            \is_array($options) ? $options : []
111 1
        );
112
113
        // cache
114 1
        $cache = new Cache($this->builder, 'assets');
115 1
        $this->cacheTags = $options;
116 1
        $locateCacheKey = \sprintf('%s_locate__%s__%s', $options['filename'] ?: implode('_', $paths), $this->builder->getBuilId(), $this->builder->getVersion());
117
118
        // locate file(s) and get content
119 1
        if (!$cache->has($locateCacheKey)) {
120 1
            $pathsCount = \count($paths);
121 1
            for ($i = 0; $i < $pathsCount; $i++) {
122
                try {
123 1
                    $this->data['missing'] = false;
124 1
                    $locate = $this->locateFile($paths[$i], $options['fallback']);
125 1
                    $file = $locate['file'];
126 1
                    $path = $locate['path'];
127 1
                    $type = Util\File::getMediaType($file)[0];
128 1
                    if ($i > 0) { // bundle
129 1
                        if ($type != $this->data['type']) {
130
                            throw new RuntimeException(\sprintf('Asset bundle type error (%s != %s).', $type, $this->data['type']));
131
                        }
132
                    }
133 1
                    $this->data['file'] = $file;
134 1
                    $this->data['files'][] = $file;
135 1
                    $this->data['path'] = $path;
136 1
                    $this->data['url'] = Util\File::isRemote($paths[$i]) ? $paths[$i] : null;
137 1
                    $this->data['ext'] = Util\File::getExtension($file);
138 1
                    $this->data['type'] = $type;
139 1
                    $this->data['subtype'] = Util\File::getMediaType($file)[1];
140 1
                    $this->data['size'] += filesize($file);
141 1
                    $this->data['content'] .= Util\File::fileGetContents($file);
142
                    // bundle default filename
143 1
                    $filename = $options['filename'];
144 1
                    if ($pathsCount > 1 && empty($filename)) {
145 1
                        switch ($this->data['ext']) {
146 1
                            case 'scss':
147 1
                            case 'css':
148 1
                                $filename = 'styles.css';
149 1
                                break;
150 1
                            case 'js':
151 1
                                $filename = 'scripts.js';
152 1
                                break;
153
                            default:
154
                                throw new RuntimeException(\sprintf('Asset bundle supports %s files only.', '.scss, .css and .js'));
155
                        }
156
                    }
157
                    // apply bundle filename to path
158 1
                    if (!empty($filename)) {
159 1
                        $this->data['path'] = '/' . ltrim($filename, '/');
160
                    }
161
                    // force root slash
162 1
                    if ($options['leading_slash']) {
163 1
                        $this->data['path'] = '/' . ltrim($this->data['path'], '/');
164
                    }
165 1
                    $this->data['_path'] = $this->data['path'];
166 1
                } catch (RuntimeException $e) {
167 1
                    if ($options['ignore_missing']) {
168 1
                        $this->data['missing'] = true;
169 1
                        continue;
170
                    }
171
                    throw new RuntimeException(\sprintf('Can\'t handle asset "%s" (%s).', $paths[$i], $e->getMessage()));
172
                }
173
            }
174 1
            $cache->set($locateCacheKey, $this->data);
175
        }
176 1
        $this->data = $cache->get($locateCacheKey);
177
178
        // missing
179 1
        if ($this->data['missing']) {
180 1
            return;
181
        }
182
183
        // cache
184 1
        $cache = new Cache($this->builder, 'assets');
185 1
        $cacheKey = $cache->createKeyFromAsset($this, $this->cacheTags);
186 1
        if (!$cache->has($cacheKey)) {
187
            // image: width, height and exif
188 1
            if ($this->data['type'] == 'image') {
189 1
                $this->data['width'] = $this->getWidth();
190 1
                $this->data['height'] = $this->getHeight();
191 1
                if ($this->data['subtype'] == 'image/jpeg') {
192 1
                    $this->data['exif'] = Util\File::readExif($this->data['file']);
193
                }
194
            }
195
            // fingerprinting
196 1
            if ($options['fingerprint']) {
197
                $this->fingerprint();
198
            }
199
            // compiling Sass files
200 1
            $this->compile();
201
            // minifying (CSS and JavScript files)
202 1
            if ($options['minify']) {
203 1
                $this->minify();
204
            }
205 1
            $cache->set($cacheKey, $this->data);
206 1
            $this->builder->getLogger()->debug(\sprintf('Asset created: "%s"', $this->data['path']));
207
            // optimizing images files (in cache)
208 1
            if ($options['optimize'] && $this->data['type'] == 'image' && !$this->isImageInCdn()) {
209 1
                $this->optimize($cache->getContentFilePathname($this->data['path']), $this->data['path']);
210
            }
211
        }
212 1
        $this->data = $cache->get($cacheKey);
213
    }
214
215
    /**
216
     * Returns path.
217
     */
218 1
    public function __toString(): string
219
    {
220 1
        $this->save();
221
222 1
        if ($this->isImageInCdn()) {
223
            return $this->buildImageCdnUrl();
224
        }
225
226 1
        if ($this->builder->getConfig()->isEnabled('canonicalurl')) {
227
            return (string) new Url($this->builder, $this->data['path'], ['canonical' => true]);
228
        }
229
230 1
        return $this->data['path'];
231
    }
232
233
    /**
234
     * Compiles a SCSS.
235
     *
236
     * @throws RuntimeException
237
     */
238 1
    public function compile(): self
239
    {
240 1
        if ($this->data['ext'] != 'scss') {
241 1
            return $this;
242
        }
243
244 1
        $cache = new Cache($this->builder, 'assets');
245
246 1
        $this->cacheTags['compile'] = true;
247
248 1
        $cacheKey = $cache->createKeyFromAsset($this, $this->cacheTags);
249 1
        if (!$cache->has($cacheKey)) {
250 1
            $scssPhp = new Compiler();
251
            // import paths
252 1
            $importDir = [];
253 1
            $importDir[] = Util::joinPath($this->config->getStaticPath());
254 1
            $importDir[] = Util::joinPath($this->config->getAssetsPath());
255 1
            $scssDir = (array) $this->config->get('assets.compile.import');
256 1
            $themes = $this->config->getTheme() ?? [];
257 1
            foreach ($scssDir as $dir) {
258 1
                $importDir[] = Util::joinPath($this->config->getStaticPath(), $dir);
259 1
                $importDir[] = Util::joinPath($this->config->getAssetsPath(), $dir);
260 1
                $importDir[] = Util::joinPath(\dirname($this->data['file']), $dir);
261 1
                foreach ($themes as $theme) {
262 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "static/$dir"));
263 1
                    $importDir[] = Util::joinPath($this->config->getThemeDirPath($theme, "assets/$dir"));
264
                }
265
            }
266 1
            $scssPhp->setQuietDeps(true);
267 1
            $scssPhp->setImportPaths(array_unique($importDir));
268
            // source map
269 1
            if ($this->builder->isDebug() && $this->config->isEnabled('assets.compile.sourcemap')) {
270
                $importDir = [];
271
                $assetDir = (string) $this->config->get('assets.dir');
272
                $assetDirPos = strrpos($this->data['file'], DIRECTORY_SEPARATOR . $assetDir . DIRECTORY_SEPARATOR);
273
                $fileRelPath = substr($this->data['file'], $assetDirPos + 8);
274
                $filePath = Util::joinFile($this->config->getOutputPath(), $fileRelPath);
275
                $importDir[] = \dirname($filePath);
276
                foreach ($scssDir as $dir) {
277
                    $importDir[] = Util::joinFile($this->config->getOutputPath(), $dir);
278
                }
279
                $scssPhp->setImportPaths(array_unique($importDir));
280
                $scssPhp->setSourceMap(Compiler::SOURCE_MAP_INLINE);
281
                $scssPhp->setSourceMapOptions([
282
                    'sourceMapBasepath' => Util::joinPath($this->config->getOutputPath()),
283
                    'sourceRoot'        => '/',
284
                ]);
285
            }
286
            // output style
287 1
            $outputStyles = ['expanded', 'compressed'];
288 1
            $outputStyle = strtolower((string) $this->config->get('assets.compile.style'));
289 1
            if (!\in_array($outputStyle, $outputStyles)) {
290
                throw new ConfigException(\sprintf('"%s" value must be "%s".', 'assets.compile.style', implode('" or "', $outputStyles)));
291
            }
292 1
            $scssPhp->setOutputStyle($outputStyle == 'compressed' ? OutputStyle::COMPRESSED : OutputStyle::EXPANDED);
293
            // variables
294 1
            $variables = $this->config->get('assets.compile.variables');
295 1
            if (!empty($variables)) {
296 1
                $variables = array_map('ScssPhp\ScssPhp\ValueConverter::parseValue', $variables);
297 1
                $scssPhp->replaceVariables($variables);
298
            }
299
            // debug
300 1
            if ($this->builder->isDebug()) {
301 1
                $scssPhp->setQuietDeps(false);
302 1
                $this->builder->getLogger()->debug(\sprintf("SCSS compiler imported paths:\n%s", Util\Str::arrayToList(array_unique($importDir))));
303
            }
304
            // update data
305 1
            $this->data['path'] = preg_replace('/sass|scss/m', 'css', $this->data['path']);
306 1
            $this->data['ext'] = 'css';
307 1
            $this->data['type'] = 'text';
308 1
            $this->data['subtype'] = 'text/css';
309 1
            $this->data['content'] = $scssPhp->compileString($this->data['content'])->getCss();
310 1
            $this->data['size'] = \strlen($this->data['content']);
311 1
            $cache->set($cacheKey, $this->data);
312 1
            $this->builder->getLogger()->debug(\sprintf('Asset compiled: "%s"', $this->data['path']));
313
        }
314 1
        $this->data = $cache->get($cacheKey);
315
316 1
        return $this;
317
    }
318
319
    /**
320
     * Minifying a CSS or a JS.
321
     *
322
     * @throws RuntimeException
323
     */
324 1
    public function minify(): self
325
    {
326
        // in debug mode, disable minify to preserve inline source map
327 1
        if ($this->builder->isDebug() && $this->config->isEnabled('assets.compile.sourcemap')) {
328
            return $this;
329
        }
330
331 1
        if ($this->data['ext'] != 'css' && $this->data['ext'] != 'js') {
332
            return $this;
333
        }
334
335 1
        if (substr($this->data['path'], -8) == '.min.css' || substr($this->data['path'], -7) == '.min.js') {
336
            return $this;
337
        }
338
339 1
        if ($this->data['ext'] == 'scss') {
340
            $this->compile();
341
        }
342
343 1
        $cache = new Cache($this->builder, 'assets');
344
345 1
        $this->cacheTags['minify'] = true;
346
347 1
        $cacheKey = $cache->createKeyFromAsset($this, $this->cacheTags);
348 1
        if (!$cache->has($cacheKey)) {
349 1
            switch ($this->data['ext']) {
350 1
                case 'css':
351 1
                    $minifier = new Minify\CSS($this->data['content']);
352 1
                    break;
353 1
                case 'js':
354 1
                    $minifier = new Minify\JS($this->data['content']);
355 1
                    break;
356
                default:
357
                    throw new RuntimeException(\sprintf('Not able to minify "%s".', $this->data['path']));
358
            }
359 1
            $this->data['content'] = $minifier->minify();
360 1
            $this->data['size'] = \strlen($this->data['content']);
361 1
            $cache->set($cacheKey, $this->data);
362 1
            $this->builder->getLogger()->debug(\sprintf('Asset minified: "%s"', $this->data['path']));
363
        }
364 1
        $this->data = $cache->get($cacheKey);
365
366 1
        return $this;
367
    }
368
369
    /**
370
     * Add hash to the file name.
371
     */
372 1
    public function fingerprint(): self
373
    {
374 1
        $cache = new Cache($this->builder, 'assets');
375
376 1
        $this->cacheTags['fingerprint'] = true;
377
378 1
        $cacheKey = $cache->createKeyFromAsset($this, $this->cacheTags);
379 1
        if (!$cache->has($cacheKey)) {
380 1
            $hash = hash('md5', $this->data['content']);
381 1
            $this->data['path'] = preg_replace(
382 1
                '/\.' . $this->data['ext'] . '$/m',
383 1
                ".$hash." . $this->data['ext'],
384 1
                $this->data['path']
385 1
            );
386 1
            $cache->set($cacheKey, $this->data);
387 1
            $this->builder->getLogger()->debug(\sprintf('Asset fingerprinted: "%s"', $this->data['path']));
388
        }
389 1
        $this->data = $cache->get($cacheKey);
390
391 1
        return $this;
392
    }
393
394
    /**
395
     * Optimizing $filepath image.
396
     * Returns the new file size.
397
     */
398 1
    public function optimize(string $filepath, string $path): int
399
    {
400 1
        $quality = (int) $this->config->get('assets.images.quality');
401 1
        $message = \sprintf('Asset processed: "%s"', $path);
402 1
        $sizeBefore = filesize($filepath);
403 1
        Optimizer::create($quality)->optimize($filepath);
404 1
        $sizeAfter = filesize($filepath);
405 1
        if ($sizeAfter < $sizeBefore) {
406
            $message = \sprintf(
407
                'Asset optimized: "%s" (%s Ko -> %s Ko)',
408
                $path,
409
                ceil($sizeBefore / 1000),
410
                ceil($sizeAfter / 1000)
411
            );
412
        }
413 1
        $this->builder->getLogger()->debug($message);
414
415 1
        return $sizeAfter;
416
    }
417
418
    /**
419
     * Resizes an image with a new $width.
420
     *
421
     * @throws RuntimeException
422
     */
423 1
    public function resize(int $width): self
424
    {
425 1
        if ($this->data['missing']) {
426
            throw new RuntimeException(\sprintf('Not able to resize "%s": file not found.', $this->data['path']));
427
        }
428 1
        if ($this->data['type'] != 'image') {
429
            throw new RuntimeException(\sprintf('Not able to resize "%s": not an image.', $this->data['path']));
430
        }
431 1
        if ($width >= $this->data['width']) {
432 1
            return $this;
433
        }
434
435 1
        $assetResized = clone $this;
436 1
        $assetResized->data['width'] = $width;
437
438 1
        if ($this->isImageInCdn()) {
439
            $assetResized->data['height'] = round($this->data['height'] / ($this->data['width'] / $width));
440
441
            return $assetResized; // returns asset with the new dimensions only: CDN do the rest of the job
442
        }
443
444 1
        $quality = (int) $this->config->get('assets.images.quality');
445
446 1
        $cache = new Cache($this->builder, 'assets');
447 1
        $this->cacheTags['quality'] = $quality;
448 1
        $this->cacheTags['width'] = $width;
449 1
        $cacheKey = $cache->createKeyFromAsset($assetResized, $this->cacheTags);
450 1
        if (!$cache->has($cacheKey)) {
451 1
            $assetResized->data['content'] = Image::resize($assetResized, $width, $quality);
452 1
            $assetResized->data['path'] = '/' . Util::joinPath(
453 1
                (string) $this->config->get('assets.target'),
454 1
                'thumbnails',
455 1
                (string) $width,
456 1
                $assetResized->data['path']
457 1
            );
458 1
            $assetResized->data['height'] = $assetResized->getHeight();
459 1
            $assetResized->data['size'] = \strlen($assetResized->data['content']);
460
461 1
            $cache->set($cacheKey, $assetResized->data);
462 1
            $this->builder->getLogger()->debug(\sprintf('Asset resized: "%s" (%sx)', $assetResized->data['path'], $width));
463
        }
464 1
        $assetResized->data = $cache->get($cacheKey);
465
466 1
        return $assetResized;
467
    }
468
469
    /**
470
     * Converts an image asset to $format format.
471
     *
472
     * @throws RuntimeException
473
     */
474 1
    public function convert(string $format, ?int $quality = null): self
475
    {
476 1
        if ($this->data['type'] != 'image') {
477
            throw new RuntimeException(\sprintf('Not able to convert "%s" (%s) to %s: not an image.', $this->data['path'], $this->data['type'], $format));
478
        }
479
480 1
        if ($quality === null) {
481 1
            $quality = (int) $this->config->get('assets.images.quality');
482
        }
483
484 1
        $asset = clone $this;
485 1
        $asset['ext'] = $format;
486 1
        $asset->data['subtype'] = "image/$format";
487
488 1
        if ($this->isImageInCdn()) {
489
            return $asset; // returns the asset with the new extension only: CDN do the rest of the job
490
        }
491
492 1
        $cache = new Cache($this->builder, 'assets');
493 1
        $this->cacheTags['quality'] = $quality;
494 1
        if ($this->data['width']) {
495 1
            $this->cacheTags['width'] = $this->data['width'];
496
        }
497 1
        $cacheKey = $cache->createKeyFromAsset($asset, $this->cacheTags);
498 1
        if (!$cache->has($cacheKey)) {
499 1
            $asset->data['content'] = Image::convert($asset, $format, $quality);
500
            $asset->data['path'] = preg_replace('/\.' . $this->data['ext'] . '$/m', ".$format", $this->data['path']);
501
            $asset->data['size'] = \strlen($asset->data['content']);
502
            $cache->set($cacheKey, $asset->data);
503
            $this->builder->getLogger()->debug(\sprintf('Asset converted: "%s" (%s -> %s)', $asset->data['path'], $this->data['ext'], $format));
504
        }
505
        $asset->data = $cache->get($cacheKey);
506
507
        return $asset;
508
    }
509
510
    /**
511
     * Converts an image asset to WebP format.
512
     *
513
     * @throws RuntimeException
514
     */
515
    public function webp(?int $quality = null): self
516
    {
517
        return $this->convert('webp', $quality);
518
    }
519
520
    /**
521
     * Converts an image asset to AVIF format.
522
     *
523
     * @throws RuntimeException
524
     */
525 1
    public function avif(?int $quality = null): self
526
    {
527 1
        return $this->convert('avif', $quality);
528
    }
529
530
    /**
531
     * Implements \ArrayAccess.
532
     */
533 1
    #[\ReturnTypeWillChange]
534
    public function offsetSet($offset, $value): void
535
    {
536 1
        if (!\is_null($offset)) {
537 1
            $this->data[$offset] = $value;
538
        }
539
    }
540
541
    /**
542
     * Implements \ArrayAccess.
543
     */
544 1
    #[\ReturnTypeWillChange]
545
    public function offsetExists($offset): bool
546
    {
547 1
        return isset($this->data[$offset]);
548
    }
549
550
    /**
551
     * Implements \ArrayAccess.
552
     */
553
    #[\ReturnTypeWillChange]
554
    public function offsetUnset($offset): void
555
    {
556
        unset($this->data[$offset]);
557
    }
558
559
    /**
560
     * Implements \ArrayAccess.
561
     */
562 1
    #[\ReturnTypeWillChange]
563
    public function offsetGet($offset)
564
    {
565 1
        return isset($this->data[$offset]) ? $this->data[$offset] : null;
566
    }
567
568
    /**
569
     * Hashing content of an asset with the specified algo, sha384 by default.
570
     * Used for SRI (Subresource Integrity).
571
     *
572
     * @see https://developer.mozilla.org/fr/docs/Web/Security/Subresource_Integrity
573
     */
574 1
    public function getIntegrity(string $algo = 'sha384'): string
575
    {
576 1
        return \sprintf('%s-%s', $algo, base64_encode(hash($algo, $this->data['content'], true)));
577
    }
578
579
    /**
580
     * Returns MP3 file infos.
581
     *
582
     * @see https://github.com/wapmorgan/Mp3Info
583
     */
584 1
    public function getAudio(): Mp3Info
585
    {
586 1
        if ($this->data['type'] !== 'audio') {
587
            throw new RuntimeException(\sprintf('Not able to get audio infos of "%s".', $this->data['path']));
588
        }
589
590 1
        return new Mp3Info($this->data['file']);
591
    }
592
593
    /**
594
     * Returns MP4 file infos.
595
     *
596
     * @see https://github.com/clwu88/php-read-mp4info
597
     */
598 1
    public function getVideo(): array
599
    {
600 1
        if ($this->data['type'] !== 'video') {
601
            throw new RuntimeException(\sprintf('Not able to get video infos of "%s".', $this->data['path']));
602
        }
603
604 1
        return (array) \Clwu\Mp4::getInfo($this->data['file']);
605
    }
606
607
    /**
608
     * Returns the Data URL (encoded in Base64).
609
     *
610
     * @throws RuntimeException
611
     */
612 1
    public function dataurl(): string
613
    {
614 1
        if ($this->data['type'] == 'image' && !Image::isSVG($this)) {
615 1
            return Image::getDataUrl($this, (int) $this->config->get('assets.images.quality'));
616
        }
617
618 1
        return \sprintf('data:%s;base64,%s', $this->data['subtype'], base64_encode($this->data['content']));
619
    }
620
621
    /**
622
     * Adds asset path to the list of assets to save.
623
     *
624
     * @throws RuntimeException
625
     */
626 1
    public function save(): void
627
    {
628 1
        if ($this->data['missing']) {
629 1
            return;
630
        }
631
632 1
        $cache = new Cache($this->builder, 'assets');
633 1
        if (!Util\File::getFS()->exists($cache->getContentFilePathname($this->data['path']))) {
634
            throw new RuntimeException(
635
                \sprintf('Can\'t add "%s" to assets list. Please clear cache and retry.', $this->data['path'])
636
            );
637
        }
638
639 1
        $this->builder->addAsset($this->data['path']);
640
    }
641
642
    /**
643
     * Is the asset an image and is it in CDN?
644
     */
645 1
    public function isImageInCdn(): bool
646
    {
647
        if (
648 1
            $this->data['type'] == 'image'
649 1
            && $this->config->isEnabled('assets.images.cdn')
650 1
            && $this->data['ext'] != 'ico'
651 1
            && (Image::isSVG($this) && $this->config->isEnabled('assets.images.cdn.svg'))
652
        ) {
653
            return true;
654
        }
655
        // handle remote image?
656 1
        if ($this->data['url'] !== null && $this->config->isEnabled('assets.images.cdn.remote')) {
657
            return true;
658
        }
659
660 1
        return false;
661
    }
662
663
    /**
664
     * Builds a relative path from a URL.
665
     * Used for remote files.
666
     */
667 1
    public static function buildPathFromUrl(string $url): string
668
    {
669 1
        $host = parse_url($url, PHP_URL_HOST);
670 1
        $path = parse_url($url, PHP_URL_PATH);
671 1
        $query = parse_url($url, PHP_URL_QUERY);
672 1
        $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
673
674
        // Google Fonts hack
675 1
        if (Util\Str::endsWith($path, '/css') || Util\Str::endsWith($path, '/css2')) {
676 1
            $ext = 'css';
677
        }
678
679 1
        return Page::slugify(\sprintf('%s%s%s%s', $host, self::sanitize($path), $query ? "-$query" : '', $query && $ext ? ".$ext" : ''));
680
    }
681
682
    /**
683
     * Replaces some characters by '_'.
684
     */
685 1
    public static function sanitize(string $string): string
686
    {
687 1
        return str_replace(['<', '>', ':', '"', '\\', '|', '?', '*'], '_', $string);
688
    }
689
690
    /**
691
     * Returns local file path and updated path, or throw an exception.
692
     *
693
     * Try to locate the file in:
694
     *   (1. remote file)
695
     *   1. assets
696
     *   2. themes/<theme>/assets
697
     *   3. static
698
     *   4. themes/<theme>/static
699
     *
700
     * If $fallback is set, it will be used if the file is not found.
701
     *
702
     * @throws RuntimeException
703
     */
704 1
    private function locateFile(string $path, ?string $fallback = null): array
705
    {
706
        // remote file
707 1
        if (Util\File::isRemote($path)) {
708
            try {
709 1
                $content = $this->getRemoteFileContent($path);
710 1
                $path = self::buildPathFromUrl($path);
711 1
                $cache = new Cache($this->builder, 'assets/remote');
712 1
                if (!$cache->has($path)) {
713 1
                    $cache->set($path, [
714 1
                        'content' => $content,
715 1
                        'path'    => $path,
716 1
                    ], \DateInterval::createFromDateString('7 days'));
717
                }
718 1
                return [
719 1
                    'file' => $cache->getContentFilePathname($path),
720 1
                    'path' => $path,
721 1
                ];
722 1
            } catch (RuntimeException $e) {
723 1
                if ($fallback === null) {
724
                    throw new RuntimeException($e->getMessage(), previous: $e);
725
                }
726 1
                $path = $fallback;
727
            }
728
        }
729
730
        // checks in assets/
731 1
        $file = Util::joinFile($this->config->getAssetsPath(), $path);
732 1
        if (Util\File::getFS()->exists($file)) {
733 1
            return [
734 1
                'file' => $file,
735 1
                'path' => $path,
736 1
            ];
737
        }
738
739
        // checks in each themes/<theme>/assets/
740 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
741 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'assets'), $path);
742 1
            if (Util\File::getFS()->exists($file)) {
743 1
                return [
744 1
                    'file' => $file,
745 1
                    'path' => $path,
746 1
                ];
747
            }
748
        }
749
750
        // checks in static/
751 1
        $file = Util::joinFile($this->config->getStaticTargetPath(), $path);
752 1
        if (Util\File::getFS()->exists($file)) {
753 1
            return [
754 1
                'file' => $file,
755 1
                'path' => $path,
756 1
            ];
757
        }
758
759
        // checks in each themes/<theme>/static/
760 1
        foreach ($this->config->getTheme() ?? [] as $theme) {
761 1
            $file = Util::joinFile($this->config->getThemeDirPath($theme, 'static'), $path);
762 1
            if (Util\File::getFS()->exists($file)) {
763 1
                return [
764 1
                    'file' => $file,
765 1
                    'path' => $path,
766 1
                ];
767
            }
768
        }
769
770 1
        throw new RuntimeException(\sprintf('Can\'t locate file "%s".', $path));
771
    }
772
773
    /**
774
     * Try to get remote file content.
775
     * Returns file content or throw an exception.
776
     *
777
     * @throws RuntimeException
778
     */
779 1
    private function getRemoteFileContent(string $path): string
780
    {
781
        try {
782 1
            if (!Util\File::isRemoteExists($path)) {
783 1
                throw new RuntimeException(\sprintf('Remote file "%s" doesn\'t exists', $path));
784
            }
785 1
            if (false === $content = Util\File::fileGetContents($path, true)) {
786
                throw new RuntimeException(\sprintf('Can\'t get content of remote file "%s".', $path));
787
            }
788 1
            if (\strlen($content) <= 1) {
789 1
                throw new RuntimeException(\sprintf('Remote file "%s" is empty.', $path));
790
            }
791 1
        } catch (RuntimeException $e) {
792 1
            throw new RuntimeException($e->getMessage());
793
        }
794
795 1
        return $content;
796
    }
797
798
    /**
799
     * Returns the width of an image/SVG.
800
     *
801
     * @throws RuntimeException
802
     */
803 1
    private function getWidth(): int
804
    {
805 1
        if ($this->data['type'] != 'image') {
806
            return 0;
807
        }
808 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
809 1
            return (int) $svg->width;
810
        }
811 1
        if (false === $size = $this->getImageSize()) {
812
            throw new RuntimeException(\sprintf('Not able to get width of "%s".', $this->data['path']));
813
        }
814
815 1
        return $size[0];
816
    }
817
818
    /**
819
     * Returns the height of an image/SVG.
820
     *
821
     * @throws RuntimeException
822
     */
823 1
    private function getHeight(): int
824
    {
825 1
        if ($this->data['type'] != 'image') {
826
            return 0;
827
        }
828 1
        if (Image::isSVG($this) && false !== $svg = Image::getSvgAttributes($this)) {
829 1
            return (int) $svg->height;
830
        }
831 1
        if (false === $size = $this->getImageSize()) {
832
            throw new RuntimeException(\sprintf('Not able to get height of "%s".', $this->data['path']));
833
        }
834
835 1
        return $size[1];
836
    }
837
838
    /**
839
     * Returns image size informations.
840
     *
841
     * @see https://www.php.net/manual/function.getimagesize.php
842
     *
843
     * @return array|false
844
     */
845 1
    private function getImageSize()
846
    {
847 1
        if (!$this->data['type'] == 'image') {
848
            return false;
849
        }
850
851
        try {
852 1
            if (false === $size = getimagesizefromstring($this->data['content'])) {
853 1
                return false;
854
            }
855
        } catch (\Exception $e) {
856
            throw new RuntimeException(\sprintf('Handling asset "%s" failed: "%s"', $this->data['path'], $e->getMessage()));
857
        }
858
859 1
        return $size;
860
    }
861
862
    /**
863
     * Builds CDN image URL.
864
     */
865
    private function buildImageCdnUrl(): string
866
    {
867
        return str_replace(
868
            [
869
                '%account%',
870
                '%image_url%',
871
                '%width%',
872
                '%quality%',
873
                '%format%',
874
            ],
875
            [
876
                $this->config->get('assets.images.cdn.account') ?? '',
877
                ltrim($this->data['url'] ?? (string) new Url($this->builder, $this->data['path'], ['canonical' => $this->config->get('assets.images.cdn.canonical') ?? true]), '/'),
878
                $this->data['width'],
879
                (int) $this->config->get('assets.images.quality'),
880
                $this->data['ext'],
881
            ],
882
            (string) $this->config->get('assets.images.cdn.url')
883
        );
884
    }
885
}
886