Passed
Push — v1 ( 8bb90a...53d8eb )
by Andrew
16:54 queued 10:32
created

Manifest::getFileContents()   B

Complexity

Conditions 10
Paths 5

Size

Total Lines 83
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 0 Features 0
Metric Value
cc 10
eloc 53
c 13
b 0
f 0
nc 5
nop 2
dl 0
loc 83
rs 7.1587

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
 * 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 GuzzleHttp\Client;
23
use GuzzleHttp\Exception\GuzzleException;
24
use GuzzleHttp\RequestOptions;
25
26
use yii\base\Exception;
27
use yii\caching\ChainedDependency;
28
use yii\caching\FileDependency;
29
use yii\caching\TagDependency;
30
use yii\web\NotFoundHttpException;
31
32
/**
33
 * @author    nystudio107
34
 * @package   Twigpack
35
 * @since     1.0.0
36
 */
37
class Manifest
38
{
39
    // Constants
40
    // =========================================================================
41
42
    const CACHE_KEY = 'twigpack';
43
    const CACHE_TAG = 'twigpack';
44
45
    const DEVMODE_CACHE_DURATION = 1;
46
47
    const CSP_HEADERS = [
48
        'Content-Security-Policy',
49
        'X-Content-Security-Policy',
50
        'X-WebKit-CSP',
51
    ];
52
53
    const SUPPRESS_ERRORS_FOR_MODULES = [
54
        'styles.js',
55
    ];
56
57
    // Protected Static Properties
58
    // =========================================================================
59
60
    /**
61
     * @var array
62
     */
63
    protected static $files;
64
65
    /**
66
     * @var bool
67
     */
68
    protected static $isHot = false;
69
70
    // Public Static Methods
71
    // =========================================================================
72
73
    /**
74
     * @param array $config
75
     * @param string $moduleName
76
     * @param bool $async
77
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
78
     *
79
     * @return string
80
     * @throws NotFoundHttpException
81
     */
82
    public static function getCssModuleTags(array $config, string $moduleName, bool $async, array $attributes = []): string
83
    {
84
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
85
        if ($legacyModule === null) {
86
            return '';
87
        }
88
        $lines = [];
89
        if ($async) {
90
            $lines[] = Html::cssFile($legacyModule, array_merge([
91
                'rel' => 'stylesheet',
92
                'media' => 'print',
93
                'onload' => "this.media='all'",
94
            ], $attributes));
95
            $lines[] = Html::cssFile($legacyModule, array_merge([
96
                'rel' => 'stylesheet',
97
                'noscript' => true,
98
            ], $attributes));
99
        } else {
100
            $lines[] = Html::cssFile($legacyModule, array_merge([
101
                'rel' => 'stylesheet',
102
            ], $attributes));
103
        }
104
105
        return implode("\r\n", $lines);
106
    }
107
108
    /**
109
     * @param string $path
110
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
111
     *
112
     * @return string
113
     */
114
    public static function getCssInlineTags(string $path, array $attributes = []): string
115
    {
116
        $result = self::getFile($path);
117
        if ($result) {
118
            $config = [];
119
            $nonce = self::getNonce();
120
            if ($nonce !== null) {
121
                $config['nonce'] = $nonce;
122
                self::includeNonce($nonce, 'style-src');
123
            }
124
            $result = Html::style($result, array_merge($config, $attributes));
125
126
            return $result;
127
        }
128
129
        return '';
130
    }
131
132
    /**
133
     * @param array $config
134
     * @param null|string $name
135
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
136
     *
137
     * @return string
138
     * @throws \Twig\Error\LoaderError
139
     */
140
    public static function getCriticalCssTags(array $config, $name = null, array $attributes = []): string
141
    {
142
        // Resolve the template name
143
        $template = Craft::$app->getView()->resolveTemplate($name ?? Twigpack::$templateName ?? '');
144
        if ($template) {
145
            $name = self::combinePaths(
146
                pathinfo($template, PATHINFO_DIRNAME),
0 ignored issues
show
Bug introduced by
It seems like pathinfo($template, nyst...lpers\PATHINFO_DIRNAME) can also be of type array; however, parameter $paths of nystudio107\twigpack\hel...anifest::combinePaths() 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

146
                /** @scrutinizer ignore-type */ pathinfo($template, PATHINFO_DIRNAME),
Loading history...
147
                pathinfo($template, PATHINFO_FILENAME)
148
            );
149
            $dirPrefix = 'templates/';
150
            if (defined('CRAFT_TEMPLATES_PATH')) {
151
                $dirPrefix = CRAFT_TEMPLATES_PATH;
152
            }
153
            $name = strstr($name, $dirPrefix);
154
            $name = (string)str_replace($dirPrefix, '', $name);
155
            $path = self::combinePaths(
156
                    $config['localFiles']['basePath'],
157
                    $config['localFiles']['criticalPrefix'],
158
                    $name
159
                ) . $config['localFiles']['criticalSuffix'];
160
161
            return self::getCssInlineTags($path, $attributes);
162
        }
163
164
        return '';
165
    }
166
167
    /**
168
     * Returns the uglified loadCSS rel=preload Polyfill as per:
169
     * https://github.com/filamentgroup/loadCSS#how-to-use-loadcss-recommended-example
170
     *
171
     * @return string
172
     * @throws \craft\errors\DeprecationException
173
     * @deprecated in 1.2.0
174
     */
175
    public static function getCssRelPreloadPolyfill(): string
176
    {
177
        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.');
178
179
        return '';
180
    }
181
182
    /**
183
     * @param array $config
184
     * @param string $moduleName
185
     * @param bool $async
186
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
187
     *
188
     * @return null|string
189
     * @throws NotFoundHttpException
190
     */
191
    public static function getJsModuleTags(array $config, string $moduleName, bool $async, array $attributes = [])
192
    {
193
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
194
        if ($legacyModule === null) {
195
            return '';
196
        }
197
        $modernModule = '';
198
        if ($async) {
199
            $modernModule = self::getModule($config, $moduleName, 'modern', true);
200
            if ($modernModule === null) {
201
                return '';
202
            }
203
        }
204
        $lines = [];
205
        if ($async) {
206
            $lines[] = Html::jsFile($modernModule, array_merge([
207
                'type' => 'module',
208
            ], $attributes));
209
            $lines[] = Html::jsFile($legacyModule, array_merge([
210
                'nomodule' => true,
211
            ], $attributes));
212
        } else {
213
            $lines[] = Html::jsFile($legacyModule, array_merge([
214
            ], $attributes));
215
        }
216
217
        return implode("\r\n", $lines);
218
    }
219
220
    /**
221
     * Safari 10.1 supports modules, but does not support the `nomodule`
222
     * attribute - it will load <script nomodule> anyway. This snippet solve
223
     * this problem, but only for script tags that load external code, e.g.:
224
     * <script nomodule src="nomodule.js"></script>
225
     *
226
     * Again: this will **not* # prevent inline script, e.g.:
227
     * <script nomodule>alert('no modules');</script>.
228
     *
229
     * This workaround is possible because Safari supports the non-standard
230
     * 'beforeload' event. This allows us to trap the module and nomodule load.
231
     *
232
     * Note also that `nomodule` is supported in later versions of Safari -
233
     * it's just 10.1 that omits this attribute.
234
     *
235
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
236
     *
237
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
238
     *
239
     * @return string
240
     */
241
    public static function getSafariNomoduleFix(array $attributes = []): string
242
    {
243
        $code = /** @lang JavaScript */
244
            <<<EOT
245
!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()}}();
246
EOT;
247
        $config = [];
