Passed
Push — v1 ( e51e7f...91c690 )
by Andrew
07:33
created

Manifest::getCspNonceType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 7
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
    // Protected Static Properties
48
    // =========================================================================
49
50
    /**
51
     * @var array
52
     */
53
    protected static $files;
54
55
    /**
56
     * @var bool
57
     */
58
    protected static $isHot = false;
59
60
    // Public Static Methods
61
    // =========================================================================
62
63
    /**
64
     * @param array $config
65
     * @param string $moduleName
66
     * @param bool $async
67
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
68
     *
69
     * @return string
70
     * @throws NotFoundHttpException
71
     */
72
    public static function getCssModuleTags(array $config, string $moduleName, bool $async, array $attributes = []): string
73
    {
74
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
75
        if ($legacyModule === null) {
76
            return '';
77
        }
78
        $lines = [];
79
        if ($async) {
80
            $lines[] = Html::cssFile($legacyModule, array_merge([
81
                'rel' => 'stylesheet',
82
                'media' => 'print',
83
                'onload' => "this.media='all'",
84
            ], $attributes));
85
            $lines[] = Html::cssFile($legacyModule, array_merge([
86
                'rel' => 'stylesheet',
87
                'noscript' => true,
88
            ], $attributes));
89
        } else {
90
            $lines[] = Html::cssFile($legacyModule, array_merge([
91
                'rel' => 'stylesheet',
92
            ], $attributes));
93
        }
94
95
        return implode("\r\n", $lines);
96
    }
97
98
    /**
99
     * @param string $path
100
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
101
     *
102
     * @return string
103
     */
104
    public static function getCssInlineTags(string $path, array $attributes = []): string
105
    {
106
        $result = self::getFile($path);
107
        if ($result) {
108
            $config = [];
109
            $nonce = self::getNonce();
110
            if ($nonce !== null) {
111
                $config['nonce'] = $nonce;
112
                self::includeNonce($nonce, 'style-src');
113
            }
114
            $result = Html::style($result, array_merge($config, $attributes));
115
116
            return $result;
117
        }
118
119
        return '';
120
    }
121
122
    /**
123
     * @param array $config
124
     * @param null|string $name
125
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
126
     *
127
     * @return string
128
     * @throws \Twig\Error\LoaderError
129
     */
130
    public static function getCriticalCssTags(array $config, $name = null, array $attributes = []): string
131
    {
132
        // Resolve the template name
133
        $template = Craft::$app->getView()->resolveTemplate($name ?? Twigpack::$templateName ?? '');
134
        if ($template) {
135
            $name = self::combinePaths(
136
                pathinfo($template, PATHINFO_DIRNAME),
137
                pathinfo($template, PATHINFO_FILENAME)
138
            );
139
            $dirPrefix = 'templates/';
140
            if (defined('CRAFT_TEMPLATES_PATH')) {
141
                $dirPrefix = CRAFT_TEMPLATES_PATH;
0 ignored issues
show
Bug introduced by
The constant nystudio107\twigpack\helpers\CRAFT_TEMPLATES_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
142
            }
143
            $name = strstr($name, $dirPrefix);
144
            $name = (string)str_replace($dirPrefix, '', $name);
145
            $path = self::combinePaths(
146
                    $config['localFiles']['basePath'],
147
                    $config['localFiles']['criticalPrefix'],
148
                    $name
149
                ) . $config['localFiles']['criticalSuffix'];
150
151
            return self::getCssInlineTags($path, $attributes);
152
        }
153
154
        return '';
155
    }
156
157
    /**
158
     * Returns the uglified loadCSS rel=preload Polyfill as per:
159
     * https://github.com/filamentgroup/loadCSS#how-to-use-loadcss-recommended-example
160
     *
161
     * @return string
162
     * @throws \craft\errors\DeprecationException
163
     * @deprecated in 1.2.0
164
     */
