Passed
Pull Request — v1 (#51)
by
unknown
05:53
created

Manifest   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 800
Duplicated Lines 0 %

Importance

Changes 30
Bugs 0 Features 0
Metric Value
eloc 343
dl 0
loc 800
rs 2
c 30
b 0
f 0
wmc 103

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getCspNonceType() 0 7 2
B getFileContents() 0 65 7
A getCriticalCssTags() 0 25 3
A getJsonFile() 0 3 1
B getModule() 0 29 11
A reportError() 0 10 4
A getCssRelPreloadPolyfill() 0 5 1
A getModuleEntry() 0 28 4
B getFileFromUri() 0 27 8
A invalidateCaches() 0 5 1
A getCssModuleTags() 0 24 3
A combinePaths() 0 24 3
A getJsModuleTags() 0 27 5
A jsonFileDecode() 0 9 2
A includeNonce() 0 18 5
A getNonce() 0 12 3
B getModuleTagsByPath() 0 64 11
A generateHtmlTagFromFileExtension() 0 11 4
A getSafariNomoduleFix() 0 14 2
A getCssInlineTags() 0 16 3
A getFile() 0 3 1
B getFileFromManifest() 0 38 8
A getModuleHash() 0 18 3
B getManifestFile() 0 40 8

How to fix   Complexity   

Complex Class

Complex classes like Manifest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manifest, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Twigpack plugin for Craft CMS 3.x
4
 *
5
 * Twigpack is the conduit between Twig and webpack, with manifest.json &
6
 * webpack-dev-server HMR support
7
 *
8
 * @link      https://nystudio107.com/
9
 * @copyright Copyright (c) 2018 nystudio107
10
 */
11
12
namespace nystudio107\twigpack\helpers;
13
14
use nystudio107\twigpack\Twigpack;
15
use nystudio107\twigpack\models\Settings;
16
17
use Craft;
18
use craft\helpers\Html;
19
use craft\helpers\Json as JsonHelper;
20
use craft\helpers\UrlHelper;
21
22
use yii\base\Exception;
23
use yii\caching\TagDependency;
24
use yii\web\NotFoundHttpException;
25
use JsonPath\JsonObject;
26
27
/**
28
 * @author    nystudio107
29
 * @package   Twigpack
30
 * @since     1.0.0
31
 */
32
class Manifest
33
{
34
    // Constants
35
    // =========================================================================
36
37
    const CACHE_KEY = 'twigpack';
38
    const CACHE_TAG = 'twigpack';
39
40
    const DEVMODE_CACHE_DURATION = 1;
41
42
    const CSP_HEADERS = [
43
        'Content-Security-Policy',
44
        'X-Content-Security-Policy',
45
        'X-WebKit-CSP',
46
    ];
47
48
    const SUPPRESS_ERRORS_FOR_MODULES = [
49
        'styles.js',
50
    ];
51
52
    // Protected Static Properties
53
    // =========================================================================
54
55
    /**
56
     * @var array
57
     */
58
    protected static $files;
59
60
    /**
61
     * @var bool
62
     */
63
    protected static $isHot = false;
64
65
    // Public Static Methods
66
    // =========================================================================
67
68
    /**
69
     * @param array $config
70
     * @param string $moduleName
71
     * @param bool $async
72
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
73
     *
74
     * @return string
75
     * @throws NotFoundHttpException
76
     */
77
    public static function getCssModuleTags(array $config, string $moduleName, bool $async, array $attributes = []): string
78
    {
79
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
80
        if ($legacyModule === null) {
81
            return '';
82
        }
83
        $lines = [];
84
        if ($async) {
85
            $lines[] = Html::cssFile($legacyModule, array_merge([
86
                'rel' => 'stylesheet',
87
                'media' => 'print',
88
                'onload' => "this.media='all'",
89
            ], $attributes));
90
            $lines[] = Html::cssFile($legacyModule, array_merge([
91
                'rel' => 'stylesheet',
92
                'noscript' => true,
93
            ], $attributes));
94
        } else {
95
            $lines[] = Html::cssFile($legacyModule, array_merge([
96
                'rel' => 'stylesheet',
97
            ], $attributes));
98
        }
99
100
        return implode("\r\n", $lines);
101
    }
102
103
    /**
104
     * @param string $path
105
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
106
     *
107
     * @return string
108
     */
109
    public static function getCssInlineTags(string $path, array $attributes = []): string
110
    {
111
        $result = self::getFile($path);
112
        if ($result) {
113
            $config = [];
114
            $nonce = self::getNonce();
115
            if ($nonce !== null) {
116
                $config['nonce'] = $nonce;
117
                self::includeNonce($nonce, 'style-src');
118
            }
119
            $result = Html::style($result, array_merge($config, $attributes));
120
121
            return $result;
122
        }
123
124
        return '';
125
    }
126
127
    /**
128
     * @param array $config
129
     * @param null|string $name
130
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
131
     *
132
     * @return string
133
     * @throws \Twig\Error\LoaderError
134
     */
135
    public static function getCriticalCssTags(array $config, $name = null, array $attributes = []): string
136
    {
137
        // Resolve the template name
138
        $template = Craft::$app->getView()->resolveTemplate($name ?? Twigpack::$templateName ?? '');
139
        if ($template) {
140
            $name = self::combinePaths(
141
                pathinfo($template, PATHINFO_DIRNAME),
142
                pathinfo($template, PATHINFO_FILENAME)
143
            );
144
            $dirPrefix = 'templates/';
145
            if (defined('CRAFT_TEMPLATES_PATH')) {
146
                $dirPrefix = CRAFT_TEMPLATES_PATH;
147
            }
148
            $name = strstr($name, $dirPrefix);
149
            $name = (string)str_replace($dirPrefix, '', $name);
150
            $path = self::combinePaths(
151
                    $config['localFiles']['basePath'],
152
                    $config['localFiles']['criticalPrefix'],
153
                    $name
154
                ) . $config['localFiles']['criticalSuffix'];
155
156
            return self::getCssInlineTags($path, $attributes);
157
        }
158
159
        return '';
160
    }
161
162
    /**
163
     * Returns the uglified loadCSS rel=preload Polyfill as per:
164
     * https://github.com/filamentgroup/loadCSS#how-to-use-loadcss-recommended-example
165
     *
166
     * @return string
167
     * @throws \craft\errors\DeprecationException
168
     * @deprecated in 1.2.0
169
     */
170
    public static function getCssRelPreloadPolyfill(): string
171
    {
172
        Craft::$app->getDeprecator()->log('craft.twigpack.includeCssRelPreloadPolyfill()', 'craft.twigpack.includeCssRelPreloadPolyfill() has been deprecated, this function now does nothing. You can safely remove craft.twigpack.includeCssRelPreloadPolyfill() from your templates.');
173
174
        return '';
175
    }
176
177
    /**
178
     * @param array $config
179
     * @param string $moduleName
180
     * @param bool $async
181
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
182
     *
183
     * @return null|string
184
     * @throws NotFoundHttpException
185
     */
186
    public static function getModuleTagsByPath(array $config, string $moduleName, bool $async, array $attributes = [])
187
    {
188
        $manifest = self::getManifestFile($config, 'modern');
189
        $moduleList = [];
190
191
        if ($manifest !== null) {
192
            $jsonObject = new JsonObject($manifest);
193
            $moduleList = $jsonObject->get($moduleName);
194
            // If json path not found $jsonObject->get return false
195
            if (!$moduleList) {
196
                $moduleList = [];
197
            } elseif (is_array($moduleList)) {
198
                // Ensure flat array, ex: if [*] is forgotten in the json path
199
                // targeting an array
200
                $flattened = [];
201
                array_walk_recursive($moduleList, function ($item) use (&$flattened) {
202
                    $flattened[] = $item;
203
                });
204
205
                $moduleList = $flattened;
206
            }
207
        }
208
209
        if (count($moduleList) === 0) {
210
            self::reportError(Craft::t(
211
              'twigpack',
212
              'Module path not found, or empty, in the manifest: {moduleName}',
213
              ['moduleName' => $moduleName]
214
          ), true);
215
216
            return '';
217
        }
218
219
        $lines = [];
220
        foreach ($moduleList as $key => $modulePath) {
221
            // Exclude hot update patchs generated by webpack dev server
222
            if (strpos($modulePath, '.hot-update.js') === false) {
223
                $legacyModule = self::getModule($config, $moduleName, 'legacy', true, $modulePath);
224
                if ($legacyModule === null) {
225
                    return '';
226
                }
227
                $modernModule = '';
228
                if ($async) {
229
                    $modernModule = self::getModule($config, $moduleName, 'modern', true, $modulePath);
230
                    if ($modernModule === null) {
231
                        return '';
232
                    }
233
                }
234
235
                if ($async) {
236
                    $lines[] = self::generateHtmlTagFromFileExtension($modernModule, array_merge([
237
                        'type' => 'module',
238
                    ], $attributes));
239
                    $lines[] = self::generateHtmlTagFromFileExtension($legacyModule, array_merge([
240
                        'nomodule' => true,
241
                    ], $attributes));
242
                } else {
243
                    $lines[] = self::generateHtmlTagFromFileExtension($legacyModule, array_merge([
244
                    ], $attributes));
245
                }
246
            }
247
        }
248
249
        return implode("\r\n", $lines);
250
    }
251
252
    /**
253
     * @param array $config
254
     * @param string $moduleName
255
     * @param bool $async
256
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
257
     *
258
     * @return null|string
259
     * @throws NotFoundHttpException
260
     */
261
    public static function getJsModuleTags(array $config, string $moduleName, bool $async, array $attributes = [])
262
    {
263
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
264
        if ($legacyModule === null) {
265
            return '';
266
        }
267
        $modernModule = '';
268
        if ($async) {
269
            $modernModule = self::getModule($config, $moduleName, 'modern', true);
270
            if ($modernModule === null) {
271
                return '';
272
            }
273
        }
274
        $lines = [];
275
        if ($async) {
276
            $lines[] = Html::jsFile($modernModule, array_merge([
277
                'type' => 'module',
278
            ], $attributes));
279
            $lines[] = Html::jsFile($legacyModule, array_merge([
280
                'nomodule' => true,
281
            ], $attributes));
282
        } else {
283
            $lines[] = Html::jsFile($legacyModule, array_merge([
284
            ], $attributes));
285
        }
286
287
        return implode("\r\n", $lines);
288
    }
289
290
    /**
291
     * Safari 10.1 supports modules, but does not support the `nomodule`
292
     * attribute - it will load <script nomodule> anyway. This snippet solve
293
     * this problem, but only for script tags that load external code, e.g.:
294
     * <script nomodule src="nomodule.js"></script>
295
     *
296
     * Again: this will **not* # prevent inline script, e.g.:
297
     * <script nomodule>alert('no modules');</script>.
298
     *
299
     * This workaround is possible because Safari supports the non-standard
300
     * 'beforeload' event. This allows us to trap the module and nomodule load.
301
     *
302
     * Note also that `nomodule` is supported in later versions of Safari -
303
     * it's just 10.1 that omits this attribute.
304
     *
305
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
306
     *
307
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
308
     *
309
     * @return string
310
     */
311
    public static function getSafariNomoduleFix(array $attributes = []): string
312
    {
313
        $code = /** @lang JavaScript */
314
            <<<EOT
315
!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();
316
EOT;
317
        $config = [];
318
        $nonce = self::getNonce();
319
        if ($nonce !== null) {
320
            $config['nonce'] = $nonce;
321
            self::includeNonce($nonce, 'script-src');
322
        }
323
324
        return Html::script($code, array_merge($config, $attributes));
325
    }
326
327
    /**
328
     * Return the URI to a module
329
     *
330
     * @param array $config
331
     * @param string $moduleName
332
     * @param string $type
333
     * @param bool $soft
334
     *
335
     * @return null|string
336
     * @throws NotFoundHttpException
337
     */
338
    public static function getModule(array $config, string $moduleName, string $type = 'modern', bool $soft = false, string $moduleJsonPath = null)
339
    {
340
        // Get the module entry
341
        $module = $moduleJsonPath ?: self::getModuleEntry($config, $moduleName, $type, $soft);
342
        if ($module !== null) {
343
            $prefix = self::$isHot
344
                ? $config['devServer']['publicPath']
345
                : $config['server']['publicPath'];
346
            $useAbsoluteUrl = $config['useAbsoluteUrl'];
347
            // If the module isn't a full URL, prefix it as required
348
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module)) {
349
                $module = self::combinePaths($prefix, $module);
350
            }
351
            // Resolve any aliases
352
            $alias = Craft::getAlias($module, false);
353
            if ($alias) {
354
                $module = $alias;
355
            }
356
            // Make sure it's a full URL, as required
357
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module) && !is_file($module)) {
0 ignored issues
show
Bug introduced by
It seems like $module can also be of type true; however, parameter $filename of is_file() 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

357
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module) && !is_file(/** @scrutinizer ignore-type */ $module)) {
Loading history...
Bug introduced by
It seems like $module can also be of type true; however, parameter $url of craft\helpers\UrlHelper::isAbsoluteUrl() 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

357
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl(/** @scrutinizer ignore-type */ $module) && !is_file($module)) {
Loading history...
358
                try {
359
                    $module = UrlHelper::siteUrl($module);
0 ignored issues
show
Bug introduced by
It seems like $module can also be of type true; however, parameter $path of craft\helpers\UrlHelper::siteUrl() 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

359
                    $module = UrlHelper::siteUrl(/** @scrutinizer ignore-type */ $module);
Loading history...
360
                } catch (Exception $e) {
361
                    Craft::error($e->getMessage(), __METHOD__);
362
                }
363
            }
