├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Attributes │ └── Unlocked.php ├── Features │ └── SupportLockedProperties │ │ ├── SupportLockedProperties.php │ │ └── UnitTest.php ├── LivewireStrict.php └── LivewireStrictServiceProvider.php └── tests └── TestCase.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: PhiloNL 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | paths-ignore: 8 | - "README.md" 9 | pull_request: 10 | types: [ready_for_review, synchronize, opened] 11 | paths-ignore: 12 | - "README.md" 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | php: [8.1, 8.2, 8.3] 21 | laravel: [10.*, 11.*] 22 | exclude: 23 | - php: 8.1 24 | laravel: 11.* 25 | 26 | name: PHP:${{ matrix.php }} / Laravel:${{ matrix.laravel }} 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup PHP, with composer and extensions 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | extensions: dom, curl, libxml, mbstring, iconv, intl, zip, pdo_sqlite 37 | tools: composer:v2 38 | coverage: none 39 | 40 | - name: Get composer cache directory 41 | id: composer-cache 42 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 43 | 44 | - name: Cache composer dependencies 45 | uses: actions/cache@v4 46 | with: 47 | path: ${{ steps.composer-cache.outputs.dir }} 48 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 49 | restore-keys: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer- 50 | 51 | - name: Install Composer dependencies 52 | run: | 53 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update --dev 54 | composer update --prefer-stable --no-interaction 55 | 56 | - name: Run Unit tests 57 | run: vendor/bin/phpunit --testsuite Unit 58 | env: 59 | RUNNING_IN_CI: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /node_modules/ 3 | _runtime_components.json 4 | SCRATCH.md 5 | COMMIT_MSG.aim.txt 6 | composer.lock 7 | .phpunit.cache/ 8 | .phpunit.result.cache 9 | .idea 10 | phpunit.xml 11 | 12 | # OS generated files 13 | .DS_Store 14 | ._* 15 | .Spotlight-V100 16 | .Trashes 17 | ehthumbs.db 18 | Thumbs.db -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philo Hermans 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 | # Livewire Strict 2 | 3 |

4 | Total Downloads 5 | Latest Stable Version 6 | License 7 |

