diff --git a/README.md b/README.md index 5feba70..08487af 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,241 @@    +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 [](README.md) [](README.pt-br.md)    +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. 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 @@ + ['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/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/Input/StripTagsSanitizer.php b/src/Processor/Input/StripTagsSanitizer.php new file mode 100644 index 0000000..4a16242 --- /dev/null +++ b/src/Processor/Input/StripTagsSanitizer.php @@ -0,0 +1,27 @@ +allowedTags = $options['allowedTags']; + } + } + + public function process(mixed $input): string + { + $input = $this->guardAgainstNonString($input); + + return strip_tags($input, $this->allowedTags); + } +} 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/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/Security/XssSanitizer.php b/src/Processor/Security/XssSanitizer.php new file mode 100644 index 0000000..6c815dc --- /dev/null +++ b/src/Processor/Security/XssSanitizer.php @@ -0,0 +1,17 @@ +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/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/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 = '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);