364
        }
365
366
        return $module;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $module also could return the type true which is incompatible with the documented return type null|string.
Loading history...
367
    }
368
369
    /**
370
     * Return the HASH value from to module
371
     *
372
     * @param array $config
373
     * @param string $moduleName
374
     * @param string $type
375
     * @param bool $soft
376
     *
377
     * @return null|string
378
     */
379
    public static function getModuleHash(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
380
    {
381
        $moduleHash = '';
382
        try {
383
            // Get the module entry
384
            $module = self::getModuleEntry($config, $moduleName, $type, $soft);
385
            if ($module !== null) {
386
                // Extract only the Hash Value
387
                $modulePath = pathinfo($module);
388
                $moduleFilename = $modulePath['filename'];
389
                $moduleHash = substr($moduleFilename, strpos($moduleFilename, ".") + 1);
390
            }
391
        } catch (Exception $e) {
392
            // return empty string if no module is found
393
            return '';
394
        }
395
396
        return $moduleHash;
397
    }
398
399
    /**
400
     * Return a module's raw entry from the manifest
401
     *
402
     * @param array $config
403
     * @param string $moduleName
404
     * @param string $type
405
     * @param bool $soft
406
     *
407
     * @return null|string
408
     * @throws NotFoundHttpException
409
     */
410
    public static function getModuleEntry(
411
        array $config,
412
        string $moduleName,
413
        string $type = 'modern',
414
        bool $soft = false,
415
        string $moduleJsonPath = null
416
    ) {
417
        $module = null;
418
        // Get the manifest file
419
        $manifest = self::getManifestFile($config, $type);
420
        if ($manifest !== null) {
421
            // Make sure it exists in the manifest
422
            if (empty($manifest[$moduleName])) {
423
                // Don't report errors for any files in SUPPRESS_ERRORS_FOR_MODULES
424
                if (!in_array($moduleName, self::SUPPRESS_ERRORS_FOR_MODULES)) {
425
                    self::reportError(Craft::t(
426
                        'twigpack',
427
                        'Module does not exist in the manifest: {moduleName}',
428
                        ['moduleName' => $moduleName]
429
                    ), $soft);
430
                }
431
432
                return null;
433
            }
434
            $module = $manifest[$moduleName];
435
        }
436
437
        return $module;
438
    }
439
440
    /**
441
     * Return a JSON-decoded manifest file
442
     *
443
     * @param array $config
444
     * @param string $type
445
     *
446
     * @return null|array
447
     * @throws NotFoundHttpException
448
     */
449
    public static function getManifestFile(array $config, string $type = 'modern')
450
    {
451
        $manifest = null;
452
        // Determine whether we should use the devServer for HMR or not
453
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
454
        self::$isHot = ($devMode && $config['useDevServer']);
455
        // Try to get the manifest
456
        while ($manifest === null) {
457
            $manifestPath = self::$isHot
458
                ? $config['devServer']['manifestPath']
459
                : $config['server']['manifestPath'];
460
            // If this is a dev-server, use the defined build type
461
            $thisType = $type;
462
            if (self::$isHot) {
463
                $thisType = $config['devServerBuildType'] === 'combined'
464
                    ? $thisType
465
                    : $config['devServerBuildType'];
466
            }
467
            // Normalize the path
468
            $path = self::combinePaths($manifestPath, $config['manifest'][$thisType]);
469
            $manifest = self::getJsonFile($path);
470
            // If the manifest isn't found, and it was hot, fall back on non-hot
471
            if ($manifest === null) {
472
                // We couldn't find a manifest; throw an error
473
                self::reportError(Craft::t(
474
                    'twigpack',
475
                    'Manifest file not found at: {manifestPath}',
476
                    ['manifestPath' => $manifestPath]
477
                ), true);
478
                if (self::$isHot) {
479
                    // Try again, but not with home module replacement
480
                    self::$isHot = false;
481
                } else {
482
                    // Give up and return null
483
                    return null;
484
                }
485
            }
486
        }
487
488
        return $manifest;
489
    }
490
491
    /**
492
     * Returns the contents of a file from a URI path
493
     *
494
     * @param string $path
495
     *
496
     * @return string
497
     */
498
    public static function getFile(string $path): string
499
    {
500
        return self::getFileFromUri($path, null, true) ?? '';
501
    }
502
503
    /**
504
     * @param array $config
505
     * @param string $fileName
506
     * @param string $type
507
     *
508
     * @return string
509
     */
510
    public static function getFileFromManifest(array $config, string $fileName, string $type = 'legacy'): string
511
    {
512
        $path = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
513
        try {
514
            $path = self::getModuleEntry($config, $fileName, $type, true);
515
        } catch (NotFoundHttpException $e) {
516
            Craft::error($e->getMessage(), __METHOD__);
517
        }
518
        if ($path !== null) {
519
            // Determine whether we should use the devServer for HMR or not
520
            $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
521
            if ($devMode) {
522
                $devServerPrefix = $config['devServer']['publicPath'];
523
                $devServerPath = self::combinePaths(
524
                    $devServerPrefix,
525
                    $path
526
                );
527
                $devServerFile = self::getFileFromUri($devServerPath, null);
528
                if ($devServerFile) {
529
                    return $devServerFile;
530
                }
531
            }
532
            // Otherwise, try not-hot files
533
            $localPrefix = $config['localFiles']['basePath'];
534
            $localPath = self::combinePaths(
535
                $localPrefix,
536
                $path
537
            );
538
            $alias = Craft::getAlias($localPath, false);
539
            if ($alias && is_string($alias)) {
540
                $localPath = $alias;
541
            }
542
            if (is_file($localPath)) {
543
                return self::getFile($localPath) ?? '';
544
            }
545
        }
546
547
        return '';
548
    }
549
550
    /**
551
     * Invalidate all of the manifest caches
552
     */
553
    public static function invalidateCaches()
554
    {
555
        $cache = Craft::$app->getCache();
556
        TagDependency::invalidate($cache, self::CACHE_TAG);
557
        Craft::info('All manifest caches cleared', __METHOD__);
558
    }
559
560
    /**
561
     * Return the contents of a JSON file from a URI path
562
     *
563
     * @param string $path
564
     *
565
     * @return null|array
566
     */
567
    protected static function getJsonFile(string $path)
568
    {
569
        return self::getFileFromUri($path, [self::class, 'jsonFileDecode']);
570
    }
571
572
    // Protected Static Methods
573
    // =========================================================================
574
575
    /**
576
     * Return the contents of a file from a URI path
577
     *
578
     * @param string $path
579
     * @param callable|null $callback
580
     * @param bool $pathOnly
581
     *
582
     * @return null|mixed
583
     */
584
    protected static function getFileFromUri(string $path, callable $callback = null, bool $pathOnly = false)
585
    {
586
        // Resolve any aliases
587
        $alias = Craft::getAlias($path, false);
588
        if ($alias && is_string($alias)) {
589
            $path = $alias;
590
        }
591
        // If we only want the file via path, make sure it exists
592
        if ($pathOnly && !is_file($path)) {
593
            Craft::warning(Craft::t(
594
                'twigpack',
595
                'File does not exist: {path}',
596
                ['path' => $path]
597
            ), __METHOD__);
598
599
            return '';
600
        }
601
        // Make sure it's a full URL
602
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file($path)) {
603
            try {
604
                $path = UrlHelper::siteUrl($path);
605
            } catch (Exception $e) {
606
                Craft::error($e->getMessage(), __METHOD__);
607
            }
608
        }
609
610
        return self::getFileContents($path, $callback);
611
    }
