Skip to content

Commit

Permalink
support CSP nonce with Vite
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Jul 28, 2022
1 parent 3877ff9 commit 40b8697
Show file tree
Hide file tree
Showing 3 changed files with 481 additions and 21 deletions.
269 changes: 253 additions & 16 deletions src/Illuminate/Foundation/Vite.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,116 @@
namespace Illuminate\Foundation;

use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;

class Vite
{
/**
* The Content Security Policy nonce to apply to all generated tags.
*
* @var string|null
*/
protected static $nonce;

/**
* The key to check for integrity hashes within the manifest.
*
* @var string|bool
*/
protected static $integrityKey = 'integrity';

/**
* The script tag attributes resolvers.
*
* @var array
*/
protected static $scriptTagAttributesResolvers = [];

/**
* The style tag attributes resolvers.
*
* @var array
*/
protected static $styleTagAttributesResolvers = [];

/**
* Generate or set a Content Security Policy nonce to apply to all generated tags.
*
* @param ?string $nonce
* @return string
*/
public static function useCspNonce($nonce = null)
{
return static::$nonce = $nonce ?? Str::random(40);
}

/**
* Get the Content Security Policy nonce applied to all generated tags.
*
* @return string|null
*/
public static function cspNonce()
{
return static::$nonce;
}

/**
* Clear the Content Security Policy nonce.
*
* @return void
*/
public static function withoutCspNonce()
{
static::$nonce = null;
}

/**
* Use the given key to detect integrity hashes in the manifest.
*
* @param bool|string $key
* @return void
*/
public static function useIntegrityKey($key)
{
static::$integrityKey = $key;
}

/**
* Use the given callback to resolve attributes for script tags.
*
* @param callable $callback
* @return void
*/
public static function useAttributesForScriptTag($callback)
{
static::$scriptTagAttributesResolvers[] = $callback;
}

/**
* Use the given callback to resolve attributes for script tags.
*
* @param callable $callback
* @return void
*/
public static function useAttributesForStyleTag($callback)
{
static::$styleTagAttributesResolvers[] = $callback;
}

/**
* Clear the attribute resolvers.
*
* @return void
*/
public static function withoutAttributeResolvers()
{
static::$styleTagAttributesResolvers = [];

static::$scriptTagAttributesResolvers = [];
}

/**
* Generate Vite tags for an entrypoint.
*
Expand All @@ -29,6 +134,7 @@ public function __invoke($entrypoints, $buildDirectory = 'build')

return new HtmlString(
$entrypoints
// TODO: for HMR, should we just pass through an empty array for the chunk?
->map(fn ($entrypoint) => $this->makeTag("{$url}/{$entrypoint}"))
->prepend($this->makeScriptTag("{$url}/@vite/client"))
->join('')
Expand All @@ -54,21 +160,27 @@ public function __invoke($entrypoints, $buildDirectory = 'build')
throw new Exception("Unable to locate file in Vite manifest: {$entrypoint}.");
}

$tags->push($this->makeTag(asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}")));
$tags->push($this->makeTagForChunk(
asset("{$buildDirectory}/{$manifest[$entrypoint]['file']}"),
$manifest[$entrypoint],
$manifest
));

if (isset($manifest[$entrypoint]['css'])) {
foreach ($manifest[$entrypoint]['css'] as $css) {
$tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
}
foreach ($manifest[$entrypoint]['css'] ?? [] as $css) {
$tags->push($this->makeTagForChunk(
asset("{$buildDirectory}/{$css}"),
$manifest[$entrypoint],
$manifest
));
}

if (isset($manifest[$entrypoint]['imports'])) {
foreach ($manifest[$entrypoint]['imports'] as $import) {
if (isset($manifest[$import]['css'])) {
foreach ($manifest[$import]['css'] as $css) {
$tags->push($this->makeStylesheetTag(asset("{$buildDirectory}/{$css}")));
}
}
foreach ($manifest[$entrypoint]['imports'] ?? [] as $import) {
foreach ($manifest[$import]['css'] ?? [] as $css) {
$tags->push(
$this->makeTagForChunk(asset("{$buildDirectory}/{$css}"),
Collection::make($manifest)->firstWhere('file', $css),
$manifest
));
}
}
}
Expand Down Expand Up @@ -108,7 +220,80 @@ public function reactRefresh()
}

/**
* Generate an appropriate tag for the given URL.
* Make tag for the given chunk.
*
* @param string $url
* @param array $chunk
* @param array $manifest
* @return string
*/
protected function makeTagForChunk($url, $chunk, $manifest)
{
if (
static::$nonce === null
&& ! array_key_exists(static::$integrityKey, $chunk)
&& static::$scriptTagAttributesResolvers === []
&& static::$styleTagAttributesResolvers === []) {
return $this->makeTag($url);
}

if ($this->isCssPath($url)) {
return $this->makeStylesheetTagWithAttributes(
$url,
$this->resolveStyleTagAttributes($url, $chunk, $manifest)
);
}

return $this->makeScriptTagWithAttributes(
$url,
$this->resolveScriptTagAttributes($url, $chunk, $manifest)
);
}

/**
* Resolve the attributes for the chunks generated script tag.
*
* @param string $url
* @param array $chunk
* @param array $manifest
* @return array
*/
protected function resolveScriptTagAttributes($url, $chunk, $manifest)
{
$attributes = static::$integrityKey
? ['integrity' => $chunk[static::$integrityKey] ?? false]
: [];

foreach (static::$scriptTagAttributesResolvers as $resolver) {
$attributes = array_merge($attributes, $resolver($chunk, $manifest, $url));
}

return $attributes;
}

/**
* Resolve the attributes for the chunks generated style tag.
*
* @param string $url
* @param array $chunk
* @param array $manifest
* @return array
*/
protected function resolveStyleTagAttributes($url, $chunk, $manifest)
{
$attributes = static::$integrityKey
? ['integrity' => $chunk[static::$integrityKey] ?? false]
: [];

foreach (static::$styleTagAttributesResolvers as $resolver) {
$attributes = array_merge($attributes, $resolver($chunk, $manifest, $url));
}

return $attributes;
}

/**
* Generate an appropriate tag for the given URL in HMR mode.
*
* @param string $url
* @return string
Expand All @@ -130,18 +315,54 @@ protected function makeTag($url)
*/
protected function makeScriptTag($url)
{
return sprintf('<script type="module" src="%s"></script>', $url);
return $this->makeScriptTagWithAttributes($url, []);
}

/**
* Generate a stylesheet tag for the given URL.
* Generate a stylesheet tag for the given URL in HMR mode.
*
* @param string $url
* @return string
*/
protected function makeStylesheetTag($url)
{
return sprintf('<link rel="stylesheet" href="%s" />', $url);
return $this->makeStylesheetTagWithAttributes($url, []);
}

/**
* Generate a script tag with attributes for the given URL.
*
* @param string $url
* @param array $attributes
* @return string
*/
protected function makeScriptTagWithAttributes($url, $attributes)
{
$attributes = $this->parseAttributes(array_merge([
'type' => 'module',
'src' => $url,
'nonce' => static::$nonce ?? false,
], $attributes));

return '<script '.implode(' ', $attributes).'></script>';
}

/**
* Generate a link tag with attributes for the given URL.
*
* @param string $url
* @param array $attributes
* @return string
*/
protected function makeStylesheetTagWithAttributes($url, $attributes)
{
$attributes = $this->parseAttributes(array_merge([
'rel' => 'stylesheet',
'href' => $url,
'nonce' => static::$nonce ?? false,
], $attributes));

return '<link '.implode(' ', $attributes).' />';
}

/**
Expand All @@ -154,4 +375,20 @@ protected function isCssPath($path)
{
return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;
}

/**
* Parse the attributes into key="value" strings.
*
* @param array $attributes
* @return array
*/
protected function parseAttributes($attributes)
{
return Collection::make($attributes)
->reject(fn ($value, $key) => $value === false)
->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])
->map(fn ($value, $key) => is_int($key) ? $value : $key.'="'.$value.'"')
->values()
->all();
}
}
2 changes: 2 additions & 0 deletions src/Illuminate/Support/Facades/Facade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Vite;
use Illuminate\Support\Arr;
use Illuminate\Support\Js;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -293,6 +294,7 @@ public static function defaultAliases()
'URL' => URL::class,
'Validator' => Validator::class,
'View' => View::class,
'Vite' => Vite::class,
]);
}

Expand Down
Loading

0 comments on commit 40b8697

Please sign in to comment.