165
    public static function getCssRelPreloadPolyfill(): string
166
    {
167
        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.');
168
169
        return '';
170
    }
171
172
    /**
173
     * @param array $config
174
     * @param string $moduleName
175
     * @param bool $async
176
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
177
     *
178
     * @return null|string
179
     * @throws NotFoundHttpException
180
     */
181
    public static function getJsModuleTags(array $config, string $moduleName, bool $async, array $attributes = [])
182
    {
183
        $legacyModule = self::getModule($config, $moduleName, 'legacy', true);
184
        if ($legacyModule === null) {
185
            return '';
186
        }
187
        $modernModule = '';
188
        if ($async) {
189
            $modernModule = self::getModule($config, $moduleName, 'modern', true);
190
            if ($modernModule === null) {
191
                return '';
192
            }
193
        }
194
        $lines = [];
195
        if ($async) {
196
            $lines[] = Html::jsFile($modernModule, array_merge([
197
                'type' => 'module',
198
            ], $attributes));
199
            $lines[] = Html::jsFile($legacyModule, array_merge([
200
                'nomodule' => true,
201
            ], $attributes));
202
        } else {
203
            $lines[] = Html::jsFile($legacyModule, array_merge([
204
            ], $attributes));
205
        }
206
207
        return implode("\r\n", $lines);
208
    }
209
210
    /**
211
     * Safari 10.1 supports modules, but does not support the `nomodule`
212
     * attribute - it will load <script nomodule> anyway. This snippet solve
213
     * this problem, but only for script tags that load external code, e.g.:
214
     * <script nomodule src="nomodule.js"></script>
215
     *
216
     * Again: this will **not* # prevent inline script, e.g.:
217
     * <script nomodule>alert('no modules');</script>.
218
     *
219
     * This workaround is possible because Safari supports the non-standard
220
     * 'beforeload' event. This allows us to trap the module and nomodule load.
221
     *
222
     * Note also that `nomodule` is supported in later versions of Safari -
223
     * it's just 10.1 that omits this attribute.
224
     *
225
     * c.f.: https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc
226
     *
227
     * @param array $attributes additional HTML key/value pair attributes to add to the resulting tag
228
     *
229
     * @return string
230
     */
231
    public static function getSafariNomoduleFix(array $attributes = []): string
232
    {
233
        $code = /** @lang JavaScript */
234
            <<<EOT
235
!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()}}();
236
EOT;
237
        $config = [];
238
        $nonce = self::getNonce();
239
        if ($nonce !== null) {
240
            $config['nonce'] = $nonce;
241
            self::includeNonce($nonce, 'script-src');
242
        }
243
244
        return Html::script($code, array_merge($config, $attributes));
245
    }
246
247
    /**
248
     * Return the URI to a module
249
     *
250
     * @param array $config
251
     * @param string $moduleName
252
     * @param string $type
253
     * @param bool $soft
254
     *
255
     * @return null|string
256
     * @throws NotFoundHttpException
257
     */
258
    public static function getModule(array $config, string $moduleName, string $type = 'modern', bool $soft = false)
259
    {
260
        // Get the module entry
261
        $module = self::getModuleEntry($config, $moduleName, $type, $soft);
262
        if ($module !== null) {
263
            $prefix = self::$isHot
264
                ? $config['devServer']['publicPath']
265
                : $config['server']['publicPath'];
266
            $useAbsoluteUrl = $config['useAbsoluteUrl'];
267
            // If the module isn't a full URL, prefix it as required
268
            if ($useAbsoluteUrl && !UrlHelper::isAbsoluteUrl($module)) {
269
                $module = self::combinePaths($prefix, $module);
270
            }
271
            // Resolve any aliases
272
            $alias = Craft::getAlias($module, false);
273
            if ($alias) {
274
                $module = $alias;
275
            }
276
            // Make sure it's a full URL, as required
277
            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

277
            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

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

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