612
613
    /**
614
     * Return the contents of a file from the passed in path
615
     *
616
     * @param string $path
617
     * @param callable $callback
618
     *
619
     * @return null|mixed
620
     */
621
    protected static function getFileContents(string $path, callable $callback = null)
622
    {
623
        // Return the memoized manifest if it exists
624
        if (!empty(self::$files[$path])) {
625
            return self::$files[$path];
626
        }
627
        // Create the dependency tags
628
        $dependency = new TagDependency([
629
            'tags' => [
630
                self::CACHE_TAG,
631
                self::CACHE_TAG . $path,
632
            ],
633
        ]);
634
        // Set the cache duration based on devMode
635
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
636
            ? self::DEVMODE_CACHE_DURATION
637
            : null;
638
        // If we're in `devMode` invalidate the cache immediately
639
        if (Craft::$app->getConfig()->getGeneral()->devMode) {
640
            self::invalidateCaches();
641
        }
642
        // Get the result from the cache, or parse the file
643
        $cache = Craft::$app->getCache();
644
        $settings = Twigpack::$plugin->getSettings();
645
        $cacheKeySuffix = $settings->cacheKeySuffix ?? '';
646
        $file = $cache->getOrSet(
647
            self::CACHE_KEY . $cacheKeySuffix . $path,
648
            function () use ($path, $callback) {
649
                $result = null;
650
                if (UrlHelper::isAbsoluteUrl($path)) {
651
                    /**
652
                     * Silly work-around for what appears to be a file_get_contents bug with https
653
                     * http://stackoverflow.com/questions/10524748/why-im-getting-500-error-when-using-file-get-contents-but-works-in-a-browser
654
                     */
655
                    $opts = [
656
                        'ssl' => [
657
                            'verify_peer' => false,
658
                            'verify_peer_name' => false,
659
                        ],
660
                        'http' => [
661
                            'timeout' => 5,
662
                            'ignore_errors' => true,
663
                            'header' => "User-Agent:Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13\r\n",
664
                        ],
665
                    ];
666
                    $context = stream_context_create($opts);
667
                    $contents = @file_get_contents($path, false, $context);
668
                } else {
669
                    $contents = @file_get_contents($path);
670
                }
671
                if ($contents) {
672
                    $result = $contents;
673
                    if ($callback) {
674
                        $result = $callback($result);
675
                    }
676
                }
677
678
                return $result;
679
            },
680
            $cacheDuration,
681
            $dependency
682
        );
683
        self::$files[$path] = $file;
684
685
        return $file;
686
    }
