Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,29 @@ $factors['years'] = [365, 'dayz'];

More information on CarbonInterval's gotchas in [Constantin's blog post on chasingcode.dev](https://chasingcode.dev/blog/carbon-php-practical-examples/).

### Content Security Policy

This package supports CSP nonces for secure script loading. Pass your nonce to the @cookieconsentscripts directive:

```blade
{{-- Basic usage with nonce --}}
@cookieconsentscripts($yourNonce)

{{-- Example with Spatie Laravel CSP --}}
@cookieconsentscripts(app('csp-nonce'))

{{-- Without CSP --}}
@cookieconsentscripts
```

How It Works

When you provide a nonce, it's added to the script tag:

```html
<script src="/cookie-consent-script" nonce="your-nonce-here" defer></script>
```

### Let your users change their mind

Users should be able to change their consent settings at any time. No worries, with this package it is quite simple to achieve: generate a button that will reset the user's cookies and show the consent modal again.
Expand Down
4 changes: 2 additions & 2 deletions dist/cookies.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"/script.js": "/script.js",
"/cookies.js": "/cookies.js",
"/Cookies.js": "/Cookies.js",
"/style.css": "/style.css"
}
2 changes: 1 addition & 1 deletion dist/script.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/AnalyticCookiesCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public function google(string $id, bool $anonymizeIp = true): static
->description(__('cookieConsent::cookies.defaults._gat'))
)
->accepted(fn(Consent $consent) => $consent
->script('<script async src="https://www.googletagmanager.com/gtag/js?id=' . $id . '"></script>')
->script('<script nonce="" async src="https://www.googletagmanager.com/gtag/js?id=' . $id . '"></script>')
->script(
'<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag(\'js\',new Date());gtag(\'config\',\'' . $id . '\', {\'anonymize_ip\':' . $anonymizeIp . '});</script>'
'<script nonce="">window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag(\'js\',new Date());gtag(\'config\',\'' . $id . '\', {\'anonymize_ip\':' . $anonymizeIp . '});</script>'
)
);
});
Expand Down
16 changes: 10 additions & 6 deletions src/ConsentResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ConsentResponse
/**
* Transform the collected data into a JSON response-object.
*/
public function handleConsent(Cookie|CookiesGroup $instance): static
public function handleConsent(Cookie|CookiesGroup $instance, string|null $nonce): static
{
if(! $instance->hasConsentCallback()) {
return $this;
Expand All @@ -35,7 +35,7 @@ public function handleConsent(Cookie|CookiesGroup $instance): static
$consent = $instance->getConsentResult();

$this->attachCookies($consent->getCookies());
$this->attachScripts($consent->getScripts());
$this->attachScripts($consent->getScripts(), $nonce);

return $this;
}
Expand Down Expand Up @@ -65,20 +65,24 @@ public function attachCookie(CookieComponent $cookie): static
/**
* Add multiple script tags to the consent response.
*/
public function attachScripts(array $tags): static
public function attachScripts(array $tags, string|null $nonce): static
{
foreach ($tags as $tag) {
$this->attachScript($tag);
$this->attachScript($tag, $nonce);
}

return $this;
}

/**
* Add a single script tag to the consent response.
*/
public function attachScript(string $tag): static
public function attachScript(string $tag, ?string $nonce = null): static
{
if ($nonce && str_contains($tag, 'nonce=""')) {
$tag = str_replace('nonce=""', 'nonce="' . $nonce . '"', $tag);
}

$this->scripts[] = $tag;

return $this;
Expand Down
52 changes: 31 additions & 21 deletions src/CookiesManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

class CookiesManager
{
protected string|null $nonce = null;
/**
* The cookies registrar.
*/
Expand Down Expand Up @@ -141,13 +142,22 @@ public function accept(string|array $categories = '*'): ConsentResponse
*/
protected function getConsentResponse(): ConsentResponse
{
return array_reduce($this->registrar->getCategories(), function($response, $category) {
return array_reduce($category->getDefined(), function(ConsentResponse $response, Cookie|CookiesGroup $instance) {
return $this->hasConsentFor($instance->name)
? $response->handleConsent($instance)
: $response;
}, $response);
}, new ConsentResponse());
$nonce = $this->nonce;
return array_reduce(
$this->registrar->getCategories(),
function($response, $category) use ($nonce) {
return array_reduce(
$category->getDefined(),
function(ConsentResponse $response, Cookie|CookiesGroup $instance) use ($nonce) {
return $this->hasConsentFor($instance->name)
? $response->handleConsent($instance, $nonce)
: $response;
},
$response
);
},
new ConsentResponse()
);
}

/**
Expand All @@ -167,11 +177,12 @@ protected function makeConsentCookie(): CookieComponent
/**
* Output all the scripts for current consent state.
*/
public function renderScripts(bool $withDefault = true): string
public function renderScripts(string|null $nonce, bool $withDefault = true): string
{
$this->nonce = $nonce;
$output = $this->shouldDisplayNotice()
? $this->getNoticeScripts($withDefault)
: $this->getConsentedScripts($withDefault);
? $this->getNoticeScripts($nonce, $withDefault)
: $this->getConsentedScripts($nonce, $withDefault);

if(strlen($output)) {
$output = '<!-- Cookie Consent -->' . $output;
Expand All @@ -180,27 +191,28 @@ public function renderScripts(bool $withDefault = true): string
return $output;
}

public function getNoticeScripts(bool $withDefault): string
public function getNoticeScripts(string|null $nonce, bool $withDefault): string
{
return $withDefault ? $this->getDefaultScriptTag() : '';
return $withDefault ? $this->getDefaultScriptTag($nonce) : '';
}

protected function getConsentedScripts(bool $withDefault): string
protected function getConsentedScripts(string|null $nonce, bool $withDefault): string
{
$output = $this->getNoticeScripts($withDefault);
$output = $this->getNoticeScripts($nonce, $withDefault);

foreach ($this->getConsentResponse()->getResponseScripts() ?? [] as $tag) {
foreach ($this->getConsentResponse($nonce)->getResponseScripts() ?? [] as $tag) {
$output .= $tag;
}

return $output;
}

protected function getDefaultScriptTag(): string
protected function getDefaultScriptTag(string|null $nonce): string
{
return '<script '
. 'src="' . route('cookieconsent.script') . '?id='
. md5(\filemtime(LCC_ROOT . '/dist/script.js')) . '" '
. md5(\filemtime(LCC_ROOT . '/dist/script.js')) . '" '
. ($nonce ? 'nonce="' . $nonce . '" ' : '')
. 'defer'
. '></script>';
}
Expand Down Expand Up @@ -282,16 +294,14 @@ public function replaceInfoTag(string $wysiwyg): string
$cookieConsentInfo = view('cookie-consent::info', [
'cookies' => $this->registrar,
])->render();
$formattedString = preg_replace(

return preg_replace(
[
'/\<(\w)[^\>]+\>\@cookieconsentinfo\<\/\1\>/',
'/\@cookieconsentinfo/',
],
$cookieConsentInfo,
$wysiwyg,
);

return $formattedString;
}
}
5 changes: 3 additions & 2 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ public function boot()
protected function registerBladeDirectives()
{
Blade::directive('cookieconsentscripts', function (string $expression) {
return '<?php echo ' . Facades\Cookies::class . '::renderScripts(' . $expression . '); ?>';
});
$expression = $expression ?: 'null';
return "<?php echo " . Facades\Cookies::class . '::renderScripts('. $expression .', true); ?>';

});
Blade::directive('cookieconsentview', function (string $expression) {
return '<?php echo ' . Facades\Cookies::class . '::renderView(); ?>';
});
Expand Down
2 changes: 1 addition & 1 deletion webpack.mix.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ let mix = require('laravel-mix');

mix.setPublicPath('dist')
.js('resources/js/script.js', 'dist')
.js('resources/js/cookies.js', 'dist')
.js('resources/js/Cookies.js', 'dist')
.sass('resources/scss/style.scss', 'dist');