248
        $nonce = self::getNonce();
249
        if ($nonce !== null) {
250
            $config['nonce'] = $nonce;
251
            self::includeNonce($nonce, 'script-src');
252
        }
253
254
        return Html::script($code, array_merge($config, $attributes));
255
    }
256
257
    /**
258
     * Return the URI to a module
259
     *
260
     * @param array $config
261
     * @param string $moduleName
262
     * @param string $type
263
     * @param bool $soft
264
     *
265
     * @return null|string
266
     * @throws NotFoundHttpException
267
     */
268
    public static function getModule(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
269
    {
270
        // Get the module entry
271
        $module = self::getModuleEntry($config, $moduleName, $type, $soft);
272
        if ($module !== null) {
273
            $prefix = self::$isHot
274
                ? $config['devServer']['publicPath']
275
                : $config['server']['publicPath'];
276
            $useAbsoluteUrl = $config['useAbsoluteUrl'];
277
            // If the module isn't a full URL, prefix it as required
278
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module)) {
279
                $module = self::combinePaths($prefix, $module);
280
            }
281
            // Resolve any aliases
282
            $alias = Craft::getAlias($module, false);
283
            if ($alias) {
284
                $module = $alias;
285
            }
286
            // Make sure it's a full URL, as required
287
            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 $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

287
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl(/** @scrutinizer ignore-type */ $module) && !is_file($module)) {
Loading history...
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

287
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module) && !is_file(/** @scrutinizer ignore-type */ $module)) {
Loading history...
288
                try {
289
                    $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

289
                    $module = UrlHelper::siteUrl(/** @scrutinizer ignore-type */ $module);
Loading history...
290
                } catch (Exception $e) {
291
                    Craft::error($e->getMessage(), __METHOD__);
292
                }
293
            }