687
688
    /**
689
     * Combined the passed in paths, whether file system or URL
690
     *
691
     * @param string ...$paths
692
     *
693
     * @return string
694
     */
695
    protected static function combinePaths(string ...$paths): string
696
    {
697
        $last_key = count($paths) - 1;
698
        array_walk($paths, function (&$val, $key) use ($last_key) {
699
            switch ($key) {
700
                case 0:
701
                    $val = rtrim($val, '/ ');
702
                    break;
703
                case $last_key:
704
                    $val = ltrim($val, '/ ');
705
                    break;
706
                default:
707
                    $val = trim($val, '/ ');
708
                    break;
709
            }
710
        });
711
712
        $first = array_shift($paths);
713
        $last = array_pop($paths);
714
        $paths = array_filter($paths);
715
        array_unshift($paths, $first);
716
        $paths[] = $last;
717
718
        return implode('/', $paths);
719
    }
720
721
    /**
722
     * @param string $error
723
     * @param bool $soft
724
     *
725
     * @throws NotFoundHttpException
726
     */
727
    protected static function reportError(string $error, $soft = false)
728
    {
729
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
730
        if ($devMode && !$soft) {
731
            throw new NotFoundHttpException($error);
732
        }
733
        if (self::$isHot) {
734
            Craft::warning($error, __METHOD__);
735
        } else {
736
            Craft::error($error, __METHOD__);
737
        }
738
    }