8 | 9 | ## Enforce additional Livewire security measures 10 | Livewire Strict helps to enforce security measures and prevents you from leaving sensitive public properties unprotected. 11 | 12 | ## Documentation 13 | You can find the [full documentation on our site](https://wire-elements.dev/blog/livewire-strict-enforce-additional-security-measures-to-livewire). 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wire-elements/livewire-strict", 3 | "description": "Add strict mode to Livewire.", 4 | "require": { 5 | "php": "^8.1" 6 | }, 7 | "license": "MIT", 8 | "autoload": { 9 | "psr-4": { 10 | "WireElements\\LivewireStrict\\": "src/" 11 | } 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "Tests\\": "tests/" 16 | } 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Philo Hermans", 21 | "email": "me@philo.dev" 22 | } 23 | ], 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "WireElements\\LivewireStrict\\LivewireStrictServiceProvider" 28 | ] 29 | } 30 | }, 31 | "minimum-stability": "dev", 32 | "prefer-stable": true, 33 | "require-dev": { 34 | "phpunit/phpunit": "^10.4", 35 | "laravel/framework": "^10.15.0|^11.0", 36 | "orchestra/testbench": "^8.21.0|^9.1", 37 | "livewire/livewire": "^3.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./src 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Attributes/Unlocked.php: -------------------------------------------------------------------------------- 1 | contains('*') && str($this->component::class)->is($component)) { 26 | $checkIsRequired = true; 27 | } 28 | 29 | if ($component === $this->component::class) { 30 | $checkIsRequired = true; 31 | } 32 | } 33 | 34 | if (! $checkIsRequired) { 35 | return; 36 | } 37 | 38 | $componentIsUnlocked = $this->component 39 | ->getAttributes() 40 | ->whereInstanceOf(Unlocked::class) 41 | ->filter(fn (Unlocked $attribute) => $attribute->getLevel() === AttributeLevel::ROOT) 42 | ->isNotEmpty(); 43 | 44 | if ($componentIsUnlocked) { 45 | return; 46 | } 47 | 48 | $propertyIsUnlocked = $this->component 49 | ->getAttributes() 50 | ->whereInstanceOf(Unlocked::class) 51 | ->filter(fn (Unlocked $attribute) => $attribute->getSubName() === $propertyName && $attribute->getLevel() === AttributeLevel::PROPERTY) 52 | ->isNotEmpty(); 53 | 54 | throw_unless($propertyIsUnlocked, CannotUpdateLockedPropertyException::class, $propertyName); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Features/SupportLockedProperties/UnitTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionMessage( 15 | 'Cannot update locked property: [count]' 16 | ); 17 | 18 | LivewireStrict::lockProperties(components: 'WireElements\*'); 19 | 20 | Livewire::test(new class extends TestComponent 21 | { 22 | public $count = 1; 23 | 24 | public function increment() 25 | { 26 | $this->count++; 27 | } 28 | }) 29 | ->assertSetStrict('count', 1) 30 | ->set('count', 2); 31 | } 32 | 33 | public function test_can_update_unlocked_property() 34 | { 35 | LivewireStrict::lockProperties(); 36 | 37 | Livewire::test(new class extends TestComponent 38 | { 39 | #[Unlocked] 40 | public $count = 1; 41 | }) 42 | ->assertSetStrict('count', 1) 43 | ->set('count', 2); 44 | } 45 | 46 | public function test_can_update_unlocked_component() 47 | { 48 | LivewireStrict::lockProperties(); 49 | 50 | Livewire::test(new #[Unlocked] class extends TestComponent 51 | { 52 | public $count = 1; 53 | }) 54 | ->assertSetStrict('count', 1) 55 | ->set('count', 2); 56 | } 57 | 58 | public function test_only_enabled_for_specific_namespace() 59 | { 60 | $this->expectExceptionMessage( 61 | 'Cannot update locked property: [count]' 62 | ); 63 | 64 | LivewireStrict::lockProperties(components: 'WireElements\*'); 65 | 66 | Livewire::test(new class extends SpecificComponent 67 | { 68 | public $count = 1; 69 | 70 | public function increment() 71 | { 72 | $this->count++; 73 | } 74 | }) 75 | ->assertSetStrict('count', 1) 76 | ->set('count', 2); 77 | } 78 | 79 | public function test_only_enabled_for_specific_components() 80 | { 81 | $this->expectExceptionMessage( 82 | 'Cannot update locked property: [count]' 83 | ); 84 | 85 | LivewireStrict::lockProperties(components: 'WireElements\LivewireStrict\Features\SupportLockedProperties\SpecificComponent*'); 86 | 87 | Livewire::test(new class extends SpecificComponent 88 | { 89 | public $count = 1; 90 | 91 | public function increment() 92 | { 93 | $this->count++; 94 | } 95 | }) 96 | ->assertSetStrict('count', 1) 97 | ->set('count', 2); 98 | } 99 | 100 | public function test_it_ignores_other_components() 101 | { 102 | LivewireStrict::lockProperties(components: 'App/*'); 103 | 104 | Livewire::test(new class extends SpecificComponent 105 | { 106 | public $count = 1; 107 | 108 | public function increment() 109 | { 110 | $this->count++; 111 | } 112 | }) 113 | ->assertSetStrict('count', 1) 114 | ->set('count', 2); 115 | } 116 | } 117 | 118 | class TestComponent extends Component 119 | { 120 | public function render() 121 | { 122 | return '
'; 123 | } 124 | } 125 | 126 | class SpecificComponent extends TestComponent 127 | { 128 | } 129 | -------------------------------------------------------------------------------- /src/LivewireStrict.php: -------------------------------------------------------------------------------- 1 | componentHook(Features\SupportLockedProperties\SupportLockedProperties::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | afterApplicationCreated(function () { 14 | $this->makeACleanSlate(); 15 | }); 16 | 17 | $this->beforeApplicationDestroyed(function () { 18 | $this->makeACleanSlate(); 19 | }); 20 | 21 | parent::setUp(); 22 | } 23 | 24 | public function makeACleanSlate() 25 | { 26 | Artisan::call('view:clear'); 27 | 28 | File::delete(app()->bootstrapPath('cache/livewire-components.php')); 29 | } 30 | 31 | protected function getPackageProviders($app) 32 | { 33 | return [ 34 | \Livewire\LivewireServiceProvider::class, 35 | LivewireStrictServiceProvider::class, 36 | ]; 37 | } 38 | 39 | protected function defineEnvironment($app) 40 | { 41 | $app['config']->set('app.key', 'base64:Hupx3yAySikrM2/edkZQNQHslgDWYfiBfCuSThJ5SK8='); 42 | } 43 | } 44 | --------------------------------------------------------------------------------