├── .github
├── FUNDING.yml
└── workflows
│ └── run-tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── config
└── cookieconsent.php
├── dist
├── cookies.js
├── cookies.js.LICENSE.txt
├── mix-manifest.json
├── script.js
└── style.css
├── laravel-cookie-consent.gif
├── package.json
├── phpunit.xml
├── resources
├── js
│ ├── Cookies.js
│ └── script.js
├── lang
│ ├── ca
│ │ └── cookies.php
│ ├── cs
│ │ └── cookies.php
│ ├── de
│ │ └── cookies.php
│ ├── en
│ │ └── cookies.php
│ ├── es
│ │ └── cookies.php
│ ├── fr
│ │ └── cookies.php
│ ├── hr
│ │ └── cookies.php
│ ├── it
│ │ └── cookies.php
│ ├── nb
│ │ └── cookies.php
│ ├── nl
│ │ └── cookies.php
│ ├── pl
│ │ └── cookies.php
│ ├── pt
│ │ └── cookies.php
│ ├── ru
│ │ └── cookies.php
│ └── uk
│ │ └── cookies.php
├── scss
│ └── style.scss
└── views
│ ├── button.blade.php
│ ├── cookies.blade.php
│ └── info.blade.php
├── routes
└── web.php
├── src
├── AnalyticCookiesCategory.php
├── Concerns
│ ├── HasAttributes.php
│ ├── HasConsentCallback.php
│ ├── HasCookies.php
│ └── HasTranslations.php
├── Consent.php
├── ConsentResponse.php
├── Cookie.php
├── CookiesCategory.php
├── CookiesGroup.php
├── CookiesManager.php
├── CookiesRegistrar.php
├── CookiesServiceProvider.php
├── EssentialCookiesCategory.php
├── Facades
│ └── Cookies.php
├── Http
│ └── Controllers
│ │ ├── AcceptAllController.php
│ │ ├── AcceptEssentialsController.php
│ │ ├── ConfigureController.php
│ │ ├── ResetController.php
│ │ └── ScriptController.php
└── ServiceProvider.php
├── stubs
└── CookiesServiceProvider.php
├── tests
├── Feature
│ ├── FacadeTest.php
│ └── ServiceProviderTest.php
├── OrchestraTestCase.php
├── Pest.php
├── PhpUnitTestCase.php
└── Unit
│ ├── AnalyticCookiesCategoryTest.php
│ ├── CookieTest.php
│ ├── CookiesCategoryTest.php
│ ├── CookiesRegistrarTest.php
│ ├── EssentialCookiesCategoryTest.php
│ ├── HasAttributesTest.php
│ ├── HasConsentCallbackTest.php
│ └── HasCookiesTest.php
└── webpack.mix.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: whitecube
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | os: [ubuntu-latest]
12 | php: [8.4, 8.3, 8.2]
13 | laravel: [12.*, 11.*, 10.*]
14 | stability: [prefer-stable]
15 | include:
16 | - laravel: 12.*
17 | testbench: 10.*
18 | - laravel: 11.*
19 | testbench: 9.*
20 | - laravel: 10.*
21 | testbench: 8.*
22 |
23 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}
24 |
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v4
28 |
29 | - name: Setup PHP
30 | uses: shivammathur/setup-php@v2
31 | with:
32 | php-version: ${{ matrix.php }}
33 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
34 | coverage: none
35 |
36 | - name: Install dependencies
37 | run: |
38 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction
39 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction
40 |
41 | - name: Execute tests
42 | run: vendor/bin/pest --color=always
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /node_modules
3 | .idea
4 | .DS_Store
5 | Thumbs.db
6 | composer.lock
7 | package-lock.json
8 | yarn.lock
9 | .phpunit.result.cache
10 | .php-cs-fixer.cache
11 | .swp
12 | *.map
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Whitecube
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Cookie Consent
2 |
3 | > ✅ 100% GDPR compliant
4 | > ✅ Fully customizable
5 | > ✅ Works with and without JS
6 |
7 | Under the [EU’s GDPR](http://ec.europa.eu/ipg/basics/legal/cookies/index_en.htm#section_2), cookies that are not strictly necessary for the basic function of your website must only be activated after your end-users have given their explicit consent to the specific purpose of their operation and collection of personal data. Despite some crazy arbitrary requirements decided by non-technical lawmakers, overall **this is a good thing** since it pushes our profession to a more respectful and user-friendly direction. More and more non-EU citizens are expecting websites to ask for their consent, potentially including your website's target audience too.
8 |
9 | Let's face it, most of the time you _could_ use alternatives for services requiring cookie usage. The most common cases being analytics tools, such as Google Analytics, which can easily be replaced by:
10 |
11 | - [Fathom](https://usefathom.com/): built by some fellow Laravel community members!
12 | - [Plausible](https://plausible.io/): made and hosted in the EU.
13 | - [Pirsch](https://pirsch.io/): made and hosted in the EU (Germany).
14 | - ... want to add a useful competitor to this list? Open an issue!
15 |
16 | The main advantage of using these alternatives is that you could avoid asking for explicit consent altogether since you would not use any cookies that aren't strictly necessary. This will always be better for your application's accessibility and user experience.
17 |
18 | Nevertheless, this package provides all the tools you'll need to cover a proper EU-compliant cookies policy:
19 |
20 | - Cookies registration & configuration
21 | - Blade views & translation files for consent alerts & pop-ups
22 | - Blade directives & Facade methods making your life easier
23 | - JavaScript code that will enhance front-end user experience
24 |
25 | We've built this package with flexibility in our mind: you'll be able to customize content, behavior and styling as you wish. Here is what it looks like out of the box:
26 |
27 | 
28 |
29 | ## Table of contents
30 |
31 | 1. [Installation](#installation)
32 | 2. [Usage](#usage)
33 | 3. [Registering cookies](#registering-cookies)
34 | - [Choosing a cookie category](#choosing-a-cookie-category)
35 | - [Cookie definition](#cookie-definition)
36 | 4. [Checking for consent](#checking-for-consent)
37 | - [Using the Cookies facade](#using-the-cookies-facade)
38 | - [Using dependency injection](#using-dependency-injection)
39 | 5. [Customization](#customization)
40 | - [The views](#the-views)
41 | - [Styling](#styling)
42 | - [Javascript](#javascript)
43 | - [Textual content and translations](#textual-content-and-translations)
44 | 6. [A few useful tips](#a-few-useful-tips)
45 | - [Cookie Policy Details Page](#cookie-policy-details-page)
46 | - [Let your users change their mind](#let-your-users-change-their-mind)
47 | - [Storing user preferences for multiple sub-domains](#storing-user-preferences-for-multiple-sub-domains)
48 | - [Keep it accessible](#keep-it-accessible)
49 |
50 | ## Installation
51 |
52 | ```bash
53 | composer require whitecube/laravel-cookie-consent
54 | ```
55 |
56 | This package will auto-register its service provider.
57 |
58 | ## Usage
59 |
60 | First, publish the package's files:
61 |
62 | 1. Publish the `CookiesServiceProvider` file: `php artisan vendor:publish --tag=laravel-cookie-consent-service-provider`
63 | 2. Register the Service Provider in your application. For applications using Laravel 9 or 10, add the Service Provider to the `providers` array in `config/app.php`:
64 | ```php
65 | 'providers' => ServiceProvider::defaultProviders()->merge([
66 | // ...
67 | App\Providers\RouteServiceProvider::class,
68 | // IMPORTANT: add the following line AFTER "App\Providers\RouteServiceProvider::class,"
69 | App\Providers\CookiesServiceProvider::class,
70 | ])->toArray(),
71 | ```
72 |
73 | For applications running Laravel 11 and above, add the Service Provider to the array in `bootstrap/providers.php`:
74 | ```php
75 | return [
76 | App\Providers\AppServiceProvider::class,
77 | App\Providers\CookiesServiceProvider::class,
78 | ];
79 | ```
80 | 3. Publish the configuration file: `php artisan vendor:publish --tag=laravel-cookie-consent-config`
81 |
82 | If you want to customize the consent modal's views:
83 |
84 | 1. Publish the customizable views: `php artisan vendor:publish --tag=laravel-cookie-consent-views`
85 | 2. Publish the translation files: `php artisan vendor:publish --tag=laravel-cookie-consent-lang`
86 |
87 | More on [customization](#customization) below.
88 |
89 | Now, we'll have to register and configure the used cookies in the freshly published `App\Providers\CookiesServiceProvider::registerCookies()` method:
90 |
91 | ```php
92 | namespace App\Providers;
93 |
94 | use Whitecube\LaravelCookieConsent\Consent;
95 | use Whitecube\LaravelCookieConsent\Facades\Cookies;
96 | use Whitecube\LaravelCookieConsent\CookiesServiceProvider as ServiceProvider;
97 |
98 | class CookiesServiceProvider extends ServiceProvider
99 | {
100 | /**
101 | * Define the cookies users should be aware of.
102 | */
103 | protected function registerCookies(): void
104 | {
105 | if (app()->environment() === 'production') {
106 | // Register Laravel's base cookies under the "required" cookies section:
107 | Cookies::essentials()
108 | ->session()
109 | ->csrf();
110 |
111 | // Register all Analytics cookies at once using one single shorthand method:
112 | Cookies::analytics()
113 | ->google(
114 | id: env('GOOGLE_ANALYTICS_ID')
115 | anonymizeIp: env('GOOGLE_ANALYTICS_ANONYMIZE_IP')
116 | );
117 |
118 | // Register custom cookies under the pre-existing "optional" category:
119 | Cookies::optional()
120 | ->name('darkmode_enabled')
121 | ->description('This cookie helps us remember your preferences regarding the interface\'s brightness.')
122 | ->duration(120)
123 | ->accepted(fn(Consent $consent, MyDarkmode $darkmode) => $consent->cookie(value: $darkmode->getDefaultValue()));
124 | }
125 | }
126 | }
127 | ```
128 |
129 | More details on the available [cookie registration](#registering-cookies) methods below.
130 |
131 | Then, let's add consent scripts and modals to the application's views using the following blade directives:
132 |
133 | - `@cookieconsentscripts`: used to add the package's default JavaScript and any third-party scripts you need to get the end-user's consent for.
134 | - `@cookieconsentview`: used to render the alert or pop-up view.
135 |
136 | ```blade
137 |
138 |
139 |
140 |
141 | @cookieconsentscripts
142 |
143 |
144 |
145 | @cookieconsentview
146 |
147 |
148 | ```
149 |
150 | ## Registering cookies
151 |
152 | This package aims to centralize cookie declaration and documentation at the same place in order to keep projects maintainable. However, the suggested methodology is not mandatory. If you wish to queue cookies or execute code upon consent somewhere else in your app's codebase, feel free to do so: we have a few available methods that can come in handy when you'll need to [check if consent has been granted](#checking-for-consent) during the request's lifecycle.
153 |
154 | ### Choosing a cookie category
155 |
156 | All registered cookies are attached to a Cookie Category, which is a convenient way to group cookies under similar topics. The aimed objective is to add usability to the detailed information views by providing understandable and summarized sections.
157 |
158 | Instead of consenting each cookie individually, users grant consent to those categories. All cookies included in such a consented category will automatically be considered as given explicit consent to.
159 |
160 | There are 3 base categories included in this package:
161 |
162 | 1. `Cookies::essentials()`: lists all cookies that add required functionality to the app. This category cannot be opted-out and automatically contains the package's consent cookie.
163 | - `Cookies::essentials()->session()`: registers Laravel's "session" cookie (defined in your app's `session.cookie` configuration) ;
164 | - `Cookies::essentials()->csrf()`: registers [Laravel's "XSRF-TOKEN"](https://laravel.com/docs/10.x/csrf) cookie.
165 | 2. `Cookies::analytics()`: lists all cookies used for statistics and data collection.
166 | - `Cookies::analytics()->google(string $trackingId, bool $anonymizeIp)`: automatically lists all Google Analytics' cookies. **This will also automatically register Google Analytics' JS scripts and inject them to the layout's `` only when consent is granted.** Convenient, huh?
167 | 3. `Cookies::optional()`: lists all cookies that serve some kind of utility feature. Since this category can ben opted-out, linked features should always check if consent has been granted before queuing or relying on their cookies.
168 |
169 | You are free to add as many custom categories as you want. To do so, simply call the `category(string $key, ?Closure $maker = null)` method on the `Cookies` facade:
170 |
171 | ```php
172 | use Whitecube\LaravelCookieConsent\Facades\Cookies;
173 |
174 | $category = Cookies::category(key: 'my-custom-category');
175 | ```
176 |
177 | The optional second parameter, `Closure $maker`, can be used to define a custom `CookiesCategory` instance:
178 |
179 | ```php
180 | use Whitecube\LaravelCookieConsent\Facades\Cookies;
181 |
182 | $category = Cookies::category(key: 'my-custom-category', maker: function(string $key) {
183 | return new MyCustomCategory($key);
184 | });
185 | ```
186 |
187 | Custom category classes should extend `Whitecube\LaravelCookieConsent\CookiesCategory`.
188 |
189 | Once defined, custom categories can be accessed using their own camel-case method:
190 |
191 | ```php
192 | use Whitecube\LaravelCookieConsent\Facades\Cookies;
193 |
194 | $category = Cookies::myCustomCategory();
195 | ```
196 |
197 | In order to add human-readable titles and descriptions to categories, you should insert new lines to the `cookieConsent::cookies.categories.[category-key]` translations. More information on [translations](#textual-content-and-translations) below.
198 |
199 | ```php
200 | return [
201 | // ...
202 | 'categories' => [
203 | // ...
204 | 'my-custom-category' => [
205 | 'title' => 'My custom category of cookies',
206 | 'description' => 'A short description of what these cookies are meant for.',
207 | ],
208 | // ...
209 | ],
210 | ];
211 | ```
212 |
213 | ### Cookie definition
214 |
215 | Once a category has been targetted, you can start defining cookies in it using the following methods:
216 |
217 | ```php
218 | Cookies::essentials() // Targetting a category
219 | ->name('darkmode_enabled') // Defining a cookie
220 | ->description('Lorem ipsum') // Adding the cookie's description for display
221 | ->duration(120); // Adding the cookie's lifetime in minutes
222 | ```
223 |
224 | Using these methods you'll have to define each cookie by calling a category each time. For convenience it is also possible to chain cookie definitions using the chainable `cookie(Closure|Cookie $cookie)` method:
225 |
226 | ```php
227 | use Whitecube\LaravelCookieConsent\Cookie;
228 |
229 | Cookies::essentials() // Targetting a category
230 | ->cookie(function(Cookie $cookie) {
231 | $cookie->name('darkmode_enabled') // Defining a cookie
232 | ->description('Lorem ipsum') // Adding the cookie's description for display
233 | ->duration(120); // Adding the cookie's lifetime in minutes
234 | })
235 | ->cookie(function(Cookie $cookie) {
236 | $cookie->name('high_contrast_enabled') // Defining a cookie
237 | ->description('Lorem ipsum') // Adding the cookie's description for display
238 | ->duration(60 * 24 * 365); // Adding the cookie's lifetime in minutes
239 | });
240 | ```
241 |
242 | #### `name(string $name)`
243 |
244 | Required. Defines the cookie name. It is used for display and as the actual cookie "key" when setting the cookie.
245 |
246 | #### `description(string $description)`
247 |
248 | Optional. Adds a textual description for the cookie. It is used for display only.
249 |
250 | #### `duration(int $minutes)`
251 |
252 | Required. Defines the cookie's lifetime in minutes. It is used for display and for the actual cookie expiration date when setting the cookie.
253 |
254 | #### `accepted(Closure $callback)`
255 |
256 | The optional "accepted" callback gets invoked when consent is granted to the category a cookie is attached to. This happens once the user configures their cookie preferences but also each time an incoming request is handled afterwards.
257 |
258 | The callback receives at least one parameter, `Consent $consent`, which is an object used to configure consent output:
259 |
260 | - `script(string $tag)`: defines a script tag that will be added to the layout's `` only when consent has been granted ;
261 | - `cookie(string $value, ?string $path = null, ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null)`: defines a cookie that will be added to the response when consent has been granted. Note that it doesn't need a name and a duration anymore since those settings have already been defined using the `name()` and `duration()` methods described above.
262 |
263 | ```php
264 | use Whitecube\LaravelCookieConsent\Consent;
265 |
266 | $cookie->accepted(function(Consent $consent) {
267 | $consent->cookie(value: 'off')->script('');
268 | });
269 | ```
270 |
271 | Other parameters can be type-hinted and will be resolved by Laravel's Service Container:
272 |
273 | ```php
274 | use App\Services\MyDependencyService;
275 | use Whitecube\LaravelCookieConsent\Consent;
276 |
277 | $cookie->accepted(function(Consent $consent, MyDependencyService $service) {
278 | $consent->script($service->getScriptTag());
279 | });
280 | ```
281 |
282 | #### Custom cookie attributes
283 |
284 | When building your own cookie notice designs, you might need extra attributes on the `Cookie` instances. We've got you covered!
285 |
286 | ```php
287 | $cookie->color = 'warning';
288 |
289 | echo $cookie->color; // "warning"
290 | ```
291 |
292 | Behind the scenes, these magic attributes use the `setAttribute` and `getAttribute` methods:
293 |
294 | ```php
295 | $cookie->setAttribute('icon', 'brightness');
296 |
297 | echo $cookie->getAttribute('icon'); // "brightness"
298 | ```
299 |
300 | But since all other cookie definition methods are chainable, you can also call custom attributes as chainable methods:
301 |
302 | ```php
303 | $cookie->subtitle('Darkmode preferences')->checkmark(true);
304 |
305 | echo $cookie->subtitle; // "brightness"
306 | echo $cookie->checkmark ? 'on' : 'off'; // "on"
307 | ```
308 |
309 | ## Checking for consent
310 |
311 | There are several ways to check for explicit user consent, each of them being useful in different contexts.
312 |
313 | ### Using the `Cookies` facade
314 |
315 | The `Cookies` facade is automatically discovered when installing this package.
316 |
317 | ```php
318 | use Whitecube\LaravelCookieConsent\Facades\Cookies;
319 |
320 | if(Cookies::hasConsentFor('my_cookie_name')) {
321 | // ...
322 | }
323 | ```
324 |
325 | ### Using dependency injection
326 |
327 | Useful when working with methods resolved by Laravel's Service Container:
328 |
329 | ```php
330 | use Whitecube\LaravelCookieConsent\CookiesManager;
331 |
332 | class FooController
333 | {
334 | public function __invoke(CookiesManager $cookies)
335 | {
336 | if($cookies->hasConsentFor('my_cookie_name')) {
337 | // ...
338 | }
339 | }
340 | }
341 | ```
342 |
343 | ## Customization
344 |
345 | Cookie notices are boring and this package's default design is no different. It has been built in a robust, accessible and neutral way so it could serve as many situations as possible.
346 |
347 | However, this world shouldn't be a boring place and even if cookie notices are part of a project's legal requirements, why not use it as an opportunity to bring a smile to your audience's face? Cookie modals are now integrated in every digital platform's user experience and therefore they should blend in accordingly: that's why we've built this package with full flexibility in our mind.
348 |
349 | ### The views
350 |
351 | A good starting point is to take a look at this package's default markup. If not already published, you can access the views using `php artisan vendor:publish --tag=laravel-cookie-consent-views`, this will copy our blade files to your app's `resources/views/vendor/cookie-consent` directory.
352 |
353 | Here you can express your unlimited creativity and push the boundaries of conventionnal Cookie notices or popups.
354 |
355 | When rendered, the view has access to these variables:
356 |
357 | - `$policy`: the URL to your app's Cookie Policy page when defined. To do so, take a look at the package's `cookieconsent.php` configuration file.
358 | - `$cookies`: the registered cookie categories with their attached cookie definitions.
359 |
360 | In order to add buttons, we'd recommend using the package's `@cookieconsentbutton()` blade directive:
361 |
362 | - `@cookieconsentbutton('accept.all')`: renders a button targetting this package's "consent to all cookies" API route ;
363 | - `@cookieconsentbutton('accept.essentials')`: renders a button targetting this package's "consent to essential cookies only" API route ;
364 | - `@cookieconsentbutton('accept.configuration')`: renders a button targetting this package's "consent to custom cookies selection" API route. Beware that this route requires the selected cookie categories as the request's payload ;
365 | - `@cookieconsentbutton('reset')`: renders a button targetting this package's "reset cookie configuration" API route.
366 |
367 | ### Styling
368 |
369 | As you probably noticed, we've included our design's CSS directly in the `cookies.blade.php` view using a `
78 |
--------------------------------------------------------------------------------
/resources/views/info.blade.php:
--------------------------------------------------------------------------------
1 | @foreach($cookies->getCategories() as $category)
2 | {{ $category->title }}
3 |
4 |
5 | @lang('cookieConsent::cookies.cookie') |
6 | @lang('cookieConsent::cookies.purpose') |
7 | @lang('cookieConsent::cookies.duration') |
8 |
9 |
10 | @foreach($category->getCookies() as $cookie)
11 |
12 | {{ $cookie->name }} |
13 | {{ $cookie->description }} |
14 | {{ \Carbon\CarbonInterval::minutes($cookie->duration)->cascade() }} |
15 |
16 | @endforeach
17 |
18 |
19 | @endforeach
20 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | 'cookieconsent.',
12 | 'domain' => config('cookieconsent.url.domain'),
13 | 'prefix' => config('cookieconsent.url.prefix'),
14 | 'middleware' => config('cookieconsent.url.middleware')
15 | ], function() {
16 | Route::get('script', ScriptController::class)
17 | ->name('script');
18 |
19 | Route::post('accept-all', AcceptAllController::class)
20 | ->name('accept.all');
21 |
22 | Route::post('accept-essentials', AcceptEssentialsController::class)
23 | ->name('accept.essentials');
24 |
25 | Route::post('configure', ConfigureController::class)
26 | ->name('accept.configuration');
27 |
28 | Route::post('reset', ResetController::class)
29 | ->name('reset');
30 | });
31 |
--------------------------------------------------------------------------------
/src/AnalyticCookiesCategory.php:
--------------------------------------------------------------------------------
1 | group(function (CookiesGroup $group) use ($anonymizeIp, $id) {
15 | $key = str_starts_with($id, 'G-') ? substr($id, 2) : $id;
16 | $anonymizeIp = $anonymizeIp === true ? 'true' : 'false';
17 |
18 | $group->name(static::GOOGLE_ANALYTICS)
19 | ->cookie(fn(Cookie $cookie) => $cookie->name('_ga')
20 | ->duration(2 * 365 * 24 * 60)
21 | ->description(__('cookieConsent::cookies.defaults._ga'))
22 | )
23 | ->cookie(fn(Cookie $cookie) => $cookie->name('_ga_' . strtoupper($key))
24 | ->duration(2 * 365 * 24 * 60)
25 | ->description(__('cookieConsent::cookies.defaults._ga_ID'))
26 | )
27 | ->cookie(fn(Cookie $cookie) => $cookie->name('_gid')
28 | ->duration(24 * 60)
29 | ->description(__('cookieConsent::cookies.defaults._gid'))
30 | )
31 | ->cookie(fn(Cookie $cookie) => $cookie->name('_gat')
32 | ->duration(1)
33 | ->description(__('cookieConsent::cookies.defaults._gat'))
34 | )
35 | ->accepted(fn(Consent $consent) => $consent
36 | ->script('')
37 | ->script(
38 | ''
39 | )
40 | );
41 | });
42 |
43 | return $this;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Concerns/HasAttributes.php:
--------------------------------------------------------------------------------
1 | setAttribute($attribute, $value);
18 | }
19 |
20 | /**
21 | * Magically get an attribute.
22 | */
23 | public function __get(string $attribute): mixed
24 | {
25 | return $this->getAttribute($attribute);
26 | }
27 |
28 | /**
29 | * Set all defined attributes at once.
30 | */
31 | public function setAttributes(array $attributes): void
32 | {
33 | $this->attributes = $attributes;
34 | }
35 |
36 | /**
37 | * Get all defined attributes at once.
38 | */
39 | public function getAttributes(): array
40 | {
41 | return $this->attributes;
42 | }
43 |
44 | /**
45 | * Set a specific attribute's value.
46 | */
47 | public function setAttribute(string $attribute, mixed $value): void
48 | {
49 | $this->attributes[$attribute] = $value;
50 | }
51 |
52 | /**
53 | * Get a specific attribute's value.
54 | */
55 | public function getAttribute(string $attribute): mixed
56 | {
57 | return $this->attributes[$attribute] ?? null;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Concerns/HasConsentCallback.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
22 |
23 | return $this;
24 | }
25 |
26 | /**
27 | * Check if there is a defined consent callback.
28 | */
29 | public function hasConsentCallback(): bool
30 | {
31 | return ! is_null($this->callback);
32 | }
33 |
34 | /**
35 | * Check if there is a defined consent callback.
36 | */
37 | public function getConsentResult(): Consent
38 | {
39 | $consent = new Consent($this);
40 |
41 | App::call($this->callback, ['consent' => $consent]);
42 |
43 | return $consent;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Concerns/HasCookies.php:
--------------------------------------------------------------------------------
1 | cookies, function($cookies, $item) {
22 | if(is_a($item, CookiesGroup::class)) {
23 | $cookies = array_merge($cookies, $item->getCookies());
24 | } else {
25 | $cookies[] = $item;
26 | }
27 | return $cookies;
28 | }, []);
29 | }
30 |
31 | /**
32 | * Return all the raw defined items.
33 | */
34 | public function getDefined(): array
35 | {
36 | return $this->cookies;
37 | }
38 |
39 | /**
40 | * Add a single cookie to this collection.
41 | */
42 | public function cookie(Closure|Cookie $cookie): static
43 | {
44 | if(is_a($cookie, Closure::class)) {
45 | $instance = new Cookie();
46 | $cookie($instance);
47 | } else {
48 | $instance = $cookie;
49 | }
50 |
51 | return $this->register($instance);
52 | }
53 |
54 | /**
55 | * Push a cookie instance or group to this collection.
56 | */
57 | protected function register(Cookie|CookiesGroup $instance): static
58 | {
59 | $this->cookies[] = $instance;
60 |
61 | return $this;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Concerns/HasTranslations.php:
--------------------------------------------------------------------------------
1 | get($key);
14 |
15 | return ($value === $key)
16 | ? $default
17 | : $value;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Consent.php:
--------------------------------------------------------------------------------
1 | instance = $instance;
30 | }
31 |
32 | /**
33 | * Add a cookie to the consent response.
34 | */
35 | public function cookie(string $value, ?string $path = null, ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = null): static
36 | {
37 | if(is_a($this->instance, CookiesGroup::class)) {
38 | throw new \Exception('Cannot configure cookie from CookiesGroup.');
39 | }
40 |
41 | $this->cookies[] = CookieFacade::make(
42 | name: $this->instance->name,
43 | value: $value,
44 | minutes: $this->instance->duration,
45 | path: $path,
46 | domain: $domain,
47 | secure: $secure,
48 | httpOnly: $httpOnly,
49 | raw: $raw,
50 | sameSite: $sameSite,
51 | );
52 |
53 | return $this;
54 | }
55 |
56 | /**
57 | * Get all the defined cookies.
58 | */
59 | public function getCookies(): array
60 | {
61 | return $this->cookies;
62 | }
63 |
64 | /**
65 | * Add multiple script tags to the consent response.
66 | */
67 | public function script(string $tag): static
68 | {
69 | $this->scripts[] = $tag;
70 |
71 | return $this;
72 | }
73 |
74 | /**
75 | * Add a single script tag to the consent response.
76 | */
77 | public function getScripts(): array
78 | {
79 | return $this->scripts;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/ConsentResponse.php:
--------------------------------------------------------------------------------
1 | hasConsentCallback()) {
32 | return $this;
33 | }
34 |
35 | $consent = $instance->getConsentResult();
36 |
37 | $this->attachCookies($consent->getCookies());
38 | $this->attachScripts($consent->getScripts());
39 |
40 | return $this;
41 | }
42 |
43 | /**
44 | * Add multiple cookies to the consent response.
45 | */
46 | public function attachCookies(array $cookies): static
47 | {
48 | foreach ($cookies as $cookie) {
49 | $this->attachCookie($cookie);
50 | }
51 |
52 | return $this;
53 | }
54 |
55 | /**
56 | * Add a single cookie to the consent response.
57 | */
58 | public function attachCookie(CookieComponent $cookie): static
59 | {
60 | $this->cookies[] = $cookie;
61 |
62 | return $this;
63 | }
64 |
65 | /**
66 | * Add multiple script tags to the consent response.
67 | */
68 | public function attachScripts(array $tags): static
69 | {
70 | foreach ($tags as $tag) {
71 | $this->attachScript($tag);
72 | }
73 |
74 | return $this;
75 | }
76 |
77 | /**
78 | * Add a single script tag to the consent response.
79 | */
80 | public function attachScript(string $tag): static
81 | {
82 | $this->scripts[] = $tag;
83 |
84 | return $this;
85 | }
86 |
87 | /**
88 | * Transform the collected data into a JSON response-object.
89 | */
90 | public function toResponse(Request $request): Response
91 | {
92 | $response = $request->expectsJson()
93 | ? response()->json($this->getResponseData())
94 | : redirect()->back();
95 |
96 | foreach($this->cookies as $cookie) {
97 | $response->withCookie($cookie);
98 | }
99 |
100 | return $response;
101 | }
102 |
103 | /**
104 | * Transform the collected data into a JSON response-object.
105 | */
106 | public function getResponseData(): array
107 | {
108 | return array_filter([
109 | 'status' => 'ok',
110 | 'scripts' => $this->getResponseScripts(),
111 | 'notice' => $this->getResponseNotice(),
112 | ]);
113 | }
114 |
115 | /**
116 | * Prepare the collected scripts for display.
117 | */
118 | public function getResponseScripts(): ?array
119 | {
120 | return $this->scripts ?: null;
121 | }
122 |
123 | /**
124 | * Prepare the displayable notice for display.
125 | */
126 | protected function getResponseNotice(): ?string
127 | {
128 | return $this->notice ?: null;
129 | }
130 | }
--------------------------------------------------------------------------------
/src/Cookie.php:
--------------------------------------------------------------------------------
1 | name = $name;
26 |
27 | return $this;
28 | }
29 |
30 | /**
31 | * Set the cookie's duration in minutes.
32 | */
33 | public function duration(int $minutes): static
34 | {
35 | $this->duration = $minutes;
36 |
37 | return $this;
38 | }
39 |
40 | /**
41 | * Set an attribute dynamically.
42 | */
43 | public function __call(string $method, array $arguments): static
44 | {
45 | $this->setAttribute($method, $arguments[0] ?? null);
46 |
47 | return $this;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/CookiesCategory.php:
--------------------------------------------------------------------------------
1 | key = $key;
24 | $this->setAttribute('title', $this->translate('categories.' . $key . '.title', ucfirst($key)));
25 | $this->setAttribute('description', $this->translate('categories.' . $key . '.description'));
26 | }
27 |
28 | /**
29 | * Get this category's identifier.
30 | */
31 | public function key(): string
32 | {
33 | return $this->key;
34 | }
35 |
36 | /**
37 | * Add a group to this category.
38 | */
39 | public function group(Closure $callback): static
40 | {
41 | $group = new CookiesGroup();
42 |
43 | $callback($group);
44 |
45 | return $this->register($group);
46 | }
47 |
48 | /**
49 | * Configure a new cookie by calling one of its setting methods.
50 | */
51 | public function __call(string $method, array $arguments): Cookie
52 | {
53 | $instance = new Cookie();
54 | $instance->$method(...$arguments);
55 |
56 | $this->cookie($instance);
57 |
58 | return $instance;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/CookiesGroup.php:
--------------------------------------------------------------------------------
1 | name = $name;
21 |
22 | return $this;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/CookiesManager.php:
--------------------------------------------------------------------------------
1 | registrar = $registrar;
27 | $this->preferences = $this->getCurrentConsentSettings($request);
28 | }
29 |
30 | /**
31 | * Retrieve the eventual existing cookie data.
32 | */
33 | protected function getCurrentConsentSettings(Request $request): ?array
34 | {
35 | $preferences = ($raw = $request->cookie(config('cookieconsent.cookie.name')))
36 | ? json_decode($raw, true)
37 | : null;
38 |
39 | if(! $preferences || ! is_int($preferences['consent_at'] ?? null)) {
40 | return null;
41 | }
42 |
43 | // Check duration in case application settings have changed since the cookie was set.
44 | if($preferences['consent_at'] + (config('cookieconsent.cookie.duration') * 60) < time()) {
45 | return null;
46 | }
47 |
48 | return $preferences;
49 | }
50 |
51 | /**
52 | * Create fresh cookie data for the given consented categories.
53 | */
54 | protected function makeConsentSettings(array $categories): array
55 | {
56 | return array_reduce($this->registrar->getCategories(), function($values, $category) use ($categories) {
57 | $state = in_array($category->key(), $categories);
58 | return array_reduce($category->getCookies(), function($values, $cookie) use ($state) {
59 | $values[$cookie->name] = $state;
60 | return $values;
61 | }, $values);
62 | }, ['consent_at' => time()]);
63 | }
64 |
65 | /**
66 | * Transfer all undefined method calls to the registrar.
67 | */
68 | public function __call(string $method, array $arguments)
69 | {
70 | return $this->registrar->$method(...$arguments);
71 | }
72 |
73 | /**
74 | * Check if the current preference settings are sufficient. If not,
75 | * the cookie preferences notice should be displayed again.
76 | */
77 | public function shouldDisplayNotice(): bool
78 | {
79 | if(! $this->preferences) {
80 | return true;
81 | }
82 |
83 | // Check if each defined cookie has been shown to the user yet.
84 | return array_reduce($this->registrar->getCategories(), function($state, $category) {
85 | return $state ? true : array_reduce($category->getCookies(), function(bool $state, Cookie $cookie) {
86 | return $state ? true : !array_key_exists($cookie->name, $this->preferences);
87 | }, false);
88 | }, false);
89 | }
90 |
91 | /**
92 | * Check if the user has given explicit consent for a specific cookie.
93 | */
94 | public function hasConsentFor(string $key): bool
95 | {
96 | if(! $this->preferences) {
97 | return false;
98 | }
99 |
100 | $groups = array_reduce($this->registrar->getCategories(), function($results, $category) use ($key) {
101 | return array_reduce($category->getDefined(), function(array $results, Cookie|CookiesGroup $instance) use ($key) {
102 | if(is_a($instance, CookiesGroup::class) && $instance->name === $key) {
103 | $results[] = $instance;
104 | }
105 | return $results;
106 | }, $results);
107 | }, []);
108 |
109 | $cookies = $groups
110 | ? array_unique(array_reduce($groups, fn($cookies, $group) => array_merge($cookies, array_map(fn($cookie) => $cookie->name, $group->getCookies())), []))
111 | : [$key];
112 |
113 | foreach($cookies as $cookie) {
114 | if(! boolval($this->preferences[$cookie] ?? false)) return false;
115 | }
116 |
117 | return true;
118 | }
119 |
120 | /**
121 | * Handle the incoming consent preferences accordingly.
122 | */
123 | public function accept(string|array $categories = '*'): ConsentResponse
124 | {
125 | if(! is_array($categories) || ! $categories) {
126 | $categories = array_map(fn($category) => $category->key(), $this->registrar->getCategories());
127 | }
128 |
129 | $this->preferences = $this->makeConsentSettings($categories);
130 |
131 | $response = $this->getConsentResponse();
132 | $response->attachCookie($this->makeConsentCookie());
133 |
134 | return $response;
135 | }
136 |
137 | /**
138 | * Call all the consented cookie callbacks and gather their
139 | * scripts and/or cookies that should be returned along the
140 | * current request's response.
141 | */
142 | protected function getConsentResponse(): ConsentResponse
143 | {
144 | return array_reduce($this->registrar->getCategories(), function($response, $category) {
145 | return array_reduce($category->getDefined(), function(ConsentResponse $response, Cookie|CookiesGroup $instance) {
146 | return $this->hasConsentFor($instance->name)
147 | ? $response->handleConsent($instance)
148 | : $response;
149 | }, $response);
150 | }, new ConsentResponse());
151 | }
152 |
153 | /**
154 | * Create a new cookie instance for the given consented categories.
155 | */
156 | protected function makeConsentCookie(): CookieComponent
157 | {
158 | return CookieFacade::make(
159 | name: config('cookieconsent.cookie.name'),
160 | value: json_encode($this->preferences),
161 | minutes: config('cookieconsent.cookie.duration'),
162 | domain: config('cookieconsent.cookie.domain'),
163 | secure: (env('APP_ENV') == 'local') ? false : true
164 | );
165 | }
166 |
167 | /**
168 | * Output all the scripts for current consent state.
169 | */
170 | public function renderScripts(bool $withDefault = true): string
171 | {
172 | $output = $this->shouldDisplayNotice()
173 | ? $this->getNoticeScripts($withDefault)
174 | : $this->getConsentedScripts($withDefault);
175 |
176 | if(strlen($output)) {
177 | $output = '' . $output;
178 | }
179 |
180 | return $output;
181 | }
182 |
183 | public function getNoticeScripts(bool $withDefault): string
184 | {
185 | return $withDefault ? $this->getDefaultScriptTag() : '';
186 | }
187 |
188 | protected function getConsentedScripts(bool $withDefault): string
189 | {
190 | $output = $this->getNoticeScripts($withDefault);
191 |
192 | foreach ($this->getConsentResponse()->getResponseScripts() ?? [] as $tag) {
193 | $output .= $tag;
194 | }
195 |
196 | return $output;
197 | }
198 |
199 | protected function getDefaultScriptTag(): string
200 | {
201 | return '';
206 | }
207 |
208 | /**
209 | * Output the consent alert/modal for current consent state.
210 | */
211 | public function renderView(): string
212 | {
213 | return $this->shouldDisplayNotice()
214 | ? $this->getNoticeMarkup()
215 | : '';
216 | }
217 |
218 | public function getNoticeMarkup(): string
219 | {
220 | if($policy = config('cookieconsent.policy')) {
221 | $policy = route($policy);
222 | }
223 |
224 | return view('cookie-consent::cookies', [
225 | 'cookies' => $this->registrar,
226 | 'policy' => $policy,
227 | ])->render();
228 | }
229 |
230 | /**
231 | * Output a single cookie consent action button.
232 | */
233 | public function renderButton(string $action, ?string $label = null, array $attributes = []): string
234 | {
235 | $url = match ($action) {
236 | 'accept.all' => route('cookieconsent.accept.all'),
237 | 'accept.essentials' => route('cookieconsent.accept.essentials'),
238 | 'accept.configuration' => route('cookieconsent.accept.configuration'),
239 | 'reset' => route('cookieconsent.reset'),
240 | default => null,
241 | };
242 |
243 | if(! $url) {
244 | throw new \InvalidArgumentException('Cookie consent action "' . $action . '" does not exist. Try one of these: "accept.all", "accept.essentials", "accept.configuration", "reset".');
245 | }
246 |
247 | $attributes = array_merge([
248 | 'method' => 'post',
249 | 'data-cookie-action' => $action,
250 | ], $attributes);
251 |
252 | if(! ($attributes['class'] ?? null)) {
253 | $attributes['class'] = 'cookiebtn';
254 | }
255 |
256 | $basename = explode(' ', $attributes['class'])[0];
257 |
258 | $attributes = collect($attributes)
259 | ->map(fn($value, $attribute) => $attribute . '="' . $value . '"')
260 | ->implode(' ');
261 |
262 | return view('cookie-consent::button', [
263 | 'url' => $url,
264 | 'label' => $label ?? $action, // TODO: use lang file
265 | 'attributes' => $attributes,
266 | 'basename' => $basename,
267 | ])->render();
268 | }
269 |
270 | /**
271 | * Output a table with all the cookies infos.
272 | */
273 | public function renderInfo(): string
274 | {
275 | return view('cookie-consent::info', [
276 | 'cookies' => $this->registrar,
277 | ])->render();
278 | }
279 |
280 | public function replaceInfoTag(string $wysiwyg): string
281 | {
282 | $cookieConsentInfo = view('cookie-consent::info', [
283 | 'cookies' => $this->registrar,
284 | ])->render();
285 |
286 | $formattedString = preg_replace(
287 | [
288 | '/\<(\w)[^\>]+\>\@cookieconsentinfo\<\/\1\>/',
289 | '/\@cookieconsentinfo/',
290 | ],
291 | $cookieConsentInfo,
292 | $wysiwyg,
293 | );
294 |
295 | return $formattedString;
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/src/CookiesRegistrar.php:
--------------------------------------------------------------------------------
1 | getOrMakeCategory('essentials', function(string $key) {
22 | return new EssentialCookiesCategory($key);
23 | });
24 | }
25 |
26 | /**
27 | * Access the pre-defined "analytics" consent-category.
28 | */
29 | public function analytics(): CookiesCategory
30 | {
31 | return $this->getOrMakeCategory('analytics', function(string $key) {
32 | return new AnalyticCookiesCategory($key);
33 | });
34 | }
35 |
36 | /**
37 | * Access the pre-defined "optional" consent-category.
38 | */
39 | public function optional(): CookiesCategory
40 | {
41 | return $this->getOrMakeCategory('optional');
42 | }
43 |
44 | /**
45 | * Define a custom category.
46 | */
47 | public function category(string $key, ?Closure $maker = null): static
48 | {
49 | $this->registerCategory($key, $maker);
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * Magically call a custom category or a cookie creation
56 | * method when inside a cookies group definition.
57 | */
58 | public function __call(string $method, array $arguments)
59 | {
60 | if($key = $this->getCategoryKeyFromMethod($method)) {
61 | return $this->categories[$key];
62 | }
63 |
64 | throw new \BadMethodCallException(sprintf(
65 | 'Method %s::%s does not exist.', static::class, $method
66 | ));
67 | }
68 |
69 | /**
70 | * Retrieve all defined cookies consent-categories.
71 | */
72 | public function getCategories(): array
73 | {
74 | return array_values($this->categories);
75 | }
76 |
77 | /**
78 | * Check if the provided key is a defined cookies consent-category.
79 | */
80 | public function hasCategory(string $key): bool
81 | {
82 | return array_key_exists($key, $this->categories);
83 | }
84 |
85 | /**
86 | * Retrieve a single cookies consent-category.
87 | */
88 | protected function getOrMakeCategory(string $key, ?Closure $maker = null): CookiesCategory
89 | {
90 | return $this->categories[$key]
91 | ?? $this->registerCategory($key, $maker);
92 | }
93 |
94 | /**
95 | * Create & configure a new cookies consent-category.
96 | */
97 | protected function registerCategory(string $key, ?Closure $maker = null): CookiesCategory
98 | {
99 | if($maker && $this->closureExpectsCategoryParameter($maker)) {
100 | $instance = new CookiesCategory($key);
101 | $instance = $maker($instance) ?? $instance;
102 | } else if ($maker) {
103 | $instance = $maker($key);
104 | } else {
105 | $instance = new CookiesCategory($key);
106 | }
107 |
108 | if(! is_a($instance, CookiesCategory::class)) {
109 | throw new \UnexpectedValueException('Unknown cookies category instance.');
110 | }
111 |
112 | return $this->categories[$key] = $instance;
113 | }
114 |
115 | /**
116 | * Check if given function is expecting a category instance as first parameter.
117 | */
118 | protected function closureExpectsCategoryParameter(Closure $maker): bool
119 | {
120 | $reflection = new ReflectionFunction($maker);
121 | $parameter = $reflection->getParameters()[0]?->getType()?->getName();
122 |
123 | return $parameter === CookiesCategory::class;
124 | }
125 |
126 | /**
127 | * Find the correct registered category key for a given method name.
128 | */
129 | protected function getCategoryKeyFromMethod(string $method): ?string
130 | {
131 | if(array_key_exists($method, $this->categories)) {
132 | return $method;
133 | }
134 |
135 | foreach (array_keys($this->categories) as $key) {
136 | if(Str::camel($key) === $method) return $key;
137 | }
138 |
139 | return null;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/CookiesServiceProvider.php:
--------------------------------------------------------------------------------
1 | booted(function () {
15 | $this->registerCookies();
16 | });
17 | }
18 |
19 | /**
20 | * Define the cookies users should be aware of.
21 | */
22 | abstract protected function registerCookies(): void;
23 |
24 | /**
25 | * Bootstrap any application services.
26 | */
27 | public function boot()
28 | {
29 | //
30 | }
31 | }
--------------------------------------------------------------------------------
/src/EssentialCookiesCategory.php:
--------------------------------------------------------------------------------
1 | cookie(function(Cookie $cookie) {
15 | $cookie->name(Config::get('cookieconsent.cookie.name'))
16 | ->duration(Config::get('cookieconsent.cookie.duration'))
17 | ->description(__('cookieConsent::cookies.defaults.consent'));
18 | });
19 | }
20 |
21 | /**
22 | * Define Laravel's session cookie.
23 | */
24 | public function session(): static
25 | {
26 | return $this->cookie(function(Cookie $cookie) {
27 | $cookie->name(Config::get('session.cookie'))
28 | ->duration(Config::get('session.lifetime'))
29 | ->description(__('cookieConsent::cookies.defaults.session'));
30 | });
31 | }
32 |
33 | /**
34 | * Define Laravel's XSRF-TOKEN cookie.
35 | */
36 | public function csrf(): static
37 | {
38 | return $this->cookie(function(Cookie $cookie) {
39 | $cookie->name('XSRF-TOKEN')
40 | ->duration(Config::get('session.lifetime'))
41 | ->description(__('cookieConsent::cookies.defaults.csrf'));
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Facades/Cookies.php:
--------------------------------------------------------------------------------
1 | accept('*')->toResponse($request);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Http/Controllers/AcceptEssentialsController.php:
--------------------------------------------------------------------------------
1 | accept(['essentials'])->toResponse($request);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ConfigureController.php:
--------------------------------------------------------------------------------
1 | get('categories', []))
13 | ->prepend('essentials')
14 | ->unique()
15 | ->filter(fn($key) => $cookies->hasCategory($key))
16 | ->values()
17 | ->all();
18 |
19 | return $cookies->accept($categories)->toResponse($request);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ResetController.php:
--------------------------------------------------------------------------------
1 | expectsJson()
13 | ? redirect()->back()
14 | : response()->json([
15 | 'status' => 'ok',
16 | 'scripts' => $cookies->getNoticeScripts(true),
17 | 'notice' => $cookies->getNoticeMarkup(),
18 | ]);
19 |
20 | return $response->withoutCookie(
21 | cookie: config('cookieconsent.cookie.name'),
22 | domain: config('cookieconsent.cookie.domain'),
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Http/Controllers/ScriptController.php:
--------------------------------------------------------------------------------
1 | generateConfig(), file_get_contents(LCC_ROOT . '/dist/cookies.js'));
12 |
13 | return response($content)->header('Content-Type', 'application/javascript');
14 | }
15 |
16 | protected function generateConfig(): string
17 | {
18 | return json_encode([
19 | 'accept.all' => route('cookieconsent.accept.all'),
20 | 'accept.essentials' => route('cookieconsent.accept.essentials'),
21 | 'accept.configuration' => route('cookieconsent.accept.configuration'),
22 | 'reset' => route('cookieconsent.reset'),
23 | ]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(LCC_ROOT.'/config/cookieconsent.php', 'cookieconsent');
21 |
22 | $this->app->singleton(CookiesRegistrar::class, function () {
23 | $registrar = new CookiesRegistrar();
24 | $registrar->essentials()->consent();
25 | return $registrar;
26 | });
27 | }
28 |
29 | /**
30 | * Bootstrap the application services.
31 | */
32 | public function boot()
33 | {
34 | $this->publishes([
35 | LCC_ROOT.'/stubs/CookiesServiceProvider.php' => app_path('Providers/CookiesServiceProvider.php'),
36 | ], 'laravel-cookie-consent-service-provider');
37 |
38 | $this->publishes([
39 | LCC_ROOT.'/config/cookieconsent.php' => config_path('cookieconsent.php'),
40 | ], 'laravel-cookie-consent-config');
41 |
42 | $this->loadViewsFrom(
43 | LCC_ROOT.'/resources/views', 'cookie-consent'
44 | );
45 |
46 | $this->publishes([
47 | LCC_ROOT.'/resources/views' => resource_path('views/vendor/cookie-consent'),
48 | ], 'laravel-cookie-consent-views');
49 |
50 | $this->loadTranslationsFrom(LCC_ROOT.'/resources/lang', 'cookieConsent');
51 |
52 | $this->publishes([
53 | realpath(LCC_ROOT.'/resources/lang') => $this->app->langPath('vendor/cookieConsent'),
54 | ], 'laravel-cookie-consent-lang');
55 |
56 | $this->registerBladeDirectives();
57 |
58 | $this->loadRoutesFrom(LCC_ROOT.'/routes/web.php');
59 | }
60 |
61 | /**
62 | * Define the cookie-consent blade directives.
63 | */
64 | protected function registerBladeDirectives()
65 | {
66 | Blade::directive('cookieconsentscripts', function (string $expression) {
67 | return '';
68 | });
69 |
70 | Blade::directive('cookieconsentview', function (string $expression) {
71 | return '';
72 | });
73 |
74 | Blade::directive('cookieconsentbutton', function (string $expression) {
75 | return '';
76 | });
77 |
78 | Blade::directive('cookieconsentinfo', function () {
79 | return '';
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/stubs/CookiesServiceProvider.php:
--------------------------------------------------------------------------------
1 | session()
18 | ->csrf();
19 |
20 | // Register all Analytics cookies at once using one single shorthand method:
21 | // Cookies::analytics()
22 | // ->google(
23 | // id: config('cookieconsent.google_analytics.id'),
24 | // anonymizeIp: config('cookieconsent.google_analytics.anonymize_ip')
25 | // );
26 |
27 | // Register custom cookies under the pre-existing "optional" category:
28 | // Cookies::optional()
29 | // ->name('darkmode_enabled')
30 | // ->description('This cookie helps us remember your preferences regarding the interface\'s brightness.')
31 | // ->duration(120)
32 | // ->accepted(fn(Consent $consent, MyDarkmode $darkmode) => $consent->cookie(value: $darkmode->getDefaultValue()));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Feature/FacadeTest.php:
--------------------------------------------------------------------------------
1 | csrf();
9 | Cookies::essentials()->name('foo')->duration(120);
10 |
11 | expect($categories = app(CookiesRegistrar::class)->getCategories())->toHaveLength(1);
12 | expect($category = $categories[0])->toBeInstanceOf(EssentialCookiesCategory::class);
13 | expect($category->getCookies())->toHaveLength(3);
14 | });
15 |
--------------------------------------------------------------------------------
/tests/Feature/ServiceProviderTest.php:
--------------------------------------------------------------------------------
1 | essentials()->csrf();
8 |
9 | expect($categories = app(CookiesRegistrar::class)->getCategories())->toHaveLength(1);
10 | expect($category = $categories[0])->toBeInstanceOf(EssentialCookiesCategory::class);
11 | expect($category->getCookies())->toHaveLength(2);
12 | });
13 |
--------------------------------------------------------------------------------
/tests/OrchestraTestCase.php:
--------------------------------------------------------------------------------
1 | in('Unit');
17 | uses(Tests\OrchestraTestCase::class)->in('Feature');
18 |
19 | /*
20 | |--------------------------------------------------------------------------
21 | | Expectations
22 | |--------------------------------------------------------------------------
23 | |
24 | | When you're writing tests, you often need to check that values meet certain conditions. The
25 | | "expect()" function gives you access to a set of "expectations" methods that you can use
26 | | to assert different things. Of course, you may extend the Expectation API at any time.
27 | |
28 | */
29 |
30 | expect()->extend('toBeOne', function () {
31 | return $this->toBe(1);
32 | });
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Functions
37 | |--------------------------------------------------------------------------
38 | |
39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
40 | | project that you don't want to repeat in every file. Here you can also expose helpers as
41 | | global functions to help you to reduce the number of lines of code in your test files.
42 | |
43 | */
44 |
45 | function something()
46 | {
47 | // ..
48 | }
49 |
--------------------------------------------------------------------------------
/tests/PhpUnitTestCase.php:
--------------------------------------------------------------------------------
1 | google('g-foo'))->toBe($category);
10 | expect($group = ($category->getDefined()[0] ?? null))->toBeInstanceOf(CookiesGroup::class);
11 |
12 | expect($group->hasConsentCallback())->toBeTrue();
13 | expect($group->getCookies())->toHaveLength(4);
14 | });
15 |
--------------------------------------------------------------------------------
/tests/Unit/CookieTest.php:
--------------------------------------------------------------------------------
1 | name('foo'))->toBe($cookie);
9 | expect($cookie->name)->toBe('foo');
10 | });
11 |
12 | it('can set duration', function () {
13 | $cookie = new Cookie();
14 |
15 | expect($cookie->duration(10))->toBe($cookie);
16 | expect($cookie->duration)->toBe(10);
17 | });
18 |
19 | it('can set custom attributes', function () {
20 | $cookie = new Cookie();
21 |
22 | $cookie->title = 'foo';
23 |
24 | expect($cookie->label('bar'))->toBe($cookie);
25 |
26 | $attributes = $cookie->getAttributes();
27 | expect($attributes['title'] ?? null)->toBe('foo');
28 | expect($attributes['label'] ?? null)->toBe('bar');
29 | });
--------------------------------------------------------------------------------
/tests/Unit/CookiesCategoryTest.php:
--------------------------------------------------------------------------------
1 | key())->toBe('foo');
10 | });
11 |
12 | it('can set custom attributes', function () {
13 | $category = new CookiesCategory('foo');
14 | $category->title = 'foo';
15 |
16 | $attributes = $category->getAttributes();
17 | expect($attributes['title'] ?? null)->toBe('foo');
18 | });
19 |
20 | it('can register and start cookie configuration from cookie method', function () {
21 | $category = new CookiesCategory('foo');
22 |
23 | expect($category->name('foo-cookie'))->toBeInstanceOf(Cookie::class);
24 | });
25 |
26 | it('can register a cookies group', function () {
27 | $category = new CookiesCategory('foo');
28 |
29 | expect($category->group(fn(CookiesGroup $group) => $group))->toBe($category);
30 |
31 | $results = $category->getDefined();
32 | expect($results)->toHaveLength(1);
33 | });
34 |
35 | it('can return all defined cookies', function () {
36 | $category = new CookiesCategory('foo');
37 | $category->cookie(new Cookie());
38 | $category->cookie(new Cookie());
39 |
40 | $results = $category->getCookies();
41 | expect($results)->toHaveLength(2);
42 | });
43 |
--------------------------------------------------------------------------------
/tests/Unit/CookiesRegistrarTest.php:
--------------------------------------------------------------------------------
1 | essentials())->toBeInstanceOf(EssentialCookiesCategory::class);
12 | expect($essentials->key())->toBe('essentials');
13 |
14 | expect($analytics = $registrar->analytics())->toBeInstanceOf(AnalyticCookiesCategory::class);
15 | expect($analytics->key())->toBe('analytics');
16 |
17 | expect($optional = $registrar->optional())->toBeInstanceOf(CookiesCategory::class);
18 | expect($optional->key())->toBe('optional');
19 | });
20 |
21 | it('can create and access custom consent categories', function () {
22 | $registrar = new CookiesRegistrar();
23 |
24 | $result = $registrar->category('simple');
25 | expect($result)->toBe($registrar);
26 | expect($simple = $registrar->simple())->toBeInstanceOf(CookiesCategory::class);
27 | expect($simple->key())->toBe('simple');
28 |
29 | $result = $registrar->category('with-key', fn(string $key) => new CookiesCategory($key));
30 | expect($result)->toBe($registrar);
31 | expect($withKey = $registrar->withKey())->toBeInstanceOf(CookiesCategory::class);
32 | expect($withKey->key())->toBe('with-key');
33 |
34 | $result = $registrar->category('with-instance', fn(CookiesCategory $category) => $category);
35 | expect($result)->toBe($registrar);
36 | expect($withInstance = $registrar->withInstance())->toBeInstanceOf(CookiesCategory::class);
37 | expect($withInstance->key())->toBe('with-instance');
38 | });
39 |
40 | it('cannot return an undefined consent category', function() {
41 | $registrar = new CookiesRegistrar();
42 | $registrar->custom();
43 | })->throws(\BadMethodCallException::class);
44 |
45 | it('can return all defined consent categories', function() {
46 | $registrar = new CookiesRegistrar();
47 | $registrar->essentials();
48 | $registrar->category('custom');
49 | $registrar->analytics();
50 |
51 | $results = $registrar->getCategories();
52 | expect($results)->toHaveLength(3);
53 | expect($results[0]->key())->toBe('essentials');
54 | expect($results[1]->key())->toBe('custom');
55 | expect($results[2]->key())->toBe('analytics');
56 | });
57 |
--------------------------------------------------------------------------------
/tests/Unit/EssentialCookiesCategoryTest.php:
--------------------------------------------------------------------------------
1 | with('app.locale')->andReturn('en');
9 | Config::shouldReceive('get')->with('app.fallback_locale')->andReturn('en');
10 | Config::shouldReceive('get')->with('database.default')->andReturn('test');
11 | Config::shouldReceive('get')->with('database.connections.test')->andReturn(null);
12 | Config::shouldReceive('get')->once()->with('cookieconsent.cookie.name')->andReturn('foo_consent');
13 | Config::shouldReceive('get')->once()->with('cookieconsent.cookie.duration')->andReturn(365 * 24 * 60);
14 |
15 | $category = new EssentialCookiesCategory('foo');
16 |
17 | expect($category->consent())->toBe($category);
18 | expect($cookie = ($category->getCookies()[0] ?? null))->toBeInstanceOf(Cookie::class);
19 | expect($cookie->name)->toBe('foo_consent');
20 | expect($cookie->duration)->toBe(365 * 24 * 60);
21 | });
22 |
23 | it('can register session cookie', function () {
24 | Config::shouldReceive('get')->with('app.locale')->andReturn('en');
25 | Config::shouldReceive('get')->with('app.fallback_locale')->andReturn('en');
26 | Config::shouldReceive('get')->with('database.default')->andReturn('test');
27 | Config::shouldReceive('get')->with('database.connections.test')->andReturn(null);
28 | Config::shouldReceive('get')->once()->with('session.cookie')->andReturn('foo_session');
29 | Config::shouldReceive('get')->once()->with('session.lifetime')->andReturn(120);
30 |
31 | $category = new EssentialCookiesCategory('foo');
32 |
33 | expect($category->session())->toBe($category);
34 | expect($cookie = ($category->getCookies()[0] ?? null))->toBeInstanceOf(Cookie::class);
35 | expect($cookie->name)->toBe('foo_session');
36 | expect($cookie->duration)->toBe(120);
37 | });
38 |
39 | it('can register csrf cookie', function () {
40 | Config::shouldReceive('get')->with('app.locale')->andReturn('en');
41 | Config::shouldReceive('get')->with('app.fallback_locale')->andReturn('en');
42 | Config::shouldReceive('get')->with('database.default')->andReturn('test');
43 | Config::shouldReceive('get')->with('database.connections.test')->andReturn(null);
44 | Config::shouldReceive('get')->once()->with('session.lifetime')->andReturn(120);
45 |
46 | $category = new EssentialCookiesCategory('foo');
47 |
48 | expect($category->csrf())->toBe($category);
49 | expect($cookie = ($category->getCookies()[0] ?? null))->toBeInstanceOf(Cookie::class);
50 | expect($cookie->name)->toBe('XSRF-TOKEN');
51 | expect($cookie->duration)->toBe(120);
52 | });
53 |
--------------------------------------------------------------------------------
/tests/Unit/HasAttributesTest.php:
--------------------------------------------------------------------------------
1 | foo = 'bar';
11 |
12 | expect($instance->foo)->toBe('bar');
13 | expect($instance->undefined)->toBeNull();
14 | });
15 |
16 | it('can methodolically set & get an attribute', function () {
17 | $instance = new class() {
18 | use HasAttributes;
19 | };
20 |
21 | $instance->setAttribute('foo', 'bar');
22 |
23 | expect($instance->getAttribute('foo'))->toBe('bar');
24 | expect($instance->getAttribute('undefined'))->toBeNull();
25 | });
26 |
27 | it('can set & get all attributes', function () {
28 | $instance = new class() {
29 | use HasAttributes;
30 | };
31 |
32 | $instance->setAttributes(['foo' => 'bar']);
33 |
34 | expect($results = $instance->getAttributes())->toBeArray();
35 | expect($results['foo'])->toBe('bar');
36 | });
37 |
--------------------------------------------------------------------------------
/tests/Unit/HasConsentCallbackTest.php:
--------------------------------------------------------------------------------
1 | hasConsentCallback())->toBeFalse();
11 | $instance->accepted(fn() => true);
12 | expect($instance->hasConsentCallback())->toBeTrue();
13 | });
14 |
--------------------------------------------------------------------------------
/tests/Unit/HasCookiesTest.php:
--------------------------------------------------------------------------------
1 | cookie($cookie))->toBe($instance);
14 | expect($results = $instance->getCookies())->toHaveLength(1);
15 | expect($results[0])->toBeInstanceOf(Cookie::class);
16 | });
17 |
18 | it('can register a cookie using callback', function () {
19 | $instance = new class() {
20 | use HasCookies;
21 | };
22 |
23 | $callback = function(Cookie $cookie) {
24 | return $cookie;
25 | };
26 |
27 | expect($instance->cookie($callback))->toBe($instance);
28 | expect($results = $instance->getCookies())->toHaveLength(1);
29 | expect($results[0])->toBeInstanceOf(Cookie::class);
30 | });
31 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix');
2 |
3 | mix.setPublicPath('dist')
4 | .js('resources/js/script.js', 'dist')
5 | .js('resources/js/cookies.js', 'dist')
6 | .sass('resources/scss/style.scss', 'dist');
--------------------------------------------------------------------------------