Passed
Push — v1 ( 17c68e...f43f25 )
by Andrew
19:03 queued 12:47
created

Manifest::getHttpResponseCode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 5
rs 10
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
26
/**
27
 * @author    nystudio107
28
 * @package   Twigpack
29
 * @since     1.0.0
30
 */
31
class Manifest
32
{
33
    // Constants
34
    // =========================================================================
35
36
    const CACHE_KEY = 'twigpack';
37
    const CACHE_TAG = 'twigpack';
38
39
    const DEVMODE_CACHE_DURATION = 1;
40
41
    const CSP_HEADERS = [
42
        'Content-Security-Policy',
43
        'X-Content-Security-Policy',
44
        'X-WebKit-CSP',
45
    ];
46
47
    const SUPPRESS_ERRORS_FOR_MODULES = [
48
        'styles.js',
49
    ];
50
51
    // Protected Static Properties
52
    // =========================================================================
53
54
    /**
55
     * @var array
56
     */
57
    protected static $files;
58
59
    /**
60
     * @var bool
61
     */
62
    protected static $isHot = false;
63
64
    // Public Static Methods
65
    // =========================================================================
66
67
    /**
68
     * @param array $config
69
     * @param string $moduleName
70
     * @param bool $async
71
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
72
     *
73
     * @return string
74
     * @throws NotFoundHttpException
75
     */
76
    public static function getCssModuleTags(array $config, string $moduleName, bool $async, array $attributes = []): string
77
    {
78
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
79
        if ($legacyModule === null) {
80
            return '';
81
        }
82
        $lines = [];
83
        if ($async) {
84
            $lines[] = Html::cssFile($legacyModule, array_merge([
85
                'rel' => 'stylesheet',
86
                'media' => 'print',
87
                'onload' => "this.media='all'",
88
            ], $attributes));
89
            $lines[] = Html::cssFile($legacyModule, array_merge([
90
                'rel' => 'stylesheet',
91
                'noscript' => true,
92
            ], $attributes));
93
        } else {
94
            $lines[] = Html::cssFile($legacyModule, array_merge([
95
                'rel' => 'stylesheet',
96
            ], $attributes));
97
        }
98
99
        return implode("\r\n", $lines);
100
    }
101
102
    /**
103
     * @param string $path
104
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
105
     *
106
     * @return string
107
     */
108
    public static function getCssInlineTags(string $path, array $attributes = []): string
109
    {
110
        $result = self::getFile($path);
111
        if ($result) {
112
            $config = [];
113
            $nonce = self::getNonce();
114
            if ($nonce !== null) {
115
                $config['nonce'] = $nonce;
116
                self::includeNonce($nonce, 'style-src');
117
            }
118
            $result = Html::style($result, array_merge($config, $attributes));
119
120
            return $result;
121
        }
122
123
        return '';
124
    }
125
126
    /**
127
     * @param array $config
128
     * @param null|string $name
129
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
130
     *
131
     * @return string
132
     * @throws \Twig\Error\LoaderError
133
     */
134
    public static function getCriticalCssTags(array $config, $name = null, array $attributes = []): string
135
    {
136
        // Resolve the template name
137
        $template = Craft::$app->getView()->resolveTemplate($name ?? Twigpack::$templateName ?? '');
138
        if ($template) {
139
            $name = self::combinePaths(
140
                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

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

281
            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

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

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