diff --git a/.gitattributes b/.gitattributes index 31f41b6..112620c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,4 +14,5 @@ /phpstan.neon export-ignore /phpunit.xml export-ignore /psalm.xml export-ignore -/Makefile export-ignore \ No newline at end of file +/Makefile export-ignore +/composer.lock \ No newline at end of file diff --git a/composer.lock b/composer.lock index be607d2..07b0549 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "kariricode/contract", - "version": "v2.7.11", + "version": "v2.8.0", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-contract.git", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b" + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/72c834a3afe2dbded8f6a7f96005635424636d4b", - "reference": "72c834a3afe2dbded8f6a7f96005635424636d4b", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-contract/zipball/ee489bbcb44339a246af01058e00b3f94891f66c", + "reference": "ee489bbcb44339a246af01058e00b3f94891f66c", "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-24T18:51:39+00:00" + "time": "2024-10-25T17:45:25+00:00" }, { "name": "kariricode/data-structure", @@ -142,23 +142,82 @@ }, "time": "2024-10-10T22:37:23+00:00" }, + { + "name": "kariricode/exception", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/KaririCode-Framework/kariricode-exception.git", + "reference": "581be984f0f4219266e494dd95accbea0cb63fdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-exception/zipball/581be984f0f4219266e494dd95accbea0cb63fdd", + "reference": "581be984f0f4219266e494dd95accbea0cb63fdd", + "shasum": "" + }, + "require": { + "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\\Exception\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walmir Silva", + "email": "community@kariricode.org" + } + ], + "description": "KaririCode Exception provides a robust and modular exception handling system for the KaririCode Framework, enabling seamless error management across various application domains.", + "homepage": "https://kariricode.org", + "keywords": [ + "error-management", + "exception-handling", + "framework", + "kariri-code", + "modular-exceptions", + "php-exceptions", + "php-framework" + ], + "support": { + "issues": "https://github.com/KaririCode-Framework/kariricode-exception/issues", + "source": "https://github.com/KaririCode-Framework/kariricode-exception" + }, + "time": "2024-10-26T12:56:55+00:00" + }, { "name": "kariricode/processor-pipeline", - "version": "v1.1.6", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline.git", - "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf" + "reference": "20aeaab04557d61b5c35c1be3e478dd3133d864b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/58a25f345d066c7d7b69331bdbe1d468513964bf", - "reference": "58a25f345d066c7d7b69331bdbe1d468513964bf", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-processor-pipeline/zipball/20aeaab04557d61b5c35c1be3e478dd3133d864b", + "reference": "20aeaab04557d61b5c35c1be3e478dd3133d864b", "shasum": "" }, "require": { "kariricode/contract": "^2.7", "kariricode/data-structure": "^1.1", + "kariricode/exception": "^1.2", + "kariricode/property-inspector": "^1.2", "php": "^8.3" }, "require-dev": { @@ -199,25 +258,24 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline/issues", "source": "https://github.com/KaririCode-Framework/kariricode-processor-pipeline" }, - "time": "2024-10-24T18:55:45+00:00" + "time": "2024-10-26T14:05:49+00:00" }, { "name": "kariricode/property-inspector", - "version": "v1.1.6", + "version": "v1.2.3", "source": { "type": "git", "url": "https://github.com/KaririCode-Framework/kariricode-property-inspector.git", - "reference": "7e70c17b74c69601514fa40a4f76aeb0c056e096" + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/7e70c17b74c69601514fa40a4f76aeb0c056e096", - "reference": "7e70c17b74c69601514fa40a4f76aeb0c056e096", + "url": "https://api.github.com/repos/KaririCode-Framework/kariricode-property-inspector/zipball/5faa6ca584ee80fbfc8de456377020703a88ab80", + "reference": "5faa6ca584ee80fbfc8de456377020703a88ab80", "shasum": "" }, "require": { "kariricode/contract": "^2.7", - "kariricode/processor-pipeline": "^1.1", "php": "^8.3" }, "require-dev": { @@ -263,7 +321,7 @@ "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues", "source": "https://github.com/KaririCode-Framework/kariricode-property-inspector" }, - "time": "2024-10-21T20:42:58+00:00" + "time": "2024-10-25T19:50:19+00:00" } ], "packages-dev": [ diff --git a/src/Contract/SanitizationResult.php b/src/Contract/SanitizationResult.php index 7d0b3a7..c3929a7 100644 --- a/src/Contract/SanitizationResult.php +++ b/src/Contract/SanitizationResult.php @@ -6,11 +6,7 @@ interface SanitizationResult { - public function addError(string $property, string $errorKey, string $message): void; - - public function setSanitizedData(string $property, mixed $value): void; - - public function hasErrors(): bool; + public function isValid(): bool; public function getErrors(): array; diff --git a/src/Contract/SanitizationResultProcessor.php b/src/Contract/SanitizationResultProcessor.php deleted file mode 100644 index 69c521a..0000000 --- a/src/Contract/SanitizationResultProcessor.php +++ /dev/null @@ -1,19 +0,0 @@ -getProcessedPropertyValues(); - $errors = $handler->getProcessingResultErrors(); - - foreach ($processedValues as $property => $data) { - $this->result->setSanitizedData($property, $data['value']); - - if (isset($errors[$property])) { - $this->addPropertyErrors($this->result, $property, $errors[$property]); - } - } - - return $this->result; - } - - private function addPropertyErrors( - SanitizationResult $result, - string $property, - array $propertyErrors - ): void { - foreach ($propertyErrors as $error) { - $result->addError($property, $error['errorKey'], $error['message']); - } - } -} diff --git a/src/Result/SanitizationResult.php b/src/Result/SanitizationResult.php new file mode 100644 index 0000000..0cdb39d --- /dev/null +++ b/src/Result/SanitizationResult.php @@ -0,0 +1,36 @@ +results->hasErrors(); + } + + public function getErrors(): array + { + return $this->results->getErrors(); + } + + public function getSanitizedData(): array + { + return $this->results->getProcessedData(); + } + + public function toArray(): array + { + return $this->results->toArray(); + } +} diff --git a/src/SanitizationResult.php b/src/SanitizationResult.php deleted file mode 100644 index b0f27fb..0000000 --- a/src/SanitizationResult.php +++ /dev/null @@ -1,59 +0,0 @@ -errors[$property])) { - $this->errors[$property] = []; - } - - // Avoid adding duplicate errors - foreach ($this->errors[$property] as $error) { - if ($error['errorKey'] === $errorKey) { - return; - } - } - - $this->errors[$property][] = [ - 'errorKey' => $errorKey, - 'message' => $message, - ]; - } - - public function setSanitizedData(string $property, mixed $value): void - { - $this->sanitizedData[$property] = $value; - } - - public function hasErrors(): bool - { - return !empty($this->errors); - } - - public function getErrors(): array - { - return $this->errors; - } - - public function getSanitizedData(): array - { - return $this->sanitizedData; - } - - public function toArray(): array - { - return [ - 'isValid' => !$this->hasErrors(), - 'errors' => $this->errors, - 'sanitizedData' => $this->sanitizedData, - ]; - } -} diff --git a/src/Sanitizer.php b/src/Sanitizer.php index ebb1752..162bc62 100644 --- a/src/Sanitizer.php +++ b/src/Sanitizer.php @@ -6,19 +6,19 @@ use KaririCode\Contract\Processor\ProcessorRegistry; use KaririCode\Contract\Sanitizer\Sanitizer as SanitizerContract; +use KaririCode\ProcessorPipeline\Handler\ProcessorAttributeHandler; use KaririCode\ProcessorPipeline\ProcessorBuilder; use KaririCode\PropertyInspector\AttributeAnalyzer; -use KaririCode\PropertyInspector\AttributeHandler; use KaririCode\PropertyInspector\Utility\PropertyInspector; use KaririCode\Sanitizer\Attribute\Sanitize; -use KaririCode\Sanitizer\Contract\SanitizationResult; -use KaririCode\Sanitizer\Processor\DefaultSanitizationResultProcessor; +use KaririCode\Sanitizer\Contract\SanitizationResult as SanitizationResultContract; +use KaririCode\Sanitizer\Result\SanitizationResult; -class Sanitizer implements SanitizerContract +final class Sanitizer implements SanitizerContract { private const IDENTIFIER = 'sanitizer'; - private ProcessorBuilder $builder; + private readonly ProcessorBuilder $builder; public function __construct( private readonly ProcessorRegistry $registry @@ -26,18 +26,23 @@ public function __construct( $this->builder = new ProcessorBuilder($this->registry); } - public function sanitize(mixed $object): SanitizationResult + public function sanitize(mixed $object): SanitizationResultContract { - $attributeHandler = new AttributeHandler(self::IDENTIFIER, $this->builder); + $attributeHandler = new ProcessorAttributeHandler( + self::IDENTIFIER, + $this->builder + ); + $propertyInspector = new PropertyInspector( new AttributeAnalyzer(Sanitize::class) ); - $propertyInspector->inspect($object, $attributeHandler); + /** @var PropertyAttributeHandler */ + $handler = $propertyInspector->inspect($object, $attributeHandler); $attributeHandler->applyChanges($object); - $resultProcessor = new DefaultSanitizationResultProcessor(); - - return $resultProcessor->process($attributeHandler); + return new SanitizationResult( + $handler->getProcessingResults() + ); } } diff --git a/src/Trait/CaseTransformerTrait.php b/src/Trait/CaseTransformerTrait.php index 00fbf43..c8dacac 100644 --- a/src/Trait/CaseTransformerTrait.php +++ b/src/Trait/CaseTransformerTrait.php @@ -18,8 +18,32 @@ protected function toUpperCase(string $input): string protected function toCamelCase(string $input): string { - $input = $this->toLowerCase($input); + // If already in camelCase, return without modifying + if ($this->isAlreadyCamelCase($input)) { + return $input; + } - return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + // Remove extra underscores and normalize + $input = trim($input, '_'); + $input = preg_replace('/_+/', '_', $input); + + // Convert to camelCase + $input = strtolower($input); + $output = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input)))); + + return $output; + } + + private function isAlreadyCamelCase(string $input): bool + { + return + // Starts with a lowercase letter + preg_match('/^[a-z]/', $input) + // Contains at least one uppercase letter after the first position + && preg_match('/[A-Z]/', substr($input, 1)) + // Does not contain underscores + && !str_contains($input, '_') + // Follows camelCase pattern (lowercase letter followed by uppercase) + && preg_match('/^[a-z]+(?:[A-Z][a-z0-9]+)*$/', $input); } } diff --git a/src/Trait/CharacterFilterTrait.php b/src/Trait/CharacterFilterTrait.php index 2105859..0ca8955 100644 --- a/src/Trait/CharacterFilterTrait.php +++ b/src/Trait/CharacterFilterTrait.php @@ -8,13 +8,41 @@ trait CharacterFilterTrait { protected function filterAllowedCharacters(string $input, string $allowed): string { - return preg_replace('/[^' . preg_quote($allowed, '/') . ']/', '', $input); + $pattern = ''; + + // Processa os intervalos (ex: a-z, 0-9) + if (preg_match_all('/([a-z0-9])-([a-z0-9])/i', $allowed, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $start = $match[1]; + $end = $match[2]; + $pattern .= $start . '-' . $end; + $allowed = str_replace($match[0], '', $allowed); + } + } + + // Adiciona caracteres individuais restantes + $pattern .= preg_quote($allowed, '/'); + + // Se não houver padrão, retorna string vazia + if (empty($pattern)) { + return ''; + } + + return preg_replace('/[^' . $pattern . ']/u', '', $input); } protected function keepOnlyAlphanumeric(string $input, array $additionalChars = []): string { - $pattern = '/[^a-zA-Z0-9' . preg_quote(implode('', $additionalChars), '/') . ']/'; + $allowed = 'a-zA-Z0-9'; + + if (!empty($additionalChars)) { + $escaped = array_map( + fn ($char) => preg_quote($char, '/'), + $additionalChars + ); + $allowed .= implode('', $escaped); + } - return preg_replace($pattern, '', $input); + return preg_replace('/[^' . $allowed . ']/u', '', $input); } } diff --git a/src/Trait/DomSanitizerTrait.php b/src/Trait/DomSanitizerTrait.php index f706ea0..edcb713 100644 --- a/src/Trait/DomSanitizerTrait.php +++ b/src/Trait/DomSanitizerTrait.php @@ -11,9 +11,18 @@ protected function createDom(string $input, bool $wrapInRoot = true): \DOMDocume $dom = new \DOMDocument('1.0', 'UTF-8'); libxml_use_internal_errors(true); - $content = $wrapInRoot ? '
' . $input . '
' : $input; + // Disable automatic entity encoding + $dom->substituteEntities = false; + $dom->formatOutput = false; + + if ($wrapInRoot) { + $safeInput = '
' . htmlspecialchars_decode($input, ENT_QUOTES | ENT_HTML5) . '
'; + } else { + $safeInput = htmlspecialchars_decode($input, ENT_QUOTES | ENT_HTML5); + } + $dom->loadHTML( - mb_convert_encoding($content, 'HTML-ENTITIES', 'UTF-8'), + mb_convert_encoding($safeInput, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); @@ -22,19 +31,24 @@ protected function createDom(string $input, bool $wrapInRoot = true): \DOMDocume protected function cleanDomOutput(\DOMDocument $dom): string { + // Save without XML declaration and DOCTYPE $output = $dom->saveHTML(); + if (false === $output) { return ''; } - return preg_replace( + // Remove DOCTYPE, html and body tags + $output = preg_replace( [ '/^\n?/', '/<\/?html[^>]*>\n?/', '/<\/?body[^>]*>\n?/', ], '', - trim($output) + $output ); + + return trim($output); } } diff --git a/src/Trait/HtmlCleanerTrait.php b/src/Trait/HtmlCleanerTrait.php index d61972f..d90835c 100644 --- a/src/Trait/HtmlCleanerTrait.php +++ b/src/Trait/HtmlCleanerTrait.php @@ -16,7 +16,12 @@ protected function removeScripts(string $input): string protected function removeComments(string $input): string { - return preg_replace('//s', '', $input); + // Regular expression that matches the innermost HTML comments first + while (preg_match('//', $input)) { + $input = preg_replace('//', '', $input); + } + + return $input; } protected function removeStyle(string $input): string diff --git a/src/Trait/UrlSanitizerTrait.php b/src/Trait/UrlSanitizerTrait.php index b46dd63..f02ba13 100644 --- a/src/Trait/UrlSanitizerTrait.php +++ b/src/Trait/UrlSanitizerTrait.php @@ -6,13 +6,19 @@ trait UrlSanitizerTrait { + private const VALID_PROTOCOLS = [ + 'http://', + 'https://', + 'ftp://', + 'sftp://', + ]; + protected function normalizeProtocol(string $url, string $defaultProtocol = 'https://'): string { - $protocols = ['http://', 'https://', 'ftp://', 'sftp://']; - foreach ($protocols as $protocol) { - if (str_starts_with(strtolower($url), $protocol)) { - return $url; - } + $hasValidProtocol = $this->hasValidProtocol($url); + + if ($hasValidProtocol) { + return $url; } return $defaultProtocol . ltrim($url, '/'); @@ -20,7 +26,69 @@ protected function normalizeProtocol(string $url, string $defaultProtocol = 'htt protected function normalizeSlashes(string $url): string { - // Preserves protocol double slashes - return preg_replace('/([^:])\/+/', '$1/', $url); + if (empty($url)) { + return ''; + } + + $protocol = $this->extractProtocol($url); + $path = $this->extractPath($url, $protocol); + $normalizedPath = $this->normalizePath($path); + + if ($this->isPathEmpty($normalizedPath)) { + return '/'; + } + + return $this->buildUrl($protocol, $normalizedPath); + } + + private function hasValidProtocol(string $url): bool + { + $lowercaseUrl = strtolower($url); + + foreach (self::VALID_PROTOCOLS as $protocol) { + if (str_starts_with($lowercaseUrl, $protocol)) { + return true; + } + } + + return false; + } + + private function extractProtocol(string $url): string + { + $matches = []; + preg_match('/^[a-zA-Z]+:/', $url, $matches); + + return $matches[0] ?? ''; + } + + private function extractPath(string $url, string $protocol): string + { + if (empty($protocol)) { + return $url; + } + + $parts = explode($protocol, $url, 2); + + return $parts[1] ?? ''; + } + + private function normalizePath(string $path): string + { + return preg_replace('/\/+/', '/', $path); + } + + private function isPathEmpty(string $path): bool + { + return '' === trim($path, '/'); + } + + private function buildUrl(string $protocol, string $path): string + { + if (empty($protocol)) { + return $path; + } + + return $protocol . '//' . ltrim($path, '/'); } } diff --git a/tests/Trait/CaseTransformerTraitTest.php b/tests/Trait/CaseTransformerTraitTest.php new file mode 100644 index 0000000..d44fa92 --- /dev/null +++ b/tests/Trait/CaseTransformerTraitTest.php @@ -0,0 +1,96 @@ +traitObject = new class { + use CaseTransformerTrait; + + public function callToLowerCase(string $input): string + { + return $this->toLowerCase($input); + } + + public function callToUpperCase(string $input): string + { + return $this->toUpperCase($input); + } + + public function callToCamelCase(string $input): string + { + return $this->toCamelCase($input); + } + }; + } + + /** + * @dataProvider toLowerCaseProvider + */ + public function testToLowerCase(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callToLowerCase($input)); + } + + /** + * @dataProvider toUpperCaseProvider + */ + public function testToUpperCase(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callToUpperCase($input)); + } + + /** + * @dataProvider toCamelCaseProvider + */ + public function testToCamelCase(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callToCamelCase($input)); + } + + public static function toLowerCaseProvider(): array + { + return [ + 'mixed case' => ['HelloWorld', 'helloworld'], + 'already lowercase' => ['hello', 'hello'], + 'all uppercase' => ['HELLO', 'hello'], + 'with numbers' => ['Hello123World', 'hello123world'], + 'with special chars' => ['Hello@World', 'hello@world'], + 'empty string' => ['', ''], + ]; + } + + public static function toUpperCaseProvider(): array + { + return [ + 'mixed case' => ['HelloWorld', 'HELLOWORLD'], + 'already uppercase' => ['HELLO', 'HELLO'], + 'all lowercase' => ['hello', 'HELLO'], + 'with numbers' => ['Hello123World', 'HELLO123WORLD'], + 'with special chars' => ['Hello@World', 'HELLO@WORLD'], + 'empty string' => ['', ''], + ]; + } + + public static function toCamelCaseProvider(): array + { + return [ + 'simple underscored' => ['hello_world', 'helloWorld'], + 'multiple underscores' => ['hello_beautiful_world', 'helloBeautifulWorld'], + 'already camel case' => ['helloWorld', 'helloWorld'], + 'uppercase' => ['HELLO_WORLD', 'helloWorld'], + 'with numbers' => ['hello_123_world', 'hello123World'], + 'empty string' => ['', ''], + 'single word' => ['hello', 'hello'], + ]; + } +} diff --git a/tests/Trait/CharacterFilterTraitTest.php b/tests/Trait/CharacterFilterTraitTest.php new file mode 100644 index 0000000..65bb5e3 --- /dev/null +++ b/tests/Trait/CharacterFilterTraitTest.php @@ -0,0 +1,71 @@ +traitObject = new class { + use CharacterFilterTrait; + + public function callFilterAllowedCharacters(string $input, string $allowed): string + { + return $this->filterAllowedCharacters($input, $allowed); + } + + public function callKeepOnlyAlphanumeric(string $input, array $additionalChars = []): string + { + return $this->keepOnlyAlphanumeric($input, $additionalChars); + } + }; + } + + /** + * @dataProvider filterAllowedCharactersProvider + */ + public function testFilterAllowedCharacters(string $input, string $allowed, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callFilterAllowedCharacters($input, $allowed)); + } + + /** + * @dataProvider keepOnlyAlphanumericProvider + */ + public function testKeepOnlyAlphanumeric(string $input, array $additionalChars, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callKeepOnlyAlphanumeric($input, $additionalChars)); + } + + public static function filterAllowedCharactersProvider(): array + { + return [ + 'basic filtering' => ['hello123!@#', 'a-z', 'hello'], + 'numbers only' => ['hello123!@#', '0-9', '123'], + 'mixed allowed chars' => ['hello123!@#', 'a-z0-9', 'hello123'], + 'special chars' => ['hello@world!', '@!', '@!'], + 'empty string' => ['', 'a-z', ''], + 'no allowed chars' => ['hello123', 'x-z', ''], + 'with spaces' => ['hello world', 'a-z ', 'hello world'], + ]; + } + + public static function keepOnlyAlphanumericProvider(): array + { + return [ + 'basic alphanumeric' => ['hello123!@#', [], 'hello123'], + 'with dash' => ['hello-123', ['-'], 'hello-123'], + 'with multiple chars' => ['hello@world!123', ['@', '!'], 'hello@world!123'], + 'empty string' => ['', [], ''], + 'only special chars' => ['!@#$%', [], ''], + 'with spaces' => ['hello world', [' '], 'hello world'], + ]; + } +} diff --git a/tests/Trait/CharacterReplacementTraitTest.php b/tests/Trait/CharacterReplacementTraitTest.php new file mode 100644 index 0000000..d0937e3 --- /dev/null +++ b/tests/Trait/CharacterReplacementTraitTest.php @@ -0,0 +1,95 @@ +traitObject = new class { + use CharacterReplacementTrait; + + public function callReplaceConsecutiveCharacters(string $input, string $char, string $replacement): string + { + return $this->replaceConsecutiveCharacters($input, $char, $replacement); + } + + public function callReplaceMultipleCharacters(string $input, array $replacements): string + { + return $this->replaceMultipleCharacters($input, $replacements); + } + }; + } + + /** + * @dataProvider replaceConsecutiveCharactersProvider + */ + public function testReplaceConsecutiveCharacters(string $input, string $char, string $replacement, string $expected): void + { + $this->assertSame( + $expected, + $this->traitObject->callReplaceConsecutiveCharacters($input, $char, $replacement) + ); + } + + /** + * @dataProvider replaceMultipleCharactersProvider + */ + public function testReplaceMultipleCharacters(string $input, array $replacements, string $expected): void + { + $this->assertSame( + $expected, + $this->traitObject->callReplaceMultipleCharacters($input, $replacements) + ); + } + + public static function replaceConsecutiveCharactersProvider(): array + { + return [ + 'basic replacement' => ['hello....world', '.', '.', 'hello.world'], + 'multiple occurrences' => ['hi....there....now', '.', '.', 'hi.there.now'], + 'no consecutive chars' => ['hello.world', '.', '.', 'hello.world'], + 'empty string' => ['', '.', '.', ''], + 'special characters' => ['test***case', '*', '_', 'test_case'], + 'with spaces' => ['hello world', ' ', ' ', 'hello world'], + ]; + } + + public static function replaceMultipleCharactersProvider(): array + { + return [ + 'basic replacements' => [ + 'hello world', + ['hello' => 'hi', 'world' => 'earth'], + 'hi earth', + ], + 'no matches' => [ + 'test case', + ['foo' => 'bar'], + 'test case', + ], + 'empty string' => [ + '', + ['a' => 'b'], + '', + ], + 'special characters' => [ + 'test@case#example', + ['@' => 'at', '#' => 'hash'], + 'testatcasehashexample', + ], + 'overlapping replacements' => [ + 'hello', + ['hell' => 'heaven', 'llo' => 'goodbye'], + 'heaveno', + ], + ]; + } +} diff --git a/tests/Trait/DomSanitizerTraitTest.php b/tests/Trait/DomSanitizerTraitTest.php new file mode 100644 index 0000000..db68b9c --- /dev/null +++ b/tests/Trait/DomSanitizerTraitTest.php @@ -0,0 +1,88 @@ +traitObject = new class { + use DomSanitizerTrait; + + public function callCreateDom(string $input, bool $wrapInRoot = true): \DOMDocument + { + return $this->createDom($input, $wrapInRoot); + } + + public function callCleanDomOutput(\DOMDocument $dom): string + { + return $this->cleanDomOutput($dom); + } + }; + } + + public function testCreateDomWithWrapping(): void + { + $input = '

Test content

'; + $dom = $this->traitObject->callCreateDom($input); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $root = $dom->getElementById('temp-root'); + $this->assertNotNull($root); + $this->assertTrue($root->hasChildNodes()); + } + + public function testCreateDomWithoutWrapping(): void + { + $input = '

Test content

'; + $dom = $this->traitObject->callCreateDom($input, false); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $root = $dom->getElementById('temp-root'); + $this->assertNull($root); + } + + public function testCleanDomOutput(): void + { + $dom = new \DOMDocument(); + $dom->loadHTML('

Test

'); + + $output = $this->traitObject->callCleanDomOutput($dom); + $this->assertSame('

Test

', $output); + } + + public function testCreateDomWithSpecialCharacters(): void + { + $input = '

Test & content

'; + $dom = $this->traitObject->callCreateDom($input); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $html = $dom->saveHTML(); + $this->assertStringContainsString('Test & content', $html); + } + + public function testCreateDomWithNestedElements(): void + { + $input = '

Test content

'; + $dom = $this->traitObject->callCreateDom($input); + + $this->assertInstanceOf(\DOMDocument::class, $dom); + $root = $dom->getElementById('temp-root'); + $this->assertNotNull($root); + $this->assertTrue($root->hasChildNodes()); + } + + public function testCleanDomOutputWithEmptyDocument(): void + { + $dom = new \DOMDocument(); + $output = $this->traitObject->callCleanDomOutput($dom); + $this->assertSame('', $output); + } +} diff --git a/tests/Trait/HtmlCleanerTraitTest.php b/tests/Trait/HtmlCleanerTraitTest.php new file mode 100644 index 0000000..3f61352 --- /dev/null +++ b/tests/Trait/HtmlCleanerTraitTest.php @@ -0,0 +1,133 @@ +traitObject = new class { + use HtmlCleanerTrait; + + public function callRemoveScripts(string $input): string + { + return $this->removeScripts($input); + } + + public function callRemoveComments(string $input): string + { + return $this->removeComments($input); + } + + public function callRemoveStyle(string $input): string + { + return $this->removeStyle($input); + } + }; + } + + /** + * @dataProvider removeScriptsProvider + */ + public function testRemoveScripts(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callRemoveScripts($input)); + } + + /** + * @dataProvider removeCommentsProvider + */ + public function testRemoveComments(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callRemoveComments($input)); + } + + /** + * @dataProvider removeStyleProvider + */ + public function testRemoveStyle(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callRemoveStyle($input)); + } + + public static function removeScriptsProvider(): array + { + return [ + 'basic script' => [ + '

Text

', + '

Text

', + ], + 'script with attributes' => [ + '', + '', + ], + 'inline event handler' => [ + 'Click me', + 'Click me', + ], + 'multiple scripts' => [ + '

Text

', + '

Text

', + ], + 'no scripts' => [ + '

Clean text

', + '

Clean text

', + ], + ]; + } + + public static function removeCommentsProvider(): array + { + return [ + 'basic comment' => [ + '

Text

', + '

Text

', + ], + 'multiple comments' => [ + '

Text

', + '

Text

', + ], + 'multiline comment' => [ + "

Text

", + '

Text

', + ], + 'no comments' => [ + '

Clean text

', + '

Clean text

', + ], + 'nested comments' => [ + ' Comment -->', + '', + ], + ]; + } + + public static function removeStyleProvider(): array + { + return [ + 'basic style' => [ + '', + '', + ], + 'style with attributes' => [ + '', + '', + ], + 'multiple styles' => [ + '

Text

', + '

Text

', + ], + 'no styles' => [ + '

Clean text

', + '

Clean text

', + ], + ]; + } +} diff --git a/tests/Trait/NumericSanitizerTraitTest.php b/tests/Trait/NumericSanitizerTraitTest.php new file mode 100644 index 0000000..08d2997 --- /dev/null +++ b/tests/Trait/NumericSanitizerTraitTest.php @@ -0,0 +1,71 @@ +traitObject = new class { + use NumericSanitizerTrait; + + public function callExtractNumbers(string $input): string + { + return $this->extractNumbers($input); + } + + public function callPreserveDecimalPoint(string $input, string $decimalPoint = '.'): string + { + return $this->preserveDecimalPoint($input, $decimalPoint); + } + }; + } + + /** + * @dataProvider extractNumbersProvider + */ + public function testExtractNumbers(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callExtractNumbers($input)); + } + + /** + * @dataProvider preserveDecimalPointProvider + */ + public function testPreserveDecimalPoint(string $input, string $decimalPoint, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callPreserveDecimalPoint($input, $decimalPoint)); + } + + public static function extractNumbersProvider(): array + { + return [ + 'only numbers' => ['123456', '123456'], + 'mixed content' => ['abc123def456', '123456'], + 'with special chars' => ['!@#123$%^456', '123456'], + 'empty string' => ['', ''], + 'no numbers' => ['abcdef', ''], + 'with spaces' => ['123 456', '123456'], + ]; + } + + public static function preserveDecimalPointProvider(): array + { + return [ + 'simple decimal' => ['123.456', '.', '123.456'], + 'custom decimal point' => ['123,456', ',', '123,456'], + 'multiple decimal points' => ['123.456.789', '.', '123.456789'], + 'no decimal point' => ['123456', '.', '123456'], + 'only decimal point' => ['.', '.', '.'], + 'decimal at start' => ['.123', '.', '.123'], + 'decimal at end' => ['123.', '.', '123.'], + ]; + } +} diff --git a/tests/Trait/UrlSanitizerTraitTest.php b/tests/Trait/UrlSanitizerTraitTest.php new file mode 100644 index 0000000..012eb2f --- /dev/null +++ b/tests/Trait/UrlSanitizerTraitTest.php @@ -0,0 +1,73 @@ +traitObject = new class { + use UrlSanitizerTrait; + + public function callNormalizeProtocol(string $url, string $defaultProtocol = 'https://'): string + { + return $this->normalizeProtocol($url, $defaultProtocol); + } + + public function callNormalizeSlashes(string $url): string + { + return $this->normalizeSlashes($url); + } + }; + } + + /** + * @dataProvider normalizeProtocolProvider + */ + public function testNormalizeProtocol(string $input, string $defaultProtocol, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeProtocol($input, $defaultProtocol)); + } + + /** + * @dataProvider normalizeSlashesProvider + */ + public function testNormalizeSlashes(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeSlashes($input)); + } + + public static function normalizeProtocolProvider(): array + { + return [ + 'no protocol' => ['example.com', 'https://', 'https://example.com'], + 'with http' => ['http://example.com', 'https://', 'http://example.com'], + 'with https' => ['https://example.com', 'http://', 'https://example.com'], + 'with ftp' => ['ftp://example.com', 'https://', 'ftp://example.com'], + 'with sftp' => ['sftp://example.com', 'https://', 'sftp://example.com'], + 'custom protocol' => ['example.com', 'http://', 'http://example.com'], + 'empty string' => ['', 'https://', 'https://'], + 'with extra slashes' => ['/example.com', 'https://', 'https://example.com'], + ]; + } + + public static function normalizeSlashesProvider(): array + { + return [ + 'normal url' => ['https://example.com/path', 'https://example.com/path'], + 'multiple slashes' => ['https://example.com//path', 'https://example.com/path'], + 'preserve protocol slashes' => ['https://example.com', 'https://example.com'], + 'complex path' => ['https://example.com/path//to///resource', 'https://example.com/path/to/resource'], + 'empty string' => ['', ''], + 'only slashes' => ['////', '/'], + 'mixed slashes' => ['http:///example.com//path', 'http://example.com/path'], + ]; + } +} diff --git a/tests/Trait/ValidationTraitTest.php b/tests/Trait/ValidationTraitTest.php new file mode 100644 index 0000000..0c668fe --- /dev/null +++ b/tests/Trait/ValidationTraitTest.php @@ -0,0 +1,93 @@ +traitObject = new class { + use ValidationTrait; + + public function callIsNotEmpty(string $input): bool + { + return $this->isNotEmpty($input); + } + + public function callIsValidUtf8(string $input): bool + { + return $this->isValidUtf8($input); + } + + public function callContainsPattern(string $input, string $pattern): bool + { + return $this->containsPattern($input, $pattern); + } + }; + } + + /** + * @dataProvider isNotEmptyProvider + */ + public function testIsNotEmpty(string $input, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callIsNotEmpty($input)); + } + + /** + * @dataProvider isValidUtf8Provider + */ + public function testIsValidUtf8(string $input, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callIsValidUtf8($input)); + } + + /** + * @dataProvider containsPatternProvider + */ + public function testContainsPattern(string $input, string $pattern, bool $expected): void + { + $this->assertSame($expected, $this->traitObject->callContainsPattern($input, $pattern)); + } + + public static function isNotEmptyProvider(): array + { + return [ + 'non empty string' => ['test', true], + 'empty string' => ['', false], + 'spaces only' => [' ', false], + 'tabs and newlines' => ["\t\n", false], + 'zero as string' => ['0', true], + 'with spaces' => [' test ', true], + ]; + } + + public static function isValidUtf8Provider(): array + { + return [ + 'ascii string' => ['Hello World', true], + 'utf8 string' => ['áéíóú', true], + 'emojis' => ['😀👍🎉', true], + 'empty string' => ['', true], + 'valid mixed content' => ['Hello 世界', true], + ]; + } + + public static function containsPatternProvider(): array + { + return [ + 'simple pattern' => ['test123', '/\d+/', true], + 'email pattern' => ['test@example.com', '/^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,}$/', true], + 'no match' => ['abcdef', '/\d+/', false], + 'complex pattern' => ['ABC-123', '/^[A-Z]+-\d+$/', true], + 'empty string' => ['', '/.*/', true], + ]; + } +} diff --git a/tests/Trait/WhitespaceSanitizerTraitTest.php b/tests/Trait/WhitespaceSanitizerTraitTest.php new file mode 100644 index 0000000..15bda3d --- /dev/null +++ b/tests/Trait/WhitespaceSanitizerTraitTest.php @@ -0,0 +1,99 @@ +traitObject = new class { + use WhitespaceSanitizerTrait; + + public function callRemoveAllWhitespace(string $input): string + { + return $this->removeAllWhitespace($input); + } + + public function callNormalizeWhitespace(string $input): string + { + return $this->normalizeWhitespace($input); + } + + public function callTrimWhitespace(string $input): string + { + return $this->trimWhitespace($input); + } + }; + } + + /** + * @dataProvider removeAllWhitespaceProvider + */ + public function testRemoveAllWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callRemoveAllWhitespace($input)); + } + + /** + * @dataProvider normalizeWhitespaceProvider + */ + public function testNormalizeWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callNormalizeWhitespace($input)); + } + + /** + * @dataProvider trimWhitespaceProvider + */ + public function testTrimWhitespace(string $input, string $expected): void + { + $this->assertSame($expected, $this->traitObject->callTrimWhitespace($input)); + } + + public static function removeAllWhitespaceProvider(): array + { + return [ + 'spaces' => ['hello world', 'helloworld'], + 'tabs and spaces' => ["hello\tworld", 'helloworld'], + 'newlines' => ["hello\nworld", 'helloworld'], + 'multiple spaces' => ['hello world', 'helloworld'], + 'complex whitespace' => ["hello\n\t world", 'helloworld'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ''], + ]; + } + + public static function normalizeWhitespaceProvider(): array + { + return [ + 'multiple spaces' => ['hello world', 'hello world'], + 'tabs' => ["hello\tworld", 'hello world'], + 'newlines' => ["hello\nworld", 'hello world'], + 'mixed whitespace' => ["hello\n\t world", 'hello world'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ' '], + 'leading/trailing spaces' => [' hello world ', ' hello world '], + ]; + } + + public static function trimWhitespaceProvider(): array + { + return [ + 'leading spaces' => [' hello', 'hello'], + 'trailing spaces' => ['hello ', 'hello'], + 'both sides' => [' hello ', 'hello'], + 'tabs' => ["\thello\t", 'hello'], + 'newlines' => ["\nhello\n", 'hello'], + 'mixed whitespace' => [" \t\nhello\t \n", 'hello'], + 'empty string' => ['', ''], + 'only whitespace' => [' ', ''], + ]; + } +}