├── .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 | ![Laravel Cookie Consent in action](https://raw.githubusercontent.com/whitecube/laravel-cookie-consent/main/laravel-cookie-consent.gif) 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 | 6 | 7 | 8 | 9 | 10 | @foreach($category->getCookies() as $cookie) 11 | 12 | 13 | 14 | 15 | 16 | @endforeach 17 | 18 |
@lang('cookieConsent::cookies.cookie')@lang('cookieConsent::cookies.purpose')@lang('cookieConsent::cookies.duration')
{{ $cookie->name }}{{ $cookie->description }}{{ \Carbon\CarbonInterval::minutes($cookie->duration)->cascade() }}
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'); --------------------------------------------------------------------------------