From 1776fa28f65d09e6832bd8ceb1ca624a6496f49c Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 15 Oct 2024 14:06:03 -0300 Subject: [PATCH 1/4] refactor(sanitizer): improve error handling and test coverage - Enhance Sanitizer class to catch PropertyInspectionException - Update SanitizerTest to reflect new error handling behavior - Add comprehensive test suite for Sanitize attribute - Ensure 100% test coverage with strong typing for all components - Adjust AttributeHandler to properly handle processor exceptions --- composer.json | 13 +- composer.lock | 139 ++++++++++++- src/Attribute/Sanitize.php | 27 +++ src/Exception/SanitizationException.php | 9 + src/Processor/AbstractSanitizerProcessor.php | 22 ++ src/Processor/Cleaner/EmailAddressCleaner.php | 18 ++ src/Processor/Cleaner/NumericValueCleaner.php | 19 ++ src/Processor/Cleaner/UrlAddressCleaner.php | 18 ++ .../Encoder/HtmlSpecialCharsEncoder.php | 19 ++ src/Processor/HtmlPurifier.php | 26 +++ src/Processor/Remover/HtmlTagRemover.php | 25 +++ src/Processor/Remover/WhitespaceRemover.php | 25 +++ src/Processor/WhitespaceSanitizer.php | 15 ++ src/Processor/XssSanitizer.php | 15 ++ src/Sanitizer.php | 46 +++++ tests/Attribute/SanitizeTest.php | 78 ++++++++ .../Cleaner/EmailAddressCleanerTest.php | 68 +++++++ .../Cleaner/NumericValueCleanerTest.php | 63 ++++++ .../Cleaner/UrlAddressCleanerTest.php | 67 +++++++ .../Encoder/HtmlSpecialCharsEncoderTest.php | 47 +++++ .../Processor/Remover/HtmlTagRemoverTest.php | 56 ++++++ .../Remover/WhitespaceRemoverTest.php | 53 +++++ tests/SanitizerTest.php | 188 ++++++++++++++++++ tests/application.php | 101 ++++++++++ 24 files changed, 1142 insertions(+), 15 deletions(-) create mode 100644 src/Attribute/Sanitize.php create mode 100644 src/Exception/SanitizationException.php create mode 100644 src/Processor/AbstractSanitizerProcessor.php create mode 100644 src/Processor/Cleaner/EmailAddressCleaner.php create mode 100644 src/Processor/Cleaner/NumericValueCleaner.php create mode 100644 src/Processor/Cleaner/UrlAddressCleaner.php create mode 100644 src/Processor/Encoder/HtmlSpecialCharsEncoder.php create mode 100644 src/Processor/HtmlPurifier.php create mode 100644 src/Processor/Remover/HtmlTagRemover.php create mode 100644 src/Processor/Remover/WhitespaceRemover.php create mode 100644 src/Processor/WhitespaceSanitizer.php create mode 100644 src/Processor/XssSanitizer.php create mode 100644 src/Sanitizer.php create mode 100644 tests/Attribute/SanitizeTest.php create mode 100644 tests/Processor/Cleaner/EmailAddressCleanerTest.php create mode 100644 tests/Processor/Cleaner/NumericValueCleanerTest.php create mode 100644 tests/Processor/Cleaner/UrlAddressCleanerTest.php create mode 100644 tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php create mode 100644 tests/Processor/Remover/HtmlTagRemoverTest.php create mode 100644 tests/Processor/Remover/WhitespaceRemoverTest.php create mode 100644 tests/SanitizerTest.php diff --git a/composer.json b/composer.json index d5b7356..289f63c 100644 --- a/composer.json +++ b/composer.json @@ -23,19 +23,18 @@ ], "require": { "php": "^8.3", - "kariricode/data-structure": "^1.0" + "kariricode/contract": "^2.7", + "kariricode/processor-pipeline": "^1.1", + "kariricode/property-inspector": "^1.0" }, "autoload": { "psr-4": { - "KaririCode\\Dotenv\\": "src" - }, - "files": [ - "src/functions.php" - ] + "KaririCode\\Sanitizer\\": "src" + } }, "autoload-dev": { "psr-4": { - "KaririCode\\Dotenv\\Tests\\": "tests" + "KaririCode\\Sanitizer\\Tests\\": "tests" } }, "require-dev": { diff --git a/composer.lock b/composer.lock index b158a09..55a09a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "22bf2f30b1441f6fdea16b4723c0184a", + "content-hash": "8388d111e06717bf1de152e1b2d19036", "packages": [ { "name": "kariricode/contract", - "version": "v2.6.3", + "version": "v2.7.5", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "5d98b009c7c5c20dd63b4440405ac81f93544e7d" + "reference": "253787b27e8fa170ef5e2b12e69a20557701a899" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/5d98b009c7c5c20dd63b4440405ac81f93544e7d", - "reference": "5d98b009c7c5c20dd63b4440405ac81f93544e7d", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/253787b27e8fa170ef5e2b12e69a20557701a899", + "reference": "253787b27e8fa170ef5e2b12e69a20557701a899", "shasum": "" }, "require": { @@ -66,7 +66,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-contract/issues", "source": "https://github.com/KaririCode-Framework/kariricode-contract" }, - "time": "2024-10-10T21:05:49+00:00" + "time": "2024-10-15T16:33:53+00:00" }, { "name": "kariricode/data-structure", @@ -141,6 +141,129 @@ "source": "https://github.com/KaririCode-Framework/kariricode-data-structure" }, "time": "2024-10-10T22:37:23+00:00" + }, + { + "name": "kariricode/processor-pipeline", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline.git", + "reference": "07c645a1c45f47ffd748e6bb8231806b465bd7bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/07c645a1c45f47ffd748e6bb8231806b465bd7bc", + "reference": "07c645a1c45f47ffd748e6bb8231806b465bd7bc", + "shasum": "" + }, + "require": { + "kariricode/contract": "^2.7", + "kariricode/data-structure": "^1.1", + "php": "^8.3" + }, + "require-dev": { + "enlightn/security-checker": "^2.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "KaririCode\\ProcessorPipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "A flexible and extensible processor pipeline component for the KaririCode framework. Enables the creation of modular, configurable processing chains for data transformation, validation, and sanitization tasks", + "homepage": "https://kariricode.org", + "keywords": [ + "KaririCode", + "configurable", + "data processing", + "modular", + "php", + "pipeline", + "processor" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline" + }, + "time": "2024-10-15T14:03:33+00:00" + }, + { + "name": "kariricode/property-inspector", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-property-inspector.git", + "reference": "30c6e85ac70fb3351ce16c05ba7ec0baf6ab98f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/30c6e85ac70fb3351ce16c05ba7ec0baf6ab98f0", + "reference": "30c6e85ac70fb3351ce16c05ba7ec0baf6ab98f0", + "shasum": "" + }, + "require": { + "kariricode/contract": "^2.7", + "kariricode/processor-pipeline": "^1.1", + "php": "^8.3" + }, + "require-dev": { + "enlightn/security-checker": "^2.0", + "friendsofphp/php-cs-fixer": "^3.51", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "KaririCode\\PropertyInspector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "A robust and flexible data sanitization component for PHP, part of the KaririCode Framework, utilizing configurable processors and native functions.", + "homepage": "https://kariricode.org", + "keywords": [ + "attribute", + "dynamic-analysis", + "framework", + "inspection", + "kariri-code", + "metadata", + "normalization", + "object-properties", + "php8", + "property-inspector", + "reflection", + "validation" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-property-inspector" + }, + "time": "2024-10-15T16:42:44+00:00" } ], "packages-dev": [ @@ -5047,12 +5170,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^8.3" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Attribute/Sanitize.php b/src/Attribute/Sanitize.php new file mode 100644 index 0000000..fafee38 --- /dev/null +++ b/src/Attribute/Sanitize.php @@ -0,0 +1,27 @@ +sanitizers; + } + + public function getFallbackValue(): mixed + { + return $this->fallbackValue; + } +} diff --git a/src/Exception/SanitizationException.php b/src/Exception/SanitizationException.php new file mode 100644 index 0000000..aeaeaf8 --- /dev/null +++ b/src/Exception/SanitizationException.php @@ -0,0 +1,9 @@ +guardAgainstNonString($input), + FILTER_SANITIZE_EMAIL + ) ?: ''; + } +} diff --git a/src/Processor/Cleaner/NumericValueCleaner.php b/src/Processor/Cleaner/NumericValueCleaner.php new file mode 100644 index 0000000..b428eeb --- /dev/null +++ b/src/Processor/Cleaner/NumericValueCleaner.php @@ -0,0 +1,19 @@ +guardAgainstNonString($input), + FILTER_SANITIZE_URL + ) ?: ''; + } +} diff --git a/src/Processor/Encoder/HtmlSpecialCharsEncoder.php b/src/Processor/Encoder/HtmlSpecialCharsEncoder.php new file mode 100644 index 0000000..fce0977 --- /dev/null +++ b/src/Processor/Encoder/HtmlSpecialCharsEncoder.php @@ -0,0 +1,19 @@ +guardAgainstNonString($input), + ENT_QUOTES | ENT_HTML5, + 'UTF-8' + ); + } +} diff --git a/src/Processor/HtmlPurifier.php b/src/Processor/HtmlPurifier.php new file mode 100644 index 0000000..5d75e82 --- /dev/null +++ b/src/Processor/HtmlPurifier.php @@ -0,0 +1,26 @@ +allowedTags = $options['allowedTags']; + } + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + + return strip_tags($input, $this->allowedTags); + } +} diff --git a/src/Processor/Remover/HtmlTagRemover.php b/src/Processor/Remover/HtmlTagRemover.php new file mode 100644 index 0000000..24f88d4 --- /dev/null +++ b/src/Processor/Remover/HtmlTagRemover.php @@ -0,0 +1,25 @@ +allowedTags = $options['allowedTags']; + } + } + + public function process(mixed $input): string + { + return strip_tags($this->guardAgainstNonString($input), $this->allowedTags); + } +} diff --git a/src/Processor/Remover/WhitespaceRemover.php b/src/Processor/Remover/WhitespaceRemover.php new file mode 100644 index 0000000..551dd43 --- /dev/null +++ b/src/Processor/Remover/WhitespaceRemover.php @@ -0,0 +1,25 @@ +charlist = $options['charlist']; + } + } + + public function process(mixed $input): string + { + return trim($this->guardAgainstNonString($input), $this->charlist); + } +} diff --git a/src/Processor/WhitespaceSanitizer.php b/src/Processor/WhitespaceSanitizer.php new file mode 100644 index 0000000..860c261 --- /dev/null +++ b/src/Processor/WhitespaceSanitizer.php @@ -0,0 +1,15 @@ +guardAgainstNonString($input); + + return preg_replace('/\s+/', ' ', trim($input)); + } +} diff --git a/src/Processor/XssSanitizer.php b/src/Processor/XssSanitizer.php new file mode 100644 index 0000000..d5eb8f8 --- /dev/null +++ b/src/Processor/XssSanitizer.php @@ -0,0 +1,15 @@ +guardAgainstNonString($input); + + return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} diff --git a/src/Sanitizer.php b/src/Sanitizer.php new file mode 100644 index 0000000..6b35b63 --- /dev/null +++ b/src/Sanitizer.php @@ -0,0 +1,46 @@ +builder = new ProcessorBuilder($this->registry); + $this->attributeHandler = new AttributeHandler(self::IDENTIFIER, $this->builder); + $this->propertyInspector = new PropertyInspector( + new AttributeAnalyzer(Sanitize::class) + ); + } + + public function sanitize(mixed $object): array + { + try { + $sanitizedValues = $this->propertyInspector->inspect($object, $this->attributeHandler); + $this->attributeHandler->applyChanges($object); + + return $sanitizedValues; + } catch (PropertyInspectionException $e) { + return []; + } + } +} diff --git a/tests/Attribute/SanitizeTest.php b/tests/Attribute/SanitizeTest.php new file mode 100644 index 0000000..41227d1 --- /dev/null +++ b/tests/Attribute/SanitizeTest.php @@ -0,0 +1,78 @@ +assertInstanceOf(ProcessableAttribute::class, new Sanitize([])); + } + + public function testSanitizeIsAttribute(): void + { + $reflectionClass = new \ReflectionClass(Sanitize::class); + $attributes = $reflectionClass->getAttributes(); + + $this->assertCount(1, $attributes); + $this->assertSame(\Attribute::class, $attributes[0]->getName()); + $this->assertSame([\Attribute::TARGET_PROPERTY], $attributes[0]->getArguments()); + } + + public function testConstructorSetsSanitizers(): void + { + $sanitizers = ['trim', 'htmlspecialchars']; + $sanitize = new Sanitize($sanitizers); + + $this->assertSame($sanitizers, $sanitize->sanitizers); + } + + public function testConstructorSetsFallbackValue(): void + { + $fallbackValue = 'default'; + $sanitize = new Sanitize([], $fallbackValue); + + $this->assertSame($fallbackValue, $sanitize->fallbackValue); + } + + public function testConstructorSetsNullFallbackValueByDefault(): void + { + $sanitize = new Sanitize([]); + + $this->assertNull($sanitize->fallbackValue); + } + + public function testGetProcessorsReturnsSanitizers(): void + { + $sanitizers = ['trim', 'htmlspecialchars']; + $sanitize = new Sanitize($sanitizers); + + $this->assertSame($sanitizers, $sanitize->getProcessors()); + } + + public function testGetFallbackValueReturnsFallbackValue(): void + { + $fallbackValue = 'default'; + $sanitize = new Sanitize([], $fallbackValue); + + $this->assertSame($fallbackValue, $sanitize->getFallbackValue()); + } + + public function testSanitizeWithMultipleArguments(): void + { + $sanitizers = ['trim', 'htmlspecialchars']; + $fallbackValue = 'default'; + $sanitize = new Sanitize($sanitizers, $fallbackValue); + + $this->assertSame($sanitizers, $sanitize->sanitizers); + $this->assertSame($fallbackValue, $sanitize->fallbackValue); + $this->assertSame($sanitizers, $sanitize->getProcessors()); + $this->assertSame($fallbackValue, $sanitize->getFallbackValue()); + } +} diff --git a/tests/Processor/Cleaner/EmailAddressCleanerTest.php b/tests/Processor/Cleaner/EmailAddressCleanerTest.php new file mode 100644 index 0000000..ffc9e8c --- /dev/null +++ b/tests/Processor/Cleaner/EmailAddressCleanerTest.php @@ -0,0 +1,68 @@ +cleaner = new EmailAddressCleaner(); + } + + /** + * @dataProvider validEmailProvider + */ + public function testProcessWithValidEmail(string $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function validEmailProvider(): array + { + return [ + ['test@example.com', 'test@example.com'], + ['test+filter@example.com', 'test+filter@example.com'], + [' test@example.com ', 'test@example.com'], + ['TEST@EXAMPLE.COM', 'TEST@EXAMPLE.COM'], + ]; + } + + /** + * @dataProvider invalidEmailProvider + */ + public function testProcessWithInvalidEmail(string $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function invalidEmailProvider(): array + { + return [ + ['not an email', 'notanemail'], + ['test@', 'test@'], + ['@example.com', '@example.com'], + ['test@example', 'test@example'], + ]; + } + + public function testProcessWithNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->cleaner->process(123); + } +} diff --git a/tests/Processor/Cleaner/NumericValueCleanerTest.php b/tests/Processor/Cleaner/NumericValueCleanerTest.php new file mode 100644 index 0000000..a3e86e6 --- /dev/null +++ b/tests/Processor/Cleaner/NumericValueCleanerTest.php @@ -0,0 +1,63 @@ +cleaner = new NumericValueCleaner(); + } + + /** + * @dataProvider validNumericProvider + */ + public function testProcessWithValidNumeric(mixed $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function validNumericProvider(): array + { + return [ + [123, '123'], + [123.45, '123.45'], + ['123', '123'], + ['123.45', '123.45'], + [' 123.45 ', '123.45'], + ['-123.45', '-123.45'], + ['+123.45', '+123.45'], + ]; + } + + /** + * @dataProvider invalidNumericProvider + */ + public function testProcessWithInvalidNumeric(mixed $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function invalidNumericProvider(): array + { + return [ + ['not a number', '0'], + ['123abc', '123'], + ['abc123', '123'], + ['', '0'], + ]; + } +} diff --git a/tests/Processor/Cleaner/UrlAddressCleanerTest.php b/tests/Processor/Cleaner/UrlAddressCleanerTest.php new file mode 100644 index 0000000..f79857c --- /dev/null +++ b/tests/Processor/Cleaner/UrlAddressCleanerTest.php @@ -0,0 +1,67 @@ +cleaner = new UrlAddressCleaner(); + } + + /** + * @dataProvider validUrlProvider + */ + public function testProcessWithValidUrl(string $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function validUrlProvider(): array + { + return [ + ['https://www.example.com', 'https://www.example.com'], + ['http://example.com/path?query=value', 'http://example.com/path?query=value'], + [' https://www.example.com ', 'https://www.example.com'], + ['www.example.com', 'www.example.com'], + ]; + } + + /** + * @dataProvider invalidUrlProvider + */ + public function testProcessWithInvalidUrl(string $input, string $expected): void + { + $this->assertSame($expected, $this->cleaner->process($input)); + } + + /** + * @return array + */ + public static function invalidUrlProvider(): array + { + return [ + ['http://', 'http://'], + ['https://', 'https://'], + ['ftp:/example.com', 'ftp:/example.com'], + ]; + } + + public function testProcessWithNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->cleaner->process(123); + } +} diff --git a/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php b/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php new file mode 100644 index 0000000..09398fb --- /dev/null +++ b/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php @@ -0,0 +1,47 @@ +encoder = new HtmlSpecialCharsEncoder(); + } + + /** + * @dataProvider htmlStringProvider + */ + public function testProcessWithHtmlString(string $input, string $expected): void + { + $this->assertSame($expected, $this->encoder->process($input)); + } + + /** + * @return array + */ + public static function htmlStringProvider(): array + { + return [ + ['

Test

', '<p>Test</p>'], + ['"quoted" & \'single-quoted\'', '"quoted" & 'single-quoted''], // Alterado para ' + ['Link', '<a href="https://example.com">Link</a>'], + ['Normal text', 'Normal text'], + ]; + } + + public function testProcessWithNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->encoder->process(123); + } +} diff --git a/tests/Processor/Remover/HtmlTagRemoverTest.php b/tests/Processor/Remover/HtmlTagRemoverTest.php new file mode 100644 index 0000000..02a6417 --- /dev/null +++ b/tests/Processor/Remover/HtmlTagRemoverTest.php @@ -0,0 +1,56 @@ +remover = new HtmlTagRemover(); + } + + /** + * @dataProvider htmlStringProvider + */ + public function testProcessWithHtmlString(string $input, string $expected): void + { + $this->assertSame($expected, $this->remover->process($input)); + } + + /** + * @return array + */ + public static function htmlStringProvider(): array + { + return [ + ['

Test

', 'Test'], + ['Link', 'Link'], + ['', 'alert("XSS");'], + ['Normal text', 'Normal text'], + ]; + } + + public function testProcessWithAllowedTags(): void + { + $this->remover->configure(['allowedTags' => ['p', 'a']]); + $input = '

Test

Link'; + $expected = '

Test

Linkalert("XSS");'; + + $this->assertSame($expected, $this->remover->process($input)); + } + + public function testProcessWithNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->remover->process(123); + } +} diff --git a/tests/Processor/Remover/WhitespaceRemoverTest.php b/tests/Processor/Remover/WhitespaceRemoverTest.php new file mode 100644 index 0000000..b437b4f --- /dev/null +++ b/tests/Processor/Remover/WhitespaceRemoverTest.php @@ -0,0 +1,53 @@ +remover = new WhitespaceRemover(); + } + + /** + * @dataProvider whitespaceStringProvider + */ + public function testProcessWithWhitespaceString(string $input, string $expected): void + { + $this->assertSame($expected, $this->remover->process($input)); + } + + /** + * @return array + */ + public static function whitespaceStringProvider(): array + { + return [ + [' Test ', 'Test'], + ["\t\tTest\t\t", 'Test'], + ["\nTest\n", 'Test'], + [" \t\n\r\0\x0BTest \t\n\r\0\x0B", 'Test'], + ]; + } + + public function testProcessWithCustomCharlist(): void + { + $this->remover->configure(['charlist' => 'a']); + $this->assertSame('Test', $this->remover->process('aaaTestaaa')); + } + + public function testProcessWithNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->remover->process(123); + } +} diff --git a/tests/SanitizerTest.php b/tests/SanitizerTest.php new file mode 100644 index 0000000..f2645ed --- /dev/null +++ b/tests/SanitizerTest.php @@ -0,0 +1,188 @@ +registry = $this->createMock(ProcessorRegistry::class); + $this->sanitizer = new Sanitizer($this->registry); + } + + public function testSanitizeProcessesObjectProperties(): void + { + $testObject = new class { + #[Sanitize(sanitizers: ['trim'])] + public string $name = ' John Doe '; + + #[Sanitize(sanitizers: ['email'])] + public string $email = 'john.doe@example..com'; + }; + + $trimProcessor = $this->createMock(Processor::class); + $trimProcessor->expects($this->once()) + ->method('process') + ->with(' John Doe ') + ->willReturn('John Doe'); + + $emailProcessor = $this->createMock(Processor::class); + $emailProcessor->expects($this->once()) + ->method('process') + ->with('john.doe@example..com') + ->willReturn('john.doe@example.com'); + + $this->registry->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['sanitizer', 'trim', $trimProcessor], + ['sanitizer', 'email', $emailProcessor], + ]); + + $sanitizedValues = $this->sanitizer->sanitize($testObject); + + $this->assertSame('John Doe', $testObject->name); + $this->assertSame('john.doe@example.com', $testObject->email); + $this->assertArrayHasKey('name', $sanitizedValues); + $this->assertArrayHasKey('email', $sanitizedValues); + $this->assertSame(['John Doe'], $sanitizedValues['name']); + $this->assertSame(['john.doe@example.com'], $sanitizedValues['email']); + } + + public function testSanitizeHandlesNonProcessableAttributes(): void + { + $testObject = new class { + #[Sanitize(sanitizers: ['trim'])] + public string $processable = ' trim me '; + + public string $nonProcessable = 'leave me alone'; + }; + + $trimProcessor = $this->createMock(Processor::class); + $trimProcessor->expects($this->once()) + ->method('process') + ->with(' trim me ') + ->willReturn('trim me'); + + $this->registry->expects($this->once()) + ->method('get') + ->with('sanitizer', 'trim') + ->willReturn($trimProcessor); + + $sanitizedValues = $this->sanitizer->sanitize($testObject); + + $this->assertSame('trim me', $testObject->processable); + $this->assertSame('leave me alone', $testObject->nonProcessable); + $this->assertArrayHasKey('processable', $sanitizedValues); + $this->assertArrayNotHasKey('nonProcessable', $sanitizedValues); + } + + public function testSanitizeHandlesExceptionsAndUsesFallbackValue(): void + { + $testObject = new class { + #[Sanitize(sanitizers: ['problematic'], fallbackValue: 'fallback')] + public string $problematic = 'cause problem'; + }; + + $problematicProcessor = $this->createMock(Processor::class); + $problematicProcessor->expects($this->once()) + ->method('process') + ->willThrowException(new \Exception('Processing failed')); + + $this->registry->expects($this->once()) + ->method('get') + ->with('sanitizer', 'problematic') + ->willReturn($problematicProcessor); + + $sanitizedValues = $this->sanitizer->sanitize($testObject); + + $this->assertSame('cause problem', $testObject->problematic); + $this->assertEmpty($sanitizedValues); + } + + public function testSanitizeHandlesPrivateAndProtectedProperties(): void + { + $testObject = new class { + #[Sanitize(sanitizers: ['trim'])] + private string $privateProp = ' private '; + + #[Sanitize(sanitizers: ['trim'])] + protected string $protectedProp = ' protected '; + + public function getPrivateProp(): string + { + return $this->privateProp; + } + + public function getProtectedProp(): string + { + return $this->protectedProp; + } + }; + + $trimProcessor = $this->createMock(Processor::class); + $trimProcessor->expects($this->exactly(2)) + ->method('process') + ->willReturnMap([ + [' private ', 'private'], + [' protected ', 'protected'], + ]); + + $this->registry->expects($this->exactly(2)) + ->method('get') + ->with('sanitizer', 'trim') + ->willReturn($trimProcessor); + + $sanitizedValues = $this->sanitizer->sanitize($testObject); + + $this->assertSame('private', $testObject->getPrivateProp()); + $this->assertSame('protected', $testObject->getProtectedProp()); + $this->assertArrayHasKey('privateProp', $sanitizedValues); + $this->assertArrayHasKey('protectedProp', $sanitizedValues); + } + + public function testSanitizeHandlesMultipleProcessorsForSingleProperty(): void + { + $testObject = new class { + #[Sanitize(sanitizers: ['trim', 'uppercase'])] + public string $multiProcessed = ' hello world '; + }; + + $trimProcessor = $this->createMock(Processor::class); + $trimProcessor->expects($this->once()) + ->method('process') + ->with(' hello world ') + ->willReturn('hello world'); + + $uppercaseProcessor = $this->createMock(Processor::class); + $uppercaseProcessor->expects($this->once()) + ->method('process') + ->with('hello world') + ->willReturn('HELLO WORLD'); + + $this->registry->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + ['sanitizer', 'trim', $trimProcessor], + ['sanitizer', 'uppercase', $uppercaseProcessor], + ]); + + $sanitizedValues = $this->sanitizer->sanitize($testObject); + + $this->assertSame('HELLO WORLD', $testObject->multiProcessed); + $this->assertArrayHasKey('multiProcessed', $sanitizedValues); + $this->assertSame(['HELLO WORLD'], $sanitizedValues['multiProcessed']); + } +} diff --git a/tests/application.php b/tests/application.php index 6c8c4f5..1045a38 100644 --- a/tests/application.php +++ b/tests/application.php @@ -1,3 +1,104 @@ name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getEmail(): string + { + return $this->email; + } + + public function setEmail(string $email): void + { + $this->email = $email; + } + + public function getAge(): string + { + return $this->age; + } + + public function setAge(string $age): void + { + $this->age = $age; + } + + public function getBio(): string + { + return $this->bio; + } + + public function setBio(string $bio): void + { + $this->bio = $bio; + } +} + +// Set up the ProcessorRegistry +$registry = new ProcessorRegistry(); +$registry->register('sanitizer', 'trim', new WhitespaceRemover()); +$registry->register('sanitizer', 'html_purifier', new HtmlPurifier()); +$registry->register('sanitizer', 'email_cleaner', new EmailAddressCleaner()); +$registry->register('sanitizer', 'numeric_value_cleaner', new NumericValueCleaner()); +$registry->register('sanitizer', 'xss_sanitizer', new XssSanitizer()); + +$autoSanitizer = new Sanitizer($registry); + +// Create a UserInput object with potentially unsafe data +$userInput = new UserInput(); +$userInput->setName(" John Doe "); +$userInput->setEmail(' john.doe@example..com '); +$userInput->setAge(' 25 years old '); +$userInput->setBio("

Hello, I'm John!

"); + +// Display original values +echo "Original values:\n"; +echo 'Name: ' . $userInput->getName() . "\n"; +echo 'Email: ' . $userInput->getEmail() . "\n"; +echo 'Age: ' . $userInput->getAge() . "\n"; +echo 'Bio: ' . $userInput->getBio() . "\n\n"; + +// Sanitize the user input +$autoSanitizer->sanitize($userInput); + +// Display sanitized values +echo "Sanitized values:\n"; +echo 'Name: ' . $userInput->getName() . "\n"; +echo 'Email: ' . $userInput->getEmail() . "\n"; +echo 'Age: ' . $userInput->getAge() . "\n"; +echo 'Bio: ' . $userInput->getBio() . "\n"; From e80dd9955884d6d628d42fa7d54e73d00e9d8531 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 15 Oct 2024 16:20:27 -0300 Subject: [PATCH 2/4] refactor(processor): restructure sanitizers and add new domain, input, and security modules - Removed legacy cleaner classes: EmailAddressCleaner, NumericValueCleaner, UrlAddressCleaner - Removed HtmlTagRemover, WhitespaceRemover, and HtmlSpecialCharsEncoder - Added new domain-level sanitizers: HtmlPurifierSanitizer, JsonSanitizer, and MarkdownSanitizer - Reorganized and added input sanitizers: HtmlSpecialCharsSanitizer, NormalizeLineBreaksSanitizer, StripTagsSanitizer (renamed from HtmlPurifier), TrimSanitizer - Added new security sanitizers: FilenameSanitizer, SqlInjectionSanitizer, XssSanitizer (moved to security module) - Updated and added corresponding test cases for all new and renamed sanitizers - Removed outdated test files for the deleted processors - Modified tests/application.php for compatibility with the new processor structure --- src/Processor/Cleaner/EmailAddressCleaner.php | 18 --- src/Processor/Cleaner/NumericValueCleaner.php | 19 --- src/Processor/Cleaner/UrlAddressCleaner.php | 18 --- .../Domain/HtmlPurifierSanitizer.php | 151 ++++++++++++++++++ src/Processor/Domain/JsonSanitizer.php | 21 +++ src/Processor/Domain/MarkdownSanitizer.php | 21 +++ .../Encoder/HtmlSpecialCharsEncoder.php | 19 --- .../Input/HtmlSpecialCharsSanitizer.php | 17 ++ .../Input/NormalizeLineBreaksSanitizer.php | 17 ++ .../StripTagsSanitizer.php} | 7 +- src/Processor/Input/TrimSanitizer.php | 27 ++++ src/Processor/Remover/HtmlTagRemover.php | 25 --- src/Processor/Remover/WhitespaceRemover.php | 25 --- src/Processor/Security/FilenameSanitizer.php | 71 ++++++++ .../Security/SqlInjectionSanitizer.php | 54 +++++++ src/Processor/{ => Security}/XssSanitizer.php | 4 +- src/Processor/WhitespaceSanitizer.php | 15 -- .../Cleaner/EmailAddressCleanerTest.php | 68 -------- .../Cleaner/NumericValueCleanerTest.php | 63 -------- .../Cleaner/UrlAddressCleanerTest.php | 67 -------- .../Domain/HtmlPurifierSanitizerTest.php | 69 ++++++++ tests/Processor/Domain/JsonSanitizerTest.php | 26 +++ .../Domain/MarkdownSanitizerTest.php | 21 +++ .../Encoder/HtmlSpecialCharsEncoderTest.php | 47 ------ .../Input/HtmlSpecialCharsSanitizerTest.php | 20 +++ .../NormalizeLineBreaksSanitizerTest.php | 20 +++ .../Input/StripTagsSanitizerTest.php | 67 ++++++++ tests/Processor/Input/TrimSanitizerTest.php | 29 ++++ .../Processor/Remover/HtmlTagRemoverTest.php | 56 ------- .../Remover/WhitespaceRemoverTest.php | 53 ------ .../Security/FilenameSanitizerTest.php | 108 +++++++++++++ .../Security/SqlInjectionSanitizerTest.php | 103 ++++++++++++ tests/Processor/Security/XssSanitizerTest.php | 20 +++ tests/application.php | 2 +- 34 files changed, 870 insertions(+), 498 deletions(-) delete mode 100644 src/Processor/Cleaner/EmailAddressCleaner.php delete mode 100644 src/Processor/Cleaner/NumericValueCleaner.php delete mode 100644 src/Processor/Cleaner/UrlAddressCleaner.php create mode 100644 src/Processor/Domain/HtmlPurifierSanitizer.php create mode 100644 src/Processor/Domain/JsonSanitizer.php create mode 100644 src/Processor/Domain/MarkdownSanitizer.php delete mode 100644 src/Processor/Encoder/HtmlSpecialCharsEncoder.php create mode 100644 src/Processor/Input/HtmlSpecialCharsSanitizer.php create mode 100644 src/Processor/Input/NormalizeLineBreaksSanitizer.php rename src/Processor/{HtmlPurifier.php => Input/StripTagsSanitizer.php} (65%) create mode 100644 src/Processor/Input/TrimSanitizer.php delete mode 100644 src/Processor/Remover/HtmlTagRemover.php delete mode 100644 src/Processor/Remover/WhitespaceRemover.php create mode 100644 src/Processor/Security/FilenameSanitizer.php create mode 100644 src/Processor/Security/SqlInjectionSanitizer.php rename src/Processor/{ => Security}/XssSanitizer.php (71%) delete mode 100644 src/Processor/WhitespaceSanitizer.php delete mode 100644 tests/Processor/Cleaner/EmailAddressCleanerTest.php delete mode 100644 tests/Processor/Cleaner/NumericValueCleanerTest.php delete mode 100644 tests/Processor/Cleaner/UrlAddressCleanerTest.php create mode 100644 tests/Processor/Domain/HtmlPurifierSanitizerTest.php create mode 100644 tests/Processor/Domain/JsonSanitizerTest.php create mode 100644 tests/Processor/Domain/MarkdownSanitizerTest.php delete mode 100644 tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php create mode 100644 tests/Processor/Input/HtmlSpecialCharsSanitizerTest.php create mode 100644 tests/Processor/Input/NormalizeLineBreaksSanitizerTest.php create mode 100644 tests/Processor/Input/StripTagsSanitizerTest.php create mode 100644 tests/Processor/Input/TrimSanitizerTest.php delete mode 100644 tests/Processor/Remover/HtmlTagRemoverTest.php delete mode 100644 tests/Processor/Remover/WhitespaceRemoverTest.php create mode 100644 tests/Processor/Security/FilenameSanitizerTest.php create mode 100644 tests/Processor/Security/SqlInjectionSanitizerTest.php create mode 100644 tests/Processor/Security/XssSanitizerTest.php diff --git a/src/Processor/Cleaner/EmailAddressCleaner.php b/src/Processor/Cleaner/EmailAddressCleaner.php deleted file mode 100644 index da76c53..0000000 --- a/src/Processor/Cleaner/EmailAddressCleaner.php +++ /dev/null @@ -1,18 +0,0 @@ -guardAgainstNonString($input), - FILTER_SANITIZE_EMAIL - ) ?: ''; - } -} diff --git a/src/Processor/Cleaner/NumericValueCleaner.php b/src/Processor/Cleaner/NumericValueCleaner.php deleted file mode 100644 index b428eeb..0000000 --- a/src/Processor/Cleaner/NumericValueCleaner.php +++ /dev/null @@ -1,19 +0,0 @@ -guardAgainstNonString($input), - FILTER_SANITIZE_URL - ) ?: ''; - } -} diff --git a/src/Processor/Domain/HtmlPurifierSanitizer.php b/src/Processor/Domain/HtmlPurifierSanitizer.php new file mode 100644 index 0000000..683070b --- /dev/null +++ b/src/Processor/Domain/HtmlPurifierSanitizer.php @@ -0,0 +1,151 @@ + ['a'], 'src' => ['img'], 'alt' => ['img']]; + private const DEFAULT_ENCODING = 'UTF-8'; + + private array $allowedTags; + private array $allowedAttributes; + private string $encoding; + + public function __construct() + { + $this->allowedTags = self::DEFAULT_ALLOWED_TAGS; + $this->allowedAttributes = self::DEFAULT_ALLOWED_ATTRIBUTES; + $this->encoding = self::DEFAULT_ENCODING; + } + + public function configure(array $options): void + { + $this->allowedTags = $options['allowedTags'] ?? $this->allowedTags; + $this->allowedAttributes = $options['allowedAttributes'] ?? $this->allowedAttributes; + $this->encoding = $options['encoding'] ?? $this->encoding; + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + $input = $this->sanitizeHtml($input); + + $dom = new \DOMDocument('1.0', $this->encoding); + $this->loadHtmlToDom($dom, $input); + $this->filterNodes($dom->getElementsByTagName('*')); + + return $this->cleanHtmlOutput($dom->saveHTML()); + } + + private function filterNodes(\DOMNodeList $nodes): void + { + for ($i = $nodes->length - 1; $i >= 0; --$i) { + $node = $nodes->item($i); + if (!$this->isAllowedTag($node->nodeName)) { + $this->unwrapNode($node); + } else { + $this->filterAttributes($node); + } + } + } + + private function filterAttributes(\DOMElement $element): void + { + for ($i = $element->attributes->length - 1; $i >= 0; --$i) { + /** @var DOMNode */ + $attr = $element->attributes->item($i); + if (!$this->isAllowedAttribute($element->nodeName, $attr->name)) { + $element->removeAttribute($attr->name); + } + } + } + + private function sanitizeHtml(string $html): string + { + $html = $this->removeScripts($html); + + return $this->removeComments($html); + } + + private function loadHtmlToDom(\DOMDocument $dom, string $html): void + { + libxml_use_internal_errors(true); + $isLoaded = $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', $this->encoding), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + $errors = libxml_get_errors(); + libxml_clear_errors(); + } + + private function cleanHtmlOutput(string $output): string + { + $output = preg_replace('/^/', '', $output); + $output = str_replace(['', '', '', ''], '', $output); + + return trim($output); + } + + private function isAllowedTag(string $tagName): bool + { + return in_array(strtolower($tagName), $this->allowedTags, true); + } + + private function isAllowedAttribute(string $elementName, string $attributeName): bool + { + return isset($this->allowedAttributes[strtolower($attributeName)]) + && in_array(strtolower($elementName), $this->allowedAttributes[strtolower($attributeName)], true); + } + + private function unwrapNode(\DOMNode $node): void + { + $parent = $node->parentNode; + while ($node->firstChild) { + $parent->insertBefore($node->firstChild, $node); + } + $parent->removeChild($node); + } + + private function removeScripts(string $html): string + { + return $this->removeElementsByTagName('script', $html); + } + + private function removeComments(string $html): string + { + return $this->removeElementsByXPath('//comment()', $html); + } + + private function removeElementsByTagName(string $tagName, string $html): string + { + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', $this->encoding), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + + $elements = $dom->getElementsByTagName($tagName); + while ($elements->length > 0) { + $elements->item(0)->parentNode->removeChild($elements->item(0)); + } + + return $dom->saveHTML(); + } + + private function removeElementsByXPath(string $query, string $html): string + { + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', $this->encoding), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + + $xpath = new \DOMXPath($dom); + foreach ($xpath->query($query) as $element) { + $element->parentNode->removeChild($element); + } + + return $dom->saveHTML(); + } +} diff --git a/src/Processor/Domain/JsonSanitizer.php b/src/Processor/Domain/JsonSanitizer.php new file mode 100644 index 0000000..26437d0 --- /dev/null +++ b/src/Processor/Domain/JsonSanitizer.php @@ -0,0 +1,21 @@ +guardAgainstNonString($input); + $decoded = json_decode($input, true); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException('Invalid JSON input'); + } + + return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + } +} diff --git a/src/Processor/Domain/MarkdownSanitizer.php b/src/Processor/Domain/MarkdownSanitizer.php new file mode 100644 index 0000000..7aca5e5 --- /dev/null +++ b/src/Processor/Domain/MarkdownSanitizer.php @@ -0,0 +1,21 @@ +guardAgainstNonString($input); + // Remove HTML tags, keeping Markdown intact + $input = strip_tags($input); + // Escape special Markdown characters + $input = preg_replace('/([*_`#])/', '\\\\$1', $input); + + return $input; + } +} diff --git a/src/Processor/Encoder/HtmlSpecialCharsEncoder.php b/src/Processor/Encoder/HtmlSpecialCharsEncoder.php deleted file mode 100644 index fce0977..0000000 --- a/src/Processor/Encoder/HtmlSpecialCharsEncoder.php +++ /dev/null @@ -1,19 +0,0 @@ -guardAgainstNonString($input), - ENT_QUOTES | ENT_HTML5, - 'UTF-8' - ); - } -} diff --git a/src/Processor/Input/HtmlSpecialCharsSanitizer.php b/src/Processor/Input/HtmlSpecialCharsSanitizer.php new file mode 100644 index 0000000..35b1fcd --- /dev/null +++ b/src/Processor/Input/HtmlSpecialCharsSanitizer.php @@ -0,0 +1,17 @@ +guardAgainstNonString($input); + + return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} diff --git a/src/Processor/Input/NormalizeLineBreaksSanitizer.php b/src/Processor/Input/NormalizeLineBreaksSanitizer.php new file mode 100644 index 0000000..c7db3dc --- /dev/null +++ b/src/Processor/Input/NormalizeLineBreaksSanitizer.php @@ -0,0 +1,17 @@ +guardAgainstNonString($input); + + return str_replace(["\r\n", "\r"], "\n", $input); + } +} diff --git a/src/Processor/HtmlPurifier.php b/src/Processor/Input/StripTagsSanitizer.php similarity index 65% rename from src/Processor/HtmlPurifier.php rename to src/Processor/Input/StripTagsSanitizer.php index 5d75e82..4a16242 100644 --- a/src/Processor/HtmlPurifier.php +++ b/src/Processor/Input/StripTagsSanitizer.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace KaririCode\Sanitizer\Processor; +namespace KaririCode\Sanitizer\Processor\Input; use KaririCode\Contract\Processor\ConfigurableProcessor; +use KaririCode\Sanitizer\Processor\AbstractSanitizerProcessor; -class HtmlPurifier extends AbstractSanitizerProcessor implements ConfigurableProcessor +class StripTagsSanitizer extends AbstractSanitizerProcessor implements ConfigurableProcessor { - private array $allowedTags = ['p', 'br', 'strong', 'em']; + private array $allowedTags = []; public function configure(array $options): void { diff --git a/src/Processor/Input/TrimSanitizer.php b/src/Processor/Input/TrimSanitizer.php new file mode 100644 index 0000000..a0566ab --- /dev/null +++ b/src/Processor/Input/TrimSanitizer.php @@ -0,0 +1,27 @@ +characterMask = $options['characterMask']; + } + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + + return trim($input, $this->characterMask); + } +} diff --git a/src/Processor/Remover/HtmlTagRemover.php b/src/Processor/Remover/HtmlTagRemover.php deleted file mode 100644 index 24f88d4..0000000 --- a/src/Processor/Remover/HtmlTagRemover.php +++ /dev/null @@ -1,25 +0,0 @@ -allowedTags = $options['allowedTags']; - } - } - - public function process(mixed $input): string - { - return strip_tags($this->guardAgainstNonString($input), $this->allowedTags); - } -} diff --git a/src/Processor/Remover/WhitespaceRemover.php b/src/Processor/Remover/WhitespaceRemover.php deleted file mode 100644 index 551dd43..0000000 --- a/src/Processor/Remover/WhitespaceRemover.php +++ /dev/null @@ -1,25 +0,0 @@ -charlist = $options['charlist']; - } - } - - public function process(mixed $input): string - { - return trim($this->guardAgainstNonString($input), $this->charlist); - } -} diff --git a/src/Processor/Security/FilenameSanitizer.php b/src/Processor/Security/FilenameSanitizer.php new file mode 100644 index 0000000..da4a321 --- /dev/null +++ b/src/Processor/Security/FilenameSanitizer.php @@ -0,0 +1,71 @@ +isValidReplacement($options['replacement'])) { + $this->replacement = $options['replacement']; + } + + if (isset($options['preserveExtension'])) { + $this->preserveExtension = (bool) $options['preserveExtension']; + } + + if (isset($options['allowedChars']) && is_array($options['allowedChars'])) { + $this->allowedChars = implode('', $options['allowedChars']); + } + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + + if ('' === $input) { + return ''; + } + + [$filename, $extension] = $this->splitFilename($input); + $sanitized = $this->sanitizeFilename($filename); + + return $sanitized . $extension; + } + + private function isValidReplacement(string $replacement): bool + { + return 1 === preg_match('/^[\w\-]$/', $replacement); + } + + private function splitFilename(string $input): array + { + if ($this->preserveExtension) { + $pathInfo = pathinfo($input); + $filename = $pathInfo['filename'] ?? ''; + $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; + } else { + $filename = preg_replace('/\.[^.]+$/', '', $input) ?: ''; + $extension = ''; + } + + return [$filename, $extension]; + } + + private function sanitizeFilename(string $filename): string + { + $sanitized = preg_replace("/[^{$this->allowedChars}]/", $this->replacement, $filename) ?? ''; + $sanitized = preg_replace('/' . preg_quote($this->replacement, '/') . '+/', $this->replacement, $sanitized) ?? ''; + + return trim($sanitized, $this->replacement); + } +} diff --git a/src/Processor/Security/SqlInjectionSanitizer.php b/src/Processor/Security/SqlInjectionSanitizer.php new file mode 100644 index 0000000..60c4ee3 --- /dev/null +++ b/src/Processor/Security/SqlInjectionSanitizer.php @@ -0,0 +1,54 @@ + '', // Remove single-line comments + '/\/\*.*?\*\//s' => '', // Remove multi-line comments + '/;/' => '', // Remove semicolons completamente + '/\s+/' => ' ', // Normalize whitespace + ]; + + private array $escapeMap = [ + "\x00" => '\\0', + "\n" => '\\n', + "\r" => '\\r', + '\\' => '\\\\', + "'" => "\\'", + '"' => '\\"', + "\x1a" => '\\Z', + ]; + + public function configure(array $options): void + { + if (isset($options['escapeMap']) && is_array($options['escapeMap'])) { + $this->escapeMap = array_merge($this->escapeMap, $options['escapeMap']); + } + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + $input = $this->removeSuspiciousPatterns($input); + + return $this->escapeString($input); + } + + private function removeSuspiciousPatterns(string $input): string + { + return preg_replace(array_keys(self::SUSPICIOUS_PATTERNS), array_values(self::SUSPICIOUS_PATTERNS), $input); + } + + private function escapeString(string $input): string + { + return strtr($input, $this->escapeMap); + } +} diff --git a/src/Processor/XssSanitizer.php b/src/Processor/Security/XssSanitizer.php similarity index 71% rename from src/Processor/XssSanitizer.php rename to src/Processor/Security/XssSanitizer.php index d5eb8f8..6c815dc 100644 --- a/src/Processor/XssSanitizer.php +++ b/src/Processor/Security/XssSanitizer.php @@ -2,7 +2,9 @@ declare(strict_types=1); -namespace KaririCode\Sanitizer\Processor; +namespace KaririCode\Sanitizer\Processor\Security; + +use KaririCode\Sanitizer\Processor\AbstractSanitizerProcessor; class XssSanitizer extends AbstractSanitizerProcessor { diff --git a/src/Processor/WhitespaceSanitizer.php b/src/Processor/WhitespaceSanitizer.php deleted file mode 100644 index 860c261..0000000 --- a/src/Processor/WhitespaceSanitizer.php +++ /dev/null @@ -1,15 +0,0 @@ -guardAgainstNonString($input); - - return preg_replace('/\s+/', ' ', trim($input)); - } -} diff --git a/tests/Processor/Cleaner/EmailAddressCleanerTest.php b/tests/Processor/Cleaner/EmailAddressCleanerTest.php deleted file mode 100644 index ffc9e8c..0000000 --- a/tests/Processor/Cleaner/EmailAddressCleanerTest.php +++ /dev/null @@ -1,68 +0,0 @@ -cleaner = new EmailAddressCleaner(); - } - - /** - * @dataProvider validEmailProvider - */ - public function testProcessWithValidEmail(string $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function validEmailProvider(): array - { - return [ - ['test@example.com', 'test@example.com'], - ['test+filter@example.com', 'test+filter@example.com'], - [' test@example.com ', 'test@example.com'], - ['TEST@EXAMPLE.COM', 'TEST@EXAMPLE.COM'], - ]; - } - - /** - * @dataProvider invalidEmailProvider - */ - public function testProcessWithInvalidEmail(string $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function invalidEmailProvider(): array - { - return [ - ['not an email', 'notanemail'], - ['test@', 'test@'], - ['@example.com', '@example.com'], - ['test@example', 'test@example'], - ]; - } - - public function testProcessWithNonStringInput(): void - { - $this->expectException(SanitizationException::class); - $this->expectExceptionMessage('Input must be a string'); - $this->cleaner->process(123); - } -} diff --git a/tests/Processor/Cleaner/NumericValueCleanerTest.php b/tests/Processor/Cleaner/NumericValueCleanerTest.php deleted file mode 100644 index a3e86e6..0000000 --- a/tests/Processor/Cleaner/NumericValueCleanerTest.php +++ /dev/null @@ -1,63 +0,0 @@ -cleaner = new NumericValueCleaner(); - } - - /** - * @dataProvider validNumericProvider - */ - public function testProcessWithValidNumeric(mixed $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function validNumericProvider(): array - { - return [ - [123, '123'], - [123.45, '123.45'], - ['123', '123'], - ['123.45', '123.45'], - [' 123.45 ', '123.45'], - ['-123.45', '-123.45'], - ['+123.45', '+123.45'], - ]; - } - - /** - * @dataProvider invalidNumericProvider - */ - public function testProcessWithInvalidNumeric(mixed $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function invalidNumericProvider(): array - { - return [ - ['not a number', '0'], - ['123abc', '123'], - ['abc123', '123'], - ['', '0'], - ]; - } -} diff --git a/tests/Processor/Cleaner/UrlAddressCleanerTest.php b/tests/Processor/Cleaner/UrlAddressCleanerTest.php deleted file mode 100644 index f79857c..0000000 --- a/tests/Processor/Cleaner/UrlAddressCleanerTest.php +++ /dev/null @@ -1,67 +0,0 @@ -cleaner = new UrlAddressCleaner(); - } - - /** - * @dataProvider validUrlProvider - */ - public function testProcessWithValidUrl(string $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function validUrlProvider(): array - { - return [ - ['https://www.example.com', 'https://www.example.com'], - ['http://example.com/path?query=value', 'http://example.com/path?query=value'], - [' https://www.example.com ', 'https://www.example.com'], - ['www.example.com', 'www.example.com'], - ]; - } - - /** - * @dataProvider invalidUrlProvider - */ - public function testProcessWithInvalidUrl(string $input, string $expected): void - { - $this->assertSame($expected, $this->cleaner->process($input)); - } - - /** - * @return array - */ - public static function invalidUrlProvider(): array - { - return [ - ['http://', 'http://'], - ['https://', 'https://'], - ['ftp:/example.com', 'ftp:/example.com'], - ]; - } - - public function testProcessWithNonStringInput(): void - { - $this->expectException(SanitizationException::class); - $this->expectExceptionMessage('Input must be a string'); - $this->cleaner->process(123); - } -} diff --git a/tests/Processor/Domain/HtmlPurifierSanitizerTest.php b/tests/Processor/Domain/HtmlPurifierSanitizerTest.php new file mode 100644 index 0000000..331bbaa --- /dev/null +++ b/tests/Processor/Domain/HtmlPurifierSanitizerTest.php @@ -0,0 +1,69 @@ +sanitizer = new HtmlPurifierSanitizer(); + } + + public function testProcessRemovesDisallowedTags(): void + { + $input = '

This is a test.

'; + $expected = '

This is a test.

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testProcessRemovesDisallowedAttributes(): void + { + $input = 'Link'; + $expected = 'Link'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testProcessRemovesHtmlComments(): void + { + $input = '

This is a test.

'; + $expected = '

This is a test.

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testConfigureChangesAllowedTags(): void + { + $this->sanitizer->configure(['allowedTags' => ['p', 'strong']]); + $input = '

This is bold and italic.

'; + $expected = '

This is bold and italic.

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testConfigureChangesAllowedAttributes(): void + { + $this->sanitizer->configure(['allowedAttributes' => ['class' => ['p']]]); + $input = '

This is a test.

'; + $expected = '

This is a test.

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testProcessHandlesNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->sanitizer->process(123); + } + + public function testProcessHandlesInvalidHtml(): void + { + $input = '

This is an unclosed paragraph'; + $expected = '

This is an unclosed paragraph

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } +} diff --git a/tests/Processor/Domain/JsonSanitizerTest.php b/tests/Processor/Domain/JsonSanitizerTest.php new file mode 100644 index 0000000..10a178a --- /dev/null +++ b/tests/Processor/Domain/JsonSanitizerTest.php @@ -0,0 +1,26 @@ +assertEquals($expected, $sanitizer->process($input)); + } + + public function testJsonSanitizerWithInvalidJson(): void + { + $this->expectException(\InvalidArgumentException::class); + $sanitizer = new JsonSanitizer(); + $sanitizer->process('{invalid json}'); + } +} diff --git a/tests/Processor/Domain/MarkdownSanitizerTest.php b/tests/Processor/Domain/MarkdownSanitizerTest.php new file mode 100644 index 0000000..98d3fd9 --- /dev/null +++ b/tests/Processor/Domain/MarkdownSanitizerTest.php @@ -0,0 +1,21 @@ +assertEquals( + 'This is \*emphasized\* and this is \*\*bold\*\*', + $sanitizer->process('This is *emphasized* and this is **bold**') + ); + $this->assertEquals('\\# Heading', $sanitizer->process('# Heading')); + } +} diff --git a/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php b/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php deleted file mode 100644 index 09398fb..0000000 --- a/tests/Processor/Encoder/HtmlSpecialCharsEncoderTest.php +++ /dev/null @@ -1,47 +0,0 @@ -encoder = new HtmlSpecialCharsEncoder(); - } - - /** - * @dataProvider htmlStringProvider - */ - public function testProcessWithHtmlString(string $input, string $expected): void - { - $this->assertSame($expected, $this->encoder->process($input)); - } - - /** - * @return array - */ - public static function htmlStringProvider(): array - { - return [ - ['

Test

', '<p>Test</p>'], - ['"quoted" & \'single-quoted\'', '"quoted" & 'single-quoted''], // Alterado para ' - ['Link', '<a href="https://example.com">Link</a>'], - ['Normal text', 'Normal text'], - ]; - } - - public function testProcessWithNonStringInput(): void - { - $this->expectException(SanitizationException::class); - $this->expectExceptionMessage('Input must be a string'); - $this->encoder->process(123); - } -} diff --git a/tests/Processor/Input/HtmlSpecialCharsSanitizerTest.php b/tests/Processor/Input/HtmlSpecialCharsSanitizerTest.php new file mode 100644 index 0000000..61f3702 --- /dev/null +++ b/tests/Processor/Input/HtmlSpecialCharsSanitizerTest.php @@ -0,0 +1,20 @@ +assertEquals( + '<script>alert("xss")</script>', + $sanitizer->process('') + ); + } +} diff --git a/tests/Processor/Input/NormalizeLineBreaksSanitizerTest.php b/tests/Processor/Input/NormalizeLineBreaksSanitizerTest.php new file mode 100644 index 0000000..7bcef33 --- /dev/null +++ b/tests/Processor/Input/NormalizeLineBreaksSanitizerTest.php @@ -0,0 +1,20 @@ +assertEquals( + "line1\nline2\nline3", + $sanitizer->process("line1\r\nline2\rline3") + ); + } +} diff --git a/tests/Processor/Input/StripTagsSanitizerTest.php b/tests/Processor/Input/StripTagsSanitizerTest.php new file mode 100644 index 0000000..2653f0c --- /dev/null +++ b/tests/Processor/Input/StripTagsSanitizerTest.php @@ -0,0 +1,67 @@ +sanitizer = new StripTagsSanitizer(); + } + + public function testStripAllTags(): void + { + $input = '

test

'; + $expected = 'testalert("xss")'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testAllowSpecificTags(): void + { + $this->sanitizer->configure(['allowedTags' => ['p']]); + $input = '

test

'; + $expected = '

test

alert("xss")'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testHandleNestedTags(): void + { + $this->sanitizer->configure(['allowedTags' => ['p', 'strong']]); + $input = '

This is important and emphasized

'; + $expected = '

This is important and emphasized

'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testHandleInvalidHtml(): void + { + $input = '

Unclosed paragraph Bold text

'; + $expected = 'Unclosed paragraph Bold text'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testPreserveTextContent(): void + { + $input = '
Hello, world!
'; + $expected = 'Hello, world!'; + $this->assertEquals($expected, $this->sanitizer->process($input)); + } + + public function testHandleEmptyInput(): void + { + $this->assertEquals('', $this->sanitizer->process('')); + } + + public function testNonStringInput(): void + { + $this->expectException(SanitizationException::class); + $this->sanitizer->process(123); + } +} diff --git a/tests/Processor/Input/TrimSanitizerTest.php b/tests/Processor/Input/TrimSanitizerTest.php new file mode 100644 index 0000000..796e93e --- /dev/null +++ b/tests/Processor/Input/TrimSanitizerTest.php @@ -0,0 +1,29 @@ +assertEquals('test', $sanitizer->process(' test ')); + $this->assertEquals('test', $sanitizer->process("\ntest\n")); + + $sanitizer->configure(['characterMask' => 'a']); + $this->assertEquals('test', $sanitizer->process('aaatestaa')); + } + + public function testTrimSanitizerWithNonString(): void + { + $this->expectException(SanitizationException::class); + $sanitizer = new TrimSanitizer(); + $sanitizer->process(123); + } +} diff --git a/tests/Processor/Remover/HtmlTagRemoverTest.php b/tests/Processor/Remover/HtmlTagRemoverTest.php deleted file mode 100644 index 02a6417..0000000 --- a/tests/Processor/Remover/HtmlTagRemoverTest.php +++ /dev/null @@ -1,56 +0,0 @@ -remover = new HtmlTagRemover(); - } - - /** - * @dataProvider htmlStringProvider - */ - public function testProcessWithHtmlString(string $input, string $expected): void - { - $this->assertSame($expected, $this->remover->process($input)); - } - - /** - * @return array - */ - public static function htmlStringProvider(): array - { - return [ - ['

Test

', 'Test'], - ['Link', 'Link'], - ['', 'alert("XSS");'], - ['Normal text', 'Normal text'], - ]; - } - - public function testProcessWithAllowedTags(): void - { - $this->remover->configure(['allowedTags' => ['p', 'a']]); - $input = '

Test

Link'; - $expected = '

Test

Linkalert("XSS");'; - - $this->assertSame($expected, $this->remover->process($input)); - } - - public function testProcessWithNonStringInput(): void - { - $this->expectException(SanitizationException::class); - $this->expectExceptionMessage('Input must be a string'); - $this->remover->process(123); - } -} diff --git a/tests/Processor/Remover/WhitespaceRemoverTest.php b/tests/Processor/Remover/WhitespaceRemoverTest.php deleted file mode 100644 index b437b4f..0000000 --- a/tests/Processor/Remover/WhitespaceRemoverTest.php +++ /dev/null @@ -1,53 +0,0 @@ -remover = new WhitespaceRemover(); - } - - /** - * @dataProvider whitespaceStringProvider - */ - public function testProcessWithWhitespaceString(string $input, string $expected): void - { - $this->assertSame($expected, $this->remover->process($input)); - } - - /** - * @return array - */ - public static function whitespaceStringProvider(): array - { - return [ - [' Test ', 'Test'], - ["\t\tTest\t\t", 'Test'], - ["\nTest\n", 'Test'], - [" \t\n\r\0\x0BTest \t\n\r\0\x0B", 'Test'], - ]; - } - - public function testProcessWithCustomCharlist(): void - { - $this->remover->configure(['charlist' => 'a']); - $this->assertSame('Test', $this->remover->process('aaaTestaaa')); - } - - public function testProcessWithNonStringInput(): void - { - $this->expectException(SanitizationException::class); - $this->expectExceptionMessage('Input must be a string'); - $this->remover->process(123); - } -} diff --git a/tests/Processor/Security/FilenameSanitizerTest.php b/tests/Processor/Security/FilenameSanitizerTest.php new file mode 100644 index 0000000..945c8e0 --- /dev/null +++ b/tests/Processor/Security/FilenameSanitizerTest.php @@ -0,0 +1,108 @@ +sanitizer = new FilenameSanitizer(); + } + + public function testBasicFilenameSanitization(): void + { + $input = 'file@name!.txt'; + $expected = 'file_name.txt'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testSanitizationWithoutExtension(): void + { + $this->sanitizer->configure(['preserveExtension' => false]); + $input = 'file@name!.txt'; + $expected = 'file_name'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testCustomReplacementCharacter(): void + { + $this->sanitizer->configure(['replacement' => '-']); + $input = 'file@name!.txt'; + $expected = 'file-name.txt'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testInvalidReplacementCharacter(): void + { + $this->sanitizer->configure(['replacement' => '*']); + $input = 'file@name!.txt'; + $expected = 'file_name.txt'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testAllowedCharacters(): void + { + $this->sanitizer->configure(['allowedChars' => ['a-z', 'A-Z', '0-9']]); + $input = 'file_name-123.txt'; + $expected = 'file_name_123.txt'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testCustomAllowedCharacters(): void + { + $this->sanitizer->configure(['allowedChars' => ['a-z', 'A-Z', '0-9', '_']]); + $input = 'file@name!.txt'; + $expected = 'file_name.txt'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testEmptyFilenameReturnsEmptyString(): void + { + $input = ''; + $expected = ''; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testFilenameWithoutExtension(): void + { + $input = 'filename@!with_no_extension'; + $expected = 'filename_with_no_extension'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testFilenameWithMultipleExtensions(): void + { + $input = 'file.name@!.tar.gz'; + $expected = 'file.name_.tar.gz'; + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testNonStringInputThrowsException(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->sanitizer->process(12345); + } + + public function testObjectInputThrowsException(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->sanitizer->process(new \stdClass()); + } + + public function testArrayInputThrowsException(): void + { + $this->expectException(SanitizationException::class); + $this->expectExceptionMessage('Input must be a string'); + $this->sanitizer->process(['invalid', 'input']); + } +} diff --git a/tests/Processor/Security/SqlInjectionSanitizerTest.php b/tests/Processor/Security/SqlInjectionSanitizerTest.php new file mode 100644 index 0000000..597523a --- /dev/null +++ b/tests/Processor/Security/SqlInjectionSanitizerTest.php @@ -0,0 +1,103 @@ +sanitizer = new SqlInjectionSanitizer(); + } + + public function testProcessWithSafeInput(): void + { + $input = 'Safe input without SQL keywords'; + $expected = 'Safe input without SQL keywords'; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testProcessWithSqlInjectionAttempt(): void + { + $input = "SELECT * FROM users WHERE name='admin'--"; + $expected = "SELECT * FROM users WHERE name=\\'admin\\'"; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testProcessWithCustomEscapeMap(): void + { + $customSanitizer = new SqlInjectionSanitizer(); + $customSanitizer->configure([ + 'escapeMap' => ["'" => "''", '\\' => '\\\\'], + ]); + + $input = "O'Reilly"; + $expected = "O''Reilly"; + + $this->assertSame($expected, $customSanitizer->process($input)); + } + + public function testProcessWithEscapedCharacters(): void + { + $input = "This contains a null byte: \x00 and a quote: '"; + $expected = "This contains a null byte: \\0 and a quote: \\'"; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testProcessWithMultiLineComment(): void + { + $input = 'SELECT * FROM users /* hidden comment */ WHERE id = 1;'; + $expected = 'SELECT * FROM users WHERE id = 1'; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testProcessWithSemicolons(): void + { + $input = 'DROP TABLE users;'; + $expected = 'DROP TABLE users'; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testProcessWithNormalizedWhitespace(): void + { + $input = 'SELECT * FROM users'; + $expected = 'SELECT * FROM users'; + + $this->assertSame($expected, $this->sanitizer->process($input)); + } + + public function testRemoveSuspiciousPatterns(): void + { + $reflection = new \ReflectionClass(SqlInjectionSanitizer::class); + $method = $reflection->getMethod('removeSuspiciousPatterns'); + $method->setAccessible(true); + + $input = 'SELECT * FROM users WHERE id = 1; -- malicious comment'; + $expected = 'SELECT * FROM users WHERE id = 1 '; + + $this->assertSame($expected, $method->invoke($this->sanitizer, $input)); + } + + public function testEscapeString(): void + { + $reflection = new \ReflectionClass(SqlInjectionSanitizer::class); + $method = $reflection->getMethod('escapeString'); + $method->setAccessible(true); + + $input = "This contains a null byte: \x00 and a quote: '"; + $expected = "This contains a null byte: \\0 and a quote: \\'"; + + $this->assertSame($expected, $method->invoke($this->sanitizer, $input)); + } +} diff --git a/tests/Processor/Security/XssSanitizerTest.php b/tests/Processor/Security/XssSanitizerTest.php new file mode 100644 index 0000000..13ef0e3 --- /dev/null +++ b/tests/Processor/Security/XssSanitizerTest.php @@ -0,0 +1,20 @@ +assertEquals( + '<script>alert("xss")</script>', + $sanitizer->process('') + ); + } +} diff --git a/tests/application.php b/tests/application.php index 1045a38..67614e4 100644 --- a/tests/application.php +++ b/tests/application.php @@ -82,7 +82,7 @@ public function setBio(string $bio): void // Create a UserInput object with potentially unsafe data $userInput = new UserInput(); $userInput->setName(" John Doe "); -$userInput->setEmail(' john.doe@example..com '); +$userInput->setEmail(' john.doe@example#.com '); $userInput->setAge(' 25 years old '); $userInput->setBio("

Hello, I'm John!

"); From c10474acf92357df8cd3739d708ef3004c7d8078 Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 15 Oct 2024 17:18:39 -0300 Subject: [PATCH 3/4] feat(sanitizer): implement comprehensive sanitizer test suite - Add UserProfile, UserPreferences, UserAvatar, and UserSearch classes - Implement various sanitizer processors for different data types - Create displayValues function for showing original and sanitized data - Set up ProcessorRegistry with all available sanitizers - Demonstrate sanitization on potentially unsafe input data --- tests/application.php | 166 ++++++++++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 39 deletions(-) diff --git a/tests/application.php b/tests/application.php index 67614e4..2232d2b 100644 --- a/tests/application.php +++ b/tests/application.php @@ -6,28 +6,32 @@ use KaririCode\ProcessorPipeline\ProcessorRegistry; use KaririCode\Sanitizer\Attribute\Sanitize; -use KaririCode\Sanitizer\Processor\Cleaner\EmailAddressCleaner; -use KaririCode\Sanitizer\Processor\Cleaner\NumericValueCleaner; -use KaririCode\Sanitizer\Processor\HtmlPurifier; -use KaririCode\Sanitizer\Processor\Remover\WhitespaceRemover; -use KaririCode\Sanitizer\Processor\XssSanitizer; +use KaririCode\Sanitizer\Processor\Domain\HtmlPurifierSanitizer; +use KaririCode\Sanitizer\Processor\Domain\JsonSanitizer; +use KaririCode\Sanitizer\Processor\Domain\MarkdownSanitizer; +use KaririCode\Sanitizer\Processor\Input\HtmlSpecialCharsSanitizer; +use KaririCode\Sanitizer\Processor\Input\NormalizeLineBreaksSanitizer; +use KaririCode\Sanitizer\Processor\Input\StripTagsSanitizer; +use KaririCode\Sanitizer\Processor\Input\TrimSanitizer; +use KaririCode\Sanitizer\Processor\Security\FilenameSanitizer; +use KaririCode\Sanitizer\Processor\Security\SqlInjectionSanitizer; +use KaririCode\Sanitizer\Processor\Security\XssSanitizer; use KaririCode\Sanitizer\Sanitizer; -class UserInput +class UserProfile { - #[Sanitize(sanitizers: ['trim', 'html_purifier', 'xss_sanitizer'])] + #[Sanitize(sanitizers: ['trim', 'html_purifier', 'xss_sanitizer', 'html_special_chars'])] private string $name = ''; - #[Sanitize(sanitizers: ['trim', 'email_cleaner'])] + #[Sanitize(sanitizers: ['trim', 'normalize_line_breaks'])] private string $email = ''; - #[Sanitize(sanitizers: ['trim', 'numeric_value_cleaner'])] + #[Sanitize(sanitizers: ['trim', 'strip_tags'])] private string $age = ''; - #[Sanitize(sanitizers: ['trim', 'html_purifier'], fallbackValue: 'No bio provided')] + #[Sanitize(sanitizers: ['trim', 'html_purifier', 'markdown'], fallbackValue: 'No bio provided')] private string $bio = ''; - // Getters and setters public function getName(): string { return $this->name; @@ -69,36 +73,120 @@ public function setBio(string $bio): void } } -// Set up the ProcessorRegistry +class UserPreferences +{ + #[Sanitize(sanitizers: ['json'])] + private string $preferences = ''; + + public function getPreferences(): string + { + return $this->preferences; + } + + public function setPreferences(string $preferences): void + { + $this->preferences = $preferences; + } +} + +class UserAvatar +{ + #[Sanitize(sanitizers: ['filename'])] + private string $avatarFilename = ''; + + public function getAvatarFilename(): string + { + return $this->avatarFilename; + } + + public function setAvatarFilename(string $avatarFilename): void + { + $this->avatarFilename = $avatarFilename; + } +} + +class UserSearch +{ + #[Sanitize(sanitizers: ['sql_injection'])] + private string $searchQuery = ''; + + public function getSearchQuery(): string + { + return $this->searchQuery; + } + + public function setSearchQuery(string $searchQuery): void + { + $this->searchQuery = $searchQuery; + } +} + $registry = new ProcessorRegistry(); -$registry->register('sanitizer', 'trim', new WhitespaceRemover()); -$registry->register('sanitizer', 'html_purifier', new HtmlPurifier()); -$registry->register('sanitizer', 'email_cleaner', new EmailAddressCleaner()); -$registry->register('sanitizer', 'numeric_value_cleaner', new NumericValueCleaner()); +$registry->register('sanitizer', 'trim', new TrimSanitizer()); +$registry->register('sanitizer', 'html_special_chars', new HtmlSpecialCharsSanitizer()); +$registry->register('sanitizer', 'normalize_line_breaks', new NormalizeLineBreaksSanitizer()); +$registry->register('sanitizer', 'strip_tags', new StripTagsSanitizer()); +$registry->register('sanitizer', 'html_purifier', new HtmlPurifierSanitizer()); +$registry->register('sanitizer', 'json', new JsonSanitizer()); +$registry->register('sanitizer', 'markdown', new MarkdownSanitizer()); +$registry->register('sanitizer', 'filename', new FilenameSanitizer()); +$registry->register('sanitizer', 'sql_injection', new SqlInjectionSanitizer()); $registry->register('sanitizer', 'xss_sanitizer', new XssSanitizer()); $autoSanitizer = new Sanitizer($registry); -// Create a UserInput object with potentially unsafe data -$userInput = new UserInput(); -$userInput->setName(" John Doe "); -$userInput->setEmail(' john.doe@example#.com '); -$userInput->setAge(' 25 years old '); -$userInput->setBio("

Hello, I'm John!

"); - -// Display original values -echo "Original values:\n"; -echo 'Name: ' . $userInput->getName() . "\n"; -echo 'Email: ' . $userInput->getEmail() . "\n"; -echo 'Age: ' . $userInput->getAge() . "\n"; -echo 'Bio: ' . $userInput->getBio() . "\n\n"; - -// Sanitize the user input -$autoSanitizer->sanitize($userInput); - -// Display sanitized values -echo "Sanitized values:\n"; -echo 'Name: ' . $userInput->getName() . "\n"; -echo 'Email: ' . $userInput->getEmail() . "\n"; -echo 'Age: ' . $userInput->getAge() . "\n"; -echo 'Bio: ' . $userInput->getBio() . "\n"; +// Create input objects with potentially unsafe data +$userProfile = new UserProfile(); +$userProfile->setName(" Walmir Silva "); +$userProfile->setEmail(" walmir.silva@example.com \r\n"); +$userProfile->setAge(' 35 '); +$userProfile->setBio("# Hello\n\n

I'm Walmir!

"); + +$userPreferences = new UserPreferences(); +$userPreferences->setPreferences('{"theme": "dark", "notifications": true}'); + +$userAvatar = new UserAvatar(); +$userAvatar->setAvatarFilename('my avatar!.jpg'); + +$userSearch = new UserSearch(); +$userSearch->setSearchQuery("users'; DROP TABLE users; --"); + +// Function to display original and sanitized values + +function displayValues($object, $sanitizer) +{ + echo "Original values:\n"; + $reflection = new ReflectionClass($object); + foreach ($reflection->getProperties() as $property) { + $propertyName = $property->getName(); + $getter = 'get' . ucfirst($propertyName); + if (method_exists($object, $getter)) { + echo ucfirst($propertyName) . ': "' . str_replace("\n", '\n', $object->$getter()) . "\"\n"; + } + } + + $sanitizer->sanitize($object); + + echo "\nSanitized values:\n"; + foreach ($reflection->getProperties() as $property) { + $propertyName = $property->getName(); + $getter = 'get' . ucfirst($propertyName); + if (method_exists($object, $getter)) { + echo ucfirst($propertyName) . ': "' . str_replace("\n", '\n', $object->$getter()) . "\"\n"; + } + } + echo "\n"; +} + +// Display and sanitize values for each object +echo "User Profile:\n"; +displayValues($userProfile, $autoSanitizer); + +echo "User Preferences:\n"; +displayValues($userPreferences, $autoSanitizer); + +echo "User Avatar:\n"; +displayValues($userAvatar, $autoSanitizer); + +echo "User Search:\n"; +displayValues($userSearch, $autoSanitizer); From 79ebd69cab9739bb1dee212cb93f9f6bbc12a67b Mon Sep 17 00:00:00 2001 From: Walmir Silva Date: Tue, 15 Oct 2024 17:29:40 -0300 Subject: [PATCH 4/4] docs(sanitizer): update README with comprehensive content and translations - Create detailed English README with complete component overview - Add Portuguese (Brazil) translation of the README - Include examples using "Walmir Silva" for consistency - Highlight integration with other KaririCode components - Add sections on available sanitizers, development setup, and community support - Ensure both versions (EN and PT-BR) are consistent and up-to-date --- README.md | 234 +++++++++++++++++++++++++++++++++++++++++++++-- README.pt-br.md | 238 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 456 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 5feba70..08487af 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,241 @@ ![PHP](https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![PHPUnit](https://img.shields.io/badge/PHPUnit-3776AB?style=for-the-badge&logo=php&logoColor=white) +A robust and flexible data sanitization component for PHP, part of the KaririCode Framework. It utilizes configurable processors and native functions to ensure data integrity and security in your applications. + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Usage](#basic-usage) + - [Advanced Usage](#advanced-usage) +- [Available Sanitizers](#available-sanitizers) +- [Integration with Other KaririCode Components](#integration-with-other-kariricode-components) +- [Development and Testing](#development-and-testing) +- [License](#license) +- [Support and Community](#support-and-community) + +## Features + +- Flexible attribute-based sanitization for object properties +- Comprehensive set of built-in sanitizers for common use cases +- Easy integration with other KaririCode components +- Configurable processors for customized sanitization logic +- Support for fallback values in case of sanitization failures +- Extensible architecture allowing custom sanitizers + +## Installation + +You can install the Sanitizer component via Composer: + +```bash +composer require kariricode/sanitizer +``` + +### Requirements + +- PHP 8.3 or higher +- Composer + +## Usage + +### Basic Usage + +1. Define your data class with sanitization attributes: + +```php +use KaririCode\Sanitizer\Attribute\Sanitize; + +class UserProfile +{ + #[Sanitize(sanitizers: ['trim', 'html_special_chars'])] + private string $name = ''; + + #[Sanitize(sanitizers: ['trim', 'normalize_line_breaks'])] + private string $email = ''; + + // Getters and setters... +} +``` + +2. Set up the sanitizer and use it: + +```php +use KaririCode\ProcessorPipeline\ProcessorRegistry; +use KaririCode\Sanitizer\Sanitizer; +use KaririCode\Sanitizer\Processor\Input\TrimSanitizer; +use KaririCode\Sanitizer\Processor\Input\HtmlSpecialCharsSanitizer; +use KaririCode\Sanitizer\Processor\Input\NormalizeLineBreaksSanitizer; + +$registry = new ProcessorRegistry(); +$registry->register('sanitizer', 'trim', new TrimSanitizer()); +$registry->register('sanitizer', 'html_special_chars', new HtmlSpecialCharsSanitizer()); +$registry->register('sanitizer', 'normalize_line_breaks', new NormalizeLineBreaksSanitizer()); + +$sanitizer = new Sanitizer($registry); + +$userProfile = new UserProfile(); +$userProfile->setName(" Walmir Silva "); +$userProfile->setEmail("walmir.silva@example.com\r\n"); + +$sanitizer->sanitize($userProfile); + +echo $userProfile->getName(); // Output: "Walmir Silva" +echo $userProfile->getEmail(); // Output: "walmir.silva@example.com\n" +``` + +### Advanced Usage + +You can create custom sanitizers by implementing the `Processor` or `ConfigurableProcessor` interfaces: + +```php +use KaririCode\Contract\Processor\ConfigurableProcessor; +use KaririCode\Sanitizer\Processor\AbstractSanitizerProcessor; + +class CustomSanitizer extends AbstractSanitizerProcessor implements ConfigurableProcessor +{ + private $option; + + public function configure(array $options): void + { + $this->option = $options['custom_option'] ?? 'default'; + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + // Custom sanitization logic here + return $input; + } +} + +// Register and use the custom sanitizer +$registry->register('sanitizer', 'custom', new CustomSanitizer()); + +class AdvancedProfile +{ + #[Sanitize(sanitizers: ['custom' => ['custom_option' => 'value']])] + private string $customField = ''; +} +``` + +## Available Sanitizers + +The Sanitizer component provides various built-in sanitizers: + +### Input Sanitizers + +- TrimSanitizer +- HtmlSpecialCharsSanitizer +- NormalizeLineBreaksSanitizer +- StripTagsSanitizer + +### Domain Sanitizers + +- HtmlPurifierSanitizer +- JsonSanitizer +- MarkdownSanitizer + +### Security Sanitizers + +- FilenameSanitizer +- SqlInjectionSanitizer +- XssSanitizer + +Each sanitizer is designed to handle specific types of data and security concerns. For detailed information on each sanitizer, please refer to the [documentation](https://kariricode.org/docs/sanitizer). + +## Integration with Other KaririCode Components + +The Sanitizer component is designed to work seamlessly with other KaririCode components: + +- **KaririCode\Contract**: Provides interfaces and contracts for consistent component integration. +- **KaririCode\ProcessorPipeline**: Utilized for building and executing sanitization pipelines. +- **KaririCode\PropertyInspector**: Used for analyzing and processing object properties with sanitization attributes. + +Example of integration: + +```php +use KaririCode\ProcessorPipeline\ProcessorRegistry; +use KaririCode\ProcessorPipeline\ProcessorBuilder; +use KaririCode\PropertyInspector\AttributeAnalyzer; +use KaririCode\PropertyInspector\AttributeHandler; +use KaririCode\PropertyInspector\Utility\PropertyInspector; +use KaririCode\Sanitizer\Sanitizer; + +$registry = new ProcessorRegistry(); +// Register sanitizers... + +$builder = new ProcessorBuilder($registry); +$attributeHandler = new AttributeHandler('sanitizer', $builder); +$propertyInspector = new PropertyInspector(new AttributeAnalyzer(Sanitize::class)); + +$sanitizer = new Sanitizer($registry); +``` + +## Development and Testing + +For development and testing purposes, this package uses Docker and Docker Compose to ensure consistency across different environments. A Makefile is provided for convenience. + +### Prerequisites + +- Docker +- Docker Compose +- Make (optional, but recommended for easier command execution) + +### Development Setup + +1. Clone the repository: + + ```bash + git clone https://github.com/KaririCode-Framework/kariricode-sanitizer.git + cd kariricode-sanitizer + ``` + +2. Set up the environment: + + ```bash + make setup-env + ``` + +3. Start the Docker containers: + + ```bash + make up + ``` + +4. Install dependencies: + ```bash + make composer-install + ``` + +### Available Make Commands + +- `make up`: Start all services in the background +- `make down`: Stop and remove all containers +- `make build`: Build Docker images +- `make shell`: Access the PHP container shell +- `make test`: Run tests +- `make coverage`: Run test coverage with visual formatting +- `make cs-fix`: Run PHP CS Fixer to fix code style +- `make quality`: Run all quality commands (cs-check, test, security-check) + +For a full list of available commands, run: + +```bash +make help +``` + ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Support and Community -- **Documentation**: [https://kariricode.org/docs/dotenv](https://kariricode.org/docs/dotenv) +- **Documentation**: [https://kariricode.org/docs/sanitizer](https://kariricode.org/docs/sanitizer) - **Issue Tracker**: [GitHub Issues](https://github.com/KaririCode-Framework/kariricode-sanitizer/issues) - **Community**: [KaririCode Club Community](https://kariricode.club) -## Acknowledgments - -- The KaririCode Framework team and contributors. -- Inspired by other popular PHP Dotenv libraries. - --- -Built with ❤️ by the KaririCode team. Empowering developers to build more robust and flexible PHP applications. +Built with ❤️ by the KaririCode team. Empowering developers to create more secure and robust PHP applications. diff --git a/README.pt-br.md b/README.pt-br.md index 574e128..e176f99 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -1,24 +1,244 @@ -# KaririCode Framework: Componente Dotenv +# Framework KaririCode: Componente Sanitizer [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) ![PHP](https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![PHPUnit](https://img.shields.io/badge/PHPUnit-3776AB?style=for-the-badge&logo=php&logoColor=white) +Um componente robusto e flexível de sanitização de dados para PHP, parte do Framework KaririCode. Utiliza processadores configuráveis e funções nativas para garantir a integridade e segurança dos dados em suas aplicações. + +## Índice + +- [Características](#características) +- [Instalação](#instalação) +- [Uso](#uso) + - [Uso Básico](#uso-básico) + - [Uso Avançado](#uso-avançado) +- [Sanitizadores Disponíveis](#sanitizadores-disponíveis) +- [Integração com Outros Componentes KaririCode](#integração-com-outros-componentes-kariricode) +- [Desenvolvimento e Testes](#desenvolvimento-e-testes) +- [Licença](#licença) +- [Suporte e Comunidade](#suporte-e-comunidade) + +## Características + +- Sanitização flexível baseada em atributos para propriedades de objetos +- Conjunto abrangente de sanitizadores integrados para casos de uso comuns +- Fácil integração com outros componentes KaririCode +- Processadores configuráveis para lógica de sanitização personalizada +- Suporte para valores de fallback em caso de falhas na sanitização +- Arquitetura extensível permitindo sanitizadores personalizados + +## Instalação + +Você pode instalar o componente Sanitizer via Composer: + +```bash +composer require kariricode/sanitizer +``` + +### Requisitos + +- PHP 8.3 ou superior +- Composer + +## Uso + +### Uso Básico + +1. Defina sua classe de dados com atributos de sanitização: + +```php +use KaririCode\Sanitizer\Attribute\Sanitize; + +class PerfilUsuario +{ + #[Sanitize(sanitizers: ['trim', 'html_special_chars'])] + private string $nome = ''; + + #[Sanitize(sanitizers: ['trim', 'normalize_line_breaks'])] + private string $email = ''; + + // Getters e setters... +} +``` + +2. Configure o sanitizador e use-o: + +```php +use KaririCode\ProcessorPipeline\ProcessorRegistry; +use KaririCode\Sanitizer\Sanitizer; +use KaririCode\Sanitizer\Processor\Input\TrimSanitizer; +use KaririCode\Sanitizer\Processor\Input\HtmlSpecialCharsSanitizer; +use KaririCode\Sanitizer\Processor\Input\NormalizeLineBreaksSanitizer; + +$registry = new ProcessorRegistry(); +$registry->register('sanitizer', 'trim', new TrimSanitizer()); +$registry->register('sanitizer', 'html_special_chars', new HtmlSpecialCharsSanitizer()); +$registry->register('sanitizer', 'normalize_line_breaks', new NormalizeLineBreaksSanitizer()); + +$sanitizer = new Sanitizer($registry); + +$perfilUsuario = new PerfilUsuario(); +$perfilUsuario->setNome(" Walmir Silva "); +$perfilUsuario->setEmail("walmir.silva@exemplo.com\r\n"); + +$sanitizer->sanitize($perfilUsuario); + +echo $perfilUsuario->getNome(); // Saída: "Walmir Silva" +echo $perfilUsuario->getEmail(); // Saída: "walmir.silva@exemplo.com\n" +``` + +### Uso Avançado + +Você pode criar sanitizadores personalizados implementando as interfaces `Processor` ou `ConfigurableProcessor`: + +```php +use KaririCode\Contract\Processor\ConfigurableProcessor; +use KaririCode\Sanitizer\Processor\AbstractSanitizerProcessor; + +class SanitizadorPersonalizado extends AbstractSanitizerProcessor implements ConfigurableProcessor +{ + private $opcao; + + public function configure(array $options): void + { + $this->opcao = $options['opcao_personalizada'] ?? 'padrao'; + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + // Lógica de sanitização personalizada aqui + return $input; + } +} + +// Registre e use o sanitizador personalizado +$registry->register('sanitizer', 'personalizado', new SanitizadorPersonalizado()); + +class PerfilAvancado +{ + #[Sanitize(sanitizers: ['personalizado' => ['opcao_personalizada' => 'valor']])] + private string $campoPersonalizado = ''; +} +``` + +## Sanitizadores Disponíveis + +O componente Sanitizer fornece vários sanitizadores integrados: + +### Sanitizadores de Entrada + +- TrimSanitizer +- HtmlSpecialCharsSanitizer +- NormalizeLineBreaksSanitizer +- StripTagsSanitizer + +### Sanitizadores de Domínio + +- HtmlPurifierSanitizer +- JsonSanitizer +- MarkdownSanitizer + +### Sanitizadores de Segurança + +- FilenameSanitizer +- SqlInjectionSanitizer +- XssSanitizer + +Cada sanitizador é projetado para lidar com tipos específicos de dados e preocupações de segurança. Para informações detalhadas sobre cada sanitizador, consulte a [documentação](https://kariricode.org/docs/sanitizer). + +## Integração com Outros Componentes KaririCode + +O componente Sanitizer foi projetado para trabalhar perfeitamente com outros componentes KaririCode: + +- **KaririCode\Contract**: Fornece interfaces e contratos para integração consistente de componentes. +- **KaririCode\ProcessorPipeline**: Utilizado para construir e executar pipelines de sanitização. +- **KaririCode\PropertyInspector**: Usado para analisar e processar propriedades de objetos com atributos de sanitização. + +Exemplo de integração: + +```php +use KaririCode\ProcessorPipeline\ProcessorRegistry; +use KaririCode\ProcessorPipeline\ProcessorBuilder; +use KaririCode\PropertyInspector\AttributeAnalyzer; +use KaririCode\PropertyInspector\AttributeHandler; +use KaririCode\PropertyInspector\Utility\PropertyInspector; +use KaririCode\Sanitizer\Sanitizer; + +$registry = new ProcessorRegistry(); +// Registre os sanitizadores... + +$builder = new ProcessorBuilder($registry); +$attributeHandler = new AttributeHandler('sanitizer', $builder); +$propertyInspector = new PropertyInspector(new AttributeAnalyzer(Sanitize::class)); + +$sanitizer = new Sanitizer($registry); +``` + +## Desenvolvimento e Testes + +Para fins de desenvolvimento e teste, este pacote usa Docker e Docker Compose para garantir consistência em diferentes ambientes. Um Makefile é fornecido para conveniência. + +### Pré-requisitos + +- Docker +- Docker Compose +- Make (opcional, mas recomendado para execução mais fácil de comandos) + +### Configuração de Desenvolvimento + +1. Clone o repositório: + + ```bash + git clone https://github.com/KaririCode-Framework/kariricode-sanitizer.git + cd kariricode-sanitizer + ``` + +2. Configure o ambiente: + + ```bash + make setup-env + ``` + +3. Inicie os contêineres Docker: + + ```bash + make up + ``` + +4. Instale as dependências: + ```bash + make composer-install + ``` + +### Comandos Make Disponíveis + +- `make up`: Inicia todos os serviços em segundo plano +- `make down`: Para e remove todos os contêineres +- `make build`: Constrói imagens Docker +- `make shell`: Acessa o shell do contêiner PHP +- `make test`: Executa testes +- `make coverage`: Executa cobertura de testes com formatação visual +- `make cs-fix`: Executa PHP CS Fixer para corrigir o estilo do código +- `make quality`: Executa todos os comandos de qualidade (cs-check, test, security-check) + +Para uma lista completa de comandos disponíveis, execute: + +```bash +make help +``` + ## Licença -Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para mais detalhes. +Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes. ## Suporte e Comunidade -- **Documentação**: [https://kariricode.org/docs/dotenv](https://kariricode.org/docs/dotenv) +- **Documentação**: [https://kariricode.org/docs/sanitizer](https://kariricode.org/docs/sanitizer) - **Rastreador de Problemas**: [GitHub Issues](https://github.com/KaririCode-Framework/kariricode-sanitizer/issues) - **Comunidade**: [Comunidade KaririCode Club](https://kariricode.club) -## Agradecimentos - -- A equipe do KaririCode Framework e contribuidores. -- Inspirado por outras bibliotecas populares de Dotenv para PHP. - --- -Construído com ❤️ pela equipe KaririCode. Capacitando desenvolvedores a criar aplicações PHP mais robustas e flexíveis. +Construído com ❤️ pela equipe KaririCode. Capacitando desenvolvedores para criar aplicações PHP mais seguras e robustas.