739
740
    // Private Static Methods
741
    // =========================================================================
742
743
    /**
744
     * @param string $nonce
745
     * @param string $cspDirective
746
     */
747
    private static function includeNonce(string $nonce, string $cspDirective)
748
    {
749
        $cspNonceType = self::getCspNonceType();
750
        if ($cspNonceType) {
751
            $cspValue = "{$cspDirective} 'nonce-$nonce'";
752
            foreach (self::CSP_HEADERS as $cspHeader) {
753
                switch ($cspNonceType) {
754
                    case 'tag':
755
                        Craft::$app->getView()->registerMetaTag([
756
                            'httpEquiv' => $cspHeader,
757
                            'value' => $cspValue,
758
                        ]);
759
                        break;
760
                    case 'header':
761
                        Craft::$app->getResponse()->getHeaders()->add($cspHeader, $cspValue . ';');
762
                        break;
763
                    default:
764
                        break;
765
                }
766
            }
767
        }
768
    }
769
770
    /**
771
     * @return string|null
772
     */
773
    private static function getCspNonceType()
774
    {
775
        /** @var Settings $settings */
776
        $settings = Twigpack::$plugin->getSettings();
777
        $cspNonceType = !empty($settings->cspNonce) ? strtolower($settings->cspNonce) : null;
778
779
        return $cspNonceType;
780
    }