294
        }
295
296
        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...
297
    }
298
299
    /**
300
     * Return the HASH value from to module
301
     *
302
     * @param array $config
303
     * @param string $moduleName
304
     * @param string $type
305
     * @param bool $soft
306
     *
307
     * @return null|string
308
     */
309
    public static function getModuleHash(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
310
    {
311
312
        $moduleHash = '';
313
        try {
314
            // Get the module entry
315
            $module = self::getModuleEntry($config, $moduleName, $type, $soft);
316
            if ($module !== null) {
317
                // Extract only the Hash Value
318
                $modulePath = pathinfo($module);
319
                $moduleFilename = $modulePath['filename'];
320
                $moduleHash = substr($moduleFilename, strpos($moduleFilename, ".") + 1);
321
            }
322
        } catch (Exception $e) {
323
            // return empty string if no module is found
324
            return '';
325
        }
326
327
        return $moduleHash;
328
    }
329
330
    /**
331
     * Return a module's raw entry from the manifest
332
     *
333
     * @param array $config
334
     * @param string $moduleName
335
     * @param string $type
336
     * @param bool $soft
337
     *
338
     * @return null|string
339
     * @throws NotFoundHttpException
340
     */
341
    public static function getModuleEntry(
342
        array $config,
343
        string $moduleName,
344
        string $type = 'modern',
345
        bool $soft = false
346
    )
347
    {
348
        $module = null;
349
        // Get the manifest file
350
        $manifest = self::getManifestFile($config, $type);
351
        if ($manifest !== null) {
352
            // Make sure it exists in the manifest
353
            if (empty($manifest[$moduleName])) {
354
                // Don't report errors for any files in SUPPRESS_ERRORS_FOR_MODULES
355
                if (!in_array($moduleName, self::SUPPRESS_ERRORS_FOR_MODULES)) {
356
                    self::reportError(Craft::t(
357
                        'twigpack',
358
                        'Module does not exist in the manifest: {moduleName}',
359
                        ['moduleName' => $moduleName]
360
                    ), $soft);
361
                }
362
363
                return null;
364
            }
365
            $module = $manifest[$moduleName];
366
        }
367
368
        return $module;
369
    }
370
371
    /**
372
     * Return a JSON-decoded manifest file
373
     *
374
     * @param array $config
375
     * @param string $type
376
     *
377
     * @return null|array
378
     * @throws NotFoundHttpException
379
     */
380
    public static function getManifestFile(array $config, string $type = 'modern')
381
    {
382
        $manifest = null;
383
        // Determine whether we should use the devServer for HMR or not
384
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
385
        self::$isHot = ($devMode && $config['useDevServer']);
386
        // Try to get the manifest
387
        while ($manifest === null) {
388
            $manifestPath = self::$isHot
389
                ? $config['devServer']['manifestPath']
390
                : $config['server']['manifestPath'];
391
            // If this is a dev-server, use the defined build type
392
            $thisType = $type;
393
            if (self::$isHot) {
394
                $thisType = $config['devServerBuildType'] === 'combined'
395
                    ? $thisType
396
                    : $config['devServerBuildType'];
397
            }
398
            // Normalize the path
399
            $path = self::combinePaths($manifestPath, $config['manifest'][$thisType]);
400
            $manifest = self::getJsonFile($path);
401
            // If the manifest isn't found, and it was hot, fall back on non-hot
402
            if ($manifest === null) {
403
                // We couldn't find a manifest; throw an error
404
                self::reportError(Craft::t(
405
                    'twigpack',
406
                    'Manifest file not found at: {manifestPath}',
407
                    ['manifestPath' => $manifestPath]
408
                ), true);
409
                if (self::$isHot) {
410
                    // Try again, but not with home module replacement
411
                    self::$isHot = false;
412
                } else {
413
                    // Give up and return null
414
                    return null;
415
                }
416
            }
417
        }
418
419
        return $manifest;
420
    }
421
422
    /**
423
     * Returns the contents of a file from a URI path
424
     *
425
     * @param string $path
426
     *
427
     * @return string
428
     */
429
    public static function getFile(string $path): string
430
    {
431
        return self::getFileFromUri($path, null, true) ?? '';
432
    }
433
434
    /**
435
     * @param array $config
436
     * @param string $fileName
437
     * @param string $type
438
     *
439
     * @return string
440
     */
441
    public static function getFileFromManifest(array $config, string $fileName, string $type = 'legacy'): string
442
    {
443
        $path = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
444
        try {
445
            $path = self::getModuleEntry($config, $fileName, $type, true);
446
        } catch (NotFoundHttpException $e) {
447
            Craft::error($e->getMessage(), __METHOD__);
448
        }
449
        if ($path !== null) {
450
            // Determine whether we should use the devServer for HMR or not
451
            $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
452
            if ($devMode) {
453
                $devServerPrefix = $config['devServer']['publicPath'];
454
                // If we're using the devserver, swap in the deverserver path
455
                if (UrlHelper::isAbsoluteUrl($path) && self::$isHot) {
456
                    $path = parse_url($path, PHP_URL_PATH);
457
                }
458
                $devServerPath = self::combinePaths(
459
                    $devServerPrefix,
460
                    $path
461
                );
462
                $devServerFile = self::getFileFromUri($devServerPath, null);
463
                if ($devServerFile) {
464
                    return $devServerFile;
465
                }
466
            }
467
            // Otherwise, try not-hot files
468
            $localPrefix = $config['localFiles']['basePath'];
469
            $localPath = self::combinePaths(
470
                $localPrefix,
471
                $path
472
            );
473
            $alias = Craft::getAlias($localPath, false);
474
            if ($alias && is_string($alias)) {
475
                $localPath = $alias;
476
            }
477
            if (is_file($localPath)) {
478
                return self::getFile($localPath) ?? '';
479
            }
480
        }
481
482
        return '';
483
    }
484
485
    /**
486
     * Invalidate all of the manifest caches
487
     */
488
    public static function invalidateCaches()
489
    {
490
        $cache = Craft::$app->getCache();
491
        TagDependency::invalidate($cache, self::CACHE_TAG);
492
        Craft::info('All manifest caches cleared', __METHOD__);
493
    }
494
495
    /**
496
     * Return the contents of a JSON file from a URI path
497
     *
498
     * @param string $path
499
     *
500
     * @return null|array
501
     */
502
    protected static function getJsonFile(string $path)
503
    {
504
        return self::getFileFromUri($path, [self::class, 'jsonFileDecode']);
505
    }
506
507
    // Protected Static Methods
508
    // =========================================================================
509
510
    /**
511
     * Return the contents of a file from a URI path
512
     *
513
     * @param string $path
514
     * @param callable|null $callback
515
     * @param bool $pathOnly
516
     *
517
     * @return null|mixed
518
     */
519
    protected static function getFileFromUri(string $path, callable $callback = null, bool $pathOnly = false)
520
    {
521
        // Resolve any aliases
522
        $alias = Craft::getAlias($path, false);
523
        if ($alias && is_string($alias)) {
524
            $path = $alias;
525
        }
526
        // If we only want the file via path, make sure it exists
527
        if ($pathOnly && !is_file($path)) {
528
            Craft::warning(Craft::t(
529
                'twigpack',
530
                'File does not exist: {path}',
531
                ['path' => $path]
532
            ), __METHOD__);
533
534
            return '';
535
        }
536
        // Make sure it's a full URL
537
        if (!UrlHelper::isAbsoluteUrl($path) && !is_file($path)) {
538
            try {
539
                $path = UrlHelper::siteUrl($path);
540
            } catch (Exception $e) {
541
                Craft::error($e->getMessage(), __METHOD__);
542
            }
543
        }
544
545
        return self::getFileContents($path, $callback);
546
    }
547
548
    /**
549
     * Return the contents of a file from the passed in path
550
     *
551
     * @param string $path
552
     * @param callable $callback
553
     *
554
     * @return null|mixed
555
     */
556
    protected static function getFileContents(string $path, callable $callback = null)
557
    {
558
        // Return the memoized manifest if it exists
559
        if (!empty(self::$files[$path])) {
560
            return self::$files[$path];
561
        }
562
        // Create the dependency tags
563
        $dependency = new TagDependency([
564
            'tags' => [
565
                self::CACHE_TAG,
566
                self::CACHE_TAG . $path,
567
            ],
568
        ]);
569
        // If this is a file path such as for the `manifest.json`, add a FileDependency so it's cache bust if the file changes
570
        if (!UrlHelper::isAbsoluteUrl($path)) {
571
            $dependency = new ChainedDependency([
572
                'dependencies' => [
573
                    new FileDependency([
574
                        'fileName' => $path
575
                    ]),
576
                    $dependency
577
                ]
578
            ]);
579
        }
580
        // Set the cache duration based on devMode
581
        $cacheDuration = Craft::$app->getConfig()->getGeneral()->devMode
582
            ? self::DEVMODE_CACHE_DURATION
583
            : null;
584
        // Get the result from the cache, or parse the file
585
        $cache = Craft::$app->getCache();
586
        $settings = Twigpack::$plugin->getSettings();
587
        $cacheKeySuffix = $settings->cacheKeySuffix ?? '';
588
        $file = $cache->getOrSet(
589
            self::CACHE_KEY . $cacheKeySuffix . $path,
590
            function () use ($path, $callback) {
591
                $result = null;
592
                $contents = null;
593
                if (UrlHelper::isAbsoluteUrl($path)) {
594
                    $clientOptions = [
595
                        RequestOptions::HTTP_ERRORS => false,
596
                        RequestOptions::CONNECT_TIMEOUT => 3,
597
                        RequestOptions::VERIFY => false,
598
                        RequestOptions::TIMEOUT => 5,
599
                    ];
600
                    // If we're hot, insert a short 50ms delay in fetching remove files, to handle a webpack-dev-server/
601
                    // Tailwind CSS JIT race condition
602
                    if (self::$isHot) {
603
                        $clientOptions = array_merge($clientOptions, [
604
                            RequestOptions::DELAY => 100,
605
                        ]);
606
                    }
607
                    $client = new Client($clientOptions);
608
                    try {
609
                        $response = $client->request('GET', $path, [
610
                            RequestOptions::HEADERS => [
611
                                'User-Agent' => "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",
612
                                'Accept' => '*/*',
613
                            ],
614
                        ]);
615
                        if ($response->getStatusCode() === 200) {
616
                            $contents = $response->getBody()->getContents();
617
                        }
618
                    } catch(\Throwable $e) {
619
                        Craft::error($e, __METHOD__);
620
                    }
621
                } else {
622
                    $contents = @file_get_contents($path);
623
                }
624
                if ($contents) {
625
                    $result = $contents;
626
                    if ($callback) {
627
                        $result = $callback($result);
628
                    }
629
                }
630
631
                return $result;
632
            },
633
            $cacheDuration,
634
            $dependency
635
        );
636
        self::$files[$path] = $file;
637
638
        return $file;
639
    }
640
641
    /**
642
     * Get the response code from a given $url
643
     *
644
     * @param $url
645
     * @param $context
646
     * @return false|string
647
     */
648
    protected static function getHttpResponseCode($url, $context)
649
    {
650
        $headers = @get_headers($url, 0, $context);
651
        if (empty($headers)) {
652
            return '404';
653
        }
654
655
        return substr($headers[0], 9, 3);
656
    }
657
658
    /**
659
     * Combined the passed in paths, whether file system or URL
660
     *
661
     * @param string ...$paths
662
     *
663
     * @return string
664
     */
665
    protected static function combinePaths(string ...$paths): string
666
    {
667
        $last_key = count($paths) - 1;
668
        array_walk($paths, function (&$val, $key) use ($last_key) {
669
            switch ($key) {
670
                case 0:
671
                    $val = rtrim($val, '/ ');
672
                    break;
673
                case $last_key:
674
                    $val = ltrim($val, '/ ');
675
                    break;
676
                default:
677
                    $val = trim($val, '/ ');
678
                    break;
679
            }
680
        });
681
682
        $first = array_shift($paths);
683
        $last = array_pop($paths);
684
        $paths = array_filter($paths);
685
        array_unshift($paths, $first);
686
        $paths[] = $last;
687
688
        return implode('/', $paths);
689
    }
690
691
    /**
692
     * @param string $error
693
     * @param bool $soft
694
     *
695
     * @throws NotFoundHttpException
696
     */
697
    protected static function reportError(string $error, $soft = false)
698
    {
699
        $devMode = Craft::$app->getConfig()->getGeneral()->devMode;
700
        if ($devMode && !$soft) {
701
            throw new NotFoundHttpException($error);
702
        }
703
        if (self::$isHot) {
704
            Craft::warning($error, __METHOD__);
705
        } else {
706
            Craft::error($error, __METHOD__);
707
        }
708
    }
709
710
    // Private Static Methods
711
    // =========================================================================
712
713
    /**
714
     * @param string $nonce
715
     * @param string $cspDirective
716
     */
717
    private static function includeNonce(string $nonce, string $cspDirective)
718
    {
719
        $cspNonceType = self::getCspNonceType();
720
        if ($cspNonceType) {
721
            $cspValue = "{$cspDirective} 'nonce-$nonce'";
722
            foreach(self::CSP_HEADERS as $cspHeader) {
723
                switch ($cspNonceType) {
724
                    case 'tag':
725
                        Craft::$app->getView()->registerMetaTag([
726
                            'httpEquiv' => $cspHeader,
727
                            'value' => $cspValue,
728
                        ]);
729
                        break;
730
                    case 'header':
731
                        Craft::$app->getResponse()->getHeaders()->add($cspHeader, $cspValue . ';');
732
                        break;
733
                    default:
734
                        break;
735
                }
736
            }
737
        }
738
    }
739
740
    /**
741
     * @return string|null
742
     */
743
    private static function getCspNonceType()
744
    {
745
        /** @var Settings $settings */
746
        $settings = Twigpack::$plugin->getSettings();
747
        $cspNonceType = !empty($settings->cspNonce) ? strtolower($settings->cspNonce) : null;
748
749
        return $cspNonceType;
750
    }
751
752
    /**
753
     * @return string|null
754
     */
755
    private static function getNonce()
756
    {
757
        $result = null;
758
        if (self::getCspNonceType() !== null) {
759
            try {
760
                $result = bin2hex(random_bytes(22));
761
            } catch (\Exception $e) {
762
                // That's okay
763
            }
764
        }
765
766
        return $result;
767
    }
768
    /**
769
     * @param $string
770
     *
771
     * @return null|array
772
     */
773
    private static function jsonFileDecode($string)
774
    {
775
        $json = JsonHelper::decodeIfJson($string);
776
        if (is_string($json)) {
777
            Craft::error('Error decoding JSON file: ' . $json, __METHOD__);
778
            $json = null;
779
        }
780
781
        return $json;
782
    }
783
}
784