781
782
    /**
783
     * @return string|null
784
     */
785
    private static function getNonce()
786
    {
787
        $result = null;
788
        if (self::getCspNonceType() !== null) {
789
            try {
790
                $result = bin2hex(random_bytes(22));
791
            } catch (\Exception $e) {
792
                // That's okay
793
            }
794
        }
795
796
        return $result;
797
    }
798
    /**
799
     * @param $string
800
     *
801
     * @return null|array
802
     */
803
    private static function jsonFileDecode($string)
804
    {
805
        $json = JsonHelper::decodeIfJson($string);
806
        if (is_string($json)) {
807
            Craft::error('Error decoding JSON file: ' . $json, __METHOD__);
808
            $json = null;
809
        }
810
811
        return $json;
812
    }
813
814
    /**
815
    * @param string $moduleName
816
    * @param array $params
817
    * @param array $attributes
818
     *
819
     * @return string
820
     */
821
    private static function generateHtmlTagFromFileExtension(string $moduleName, array $params, array $attributes = [])
822
    {
823
        if (preg_match('/\.css(\?.*)?$/i', $moduleName)) {
824
            return Html::cssFile($moduleName, $params, $attributes);
0 ignored issues
show
Unused Code introduced by
The call to yii\helpers\BaseHtml::cssFile() has too many arguments starting with $attributes. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

824
            return Html::/** @scrutinizer ignore-call */ cssFile($moduleName, $params, $attributes);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
825
        } elseif (preg_match('/\.js(\?.*)?$/i', $moduleName)) {
826
            return Html::jsFile($moduleName, $params, $attributes);
0 ignored issues
show
Unused Code introduced by
The call to yii\helpers\BaseHtml::jsFile() has too many arguments starting with $attributes. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

826
            return Html::/** @scrutinizer ignore-call */ jsFile($moduleName, $params, $attributes);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
827
        } elseif (preg_match('/\.(svg|png|jpe?g|webp|avif|gif)(\?.*)?$/i', $moduleName)) {
828
            return Html::img($moduleName, $params, $attributes);
0 ignored issues
show
Unused Code introduced by
The call to yii\helpers\BaseHtml::img() has too many arguments starting with $attributes. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

828
            return Html::/** @scrutinizer ignore-call */ img($moduleName, $params, $attributes);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
829
        }
830
831
        return $moduleName;
832
    }
833
}
834