├── LICENSE.md ├── README.md ├── composer.json ├── config └── muddle.php └── src ├── Attributes └── Unsafe.php ├── Components ├── Append.php ├── Concatenation.php ├── Encrypt.php ├── Entities.php ├── Hex.php ├── Link.php ├── LinkComponent.php ├── Random.php ├── Rotate.php ├── Text.php ├── TextAppend.php ├── TextComponent.php ├── TextConcatenation.php ├── TextDisplayNone.php ├── TextEncrypt.php ├── TextEntities.php ├── TextHex.php ├── TextRandom.php └── TextRotate.php ├── Contracts ├── LinkStrategy.php └── TextStrategy.php ├── Facades └── Muddle.php ├── Muddle.php ├── MuddleServiceProvider.php ├── Strategies ├── Concerns │ └── PicksRandomSibling.php ├── Link │ ├── Append.php │ ├── Concatenation.php │ ├── Encrypt.php │ ├── Entities.php │ ├── Hex.php │ ├── Plain.php │ ├── Random.php │ ├── Rotate.php │ └── UrlEncode.php └── Text │ ├── Append.php │ ├── Comment.php │ ├── Concatenation.php │ ├── DisplayNone.php │ ├── Encrypt.php │ ├── Entities.php │ ├── Hex.php │ ├── Plain.php │ ├── Random.php │ └── Rotate.php └── Support └── Str.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Mo Khosh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Muddle: Obfuscate, Obscure, Confuse, Jumble, Complicate, Perplex 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/mokhosh/muddle.svg?style=flat-square)](https://packagist.org/packages/mokhosh/muddle) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/mokhosh/muddle/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/mokhosh/muddle/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/mokhosh/muddle/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/mokhosh/muddle/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/mokhosh/muddle.svg?style=flat-square)](https://packagist.org/packages/mokhosh/muddle) 7 | 8 | Please, don't just put plain raw emails on your web pages. Bots are going to scrape your pages and fill all of our inboxes with spam emails. 9 | 10 | Obfuscate emails and strings in PHP and Laravel to keep those nasty bots away from finding your email or worse, your users' emails. 11 | 12 | This package uses different strategies to obfuscate clickable and non-clickable emails, so you can choose what suits your needs best. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require mokhosh/muddle 20 | ``` 21 | 22 | ## Usage 23 | 24 | In Laravel Projects: 25 | 26 | ```blade 27 | {{-- instead of handing your emails to spammers like this: --}} 28 | {{ $user->name }} 29 | 30 | {{-- do this: --}} 31 | 32 | 33 | {{-- and we will confuscate the email in random ways to make it impossible for bots to steal your emails --}} 34 | 35 | {{-- default strategy components based on config --}} 36 | {{-- muddled email link --}} 37 | {{-- muddled email text --}} 38 | 39 | {{-- specific link strategy components --}} 40 | {{-- picks a random strategy each time --}} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {{-- specific text strategy components --}} 49 | {{-- picks a random strategy each time --}} 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | ```php 60 | use Mokhosh\Muddle\Facades\Muddle; 61 | use Mokhosh\Muddle\Strategies\Text; 62 | use Mokhosh\Muddle\Strategies\Link; 63 | 64 | // default strategy 65 | Muddle::text('test@example.com'); 66 | Muddle::link('test@example.com'); 67 | 68 | // specific strategy 69 | Muddle::strategy(text: new Text\Encrypt)->text('test@example.com') 70 | Muddle::strategy(link: new Link\Encrypt)->link('test@example.com'); 71 | ``` 72 | 73 | In plain PHP Projects: 74 | 75 | ```php 76 | use Mokhosh\Muddle\Muddle; 77 | use Mokhosh\Muddle\Strategies\Text; 78 | use Mokhosh\Muddle\Strategies\Link; 79 | 80 | $muddle = new Muddle( 81 | text: new Text\Random, 82 | link: new Link\Random, 83 | ); 84 | 85 | $muddle->link('test@example.com'); 86 | ``` 87 | 88 | ## Configuration 89 | 90 | You can publish the config file with: 91 | 92 | ```bash 93 | php artisan vendor:publish --tag="muddle-config" 94 | ``` 95 | 96 | This is the contents of the published config file: 97 | 98 | ```php 99 | return [ 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Default Strategy 104 | |-------------------------------------------------------------------------- 105 | | 106 | | Set default strategies for obfuscating text and email links 107 | | 108 | */ 109 | 'strategy' => [ 110 | 'text' => \Mokhosh\Muddle\Strategies\Text\Random::class, 111 | 'link' => \Mokhosh\Muddle\Strategies\Link\Random::class, 112 | ], 113 | 114 | ]; 115 | ``` 116 | 117 | ## Testing 118 | 119 | ```bash 120 | composer test 121 | ``` 122 | 123 | ## Todo 124 | 125 | - [ ] Add Dusk tests 126 | - [ ] Make loading components dynamic 127 | 128 | ## Contributing 129 | 130 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 131 | 132 | ## Security Vulnerabilities 133 | 134 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 135 | 136 | ## Credits 137 | 138 | - [Mo Khosh](https://github.com/mokhosh) 139 | - [Joe Tannenbaum](https://github.com/joetannenbaum/obfuscate) for the inspiration. 140 | - [Spencer Mortensen](https://spencermortensen.com/articles/email-obfuscation) for the information. 141 | 142 | ## License 143 | 144 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 145 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mokhosh/muddle", 3 | "description": "Obfuscate emails and strings in PHP and Laravel", 4 | "keywords": [ 5 | "Mo Khosh", 6 | "laravel", 7 | "muddle" 8 | ], 9 | "homepage": "https://github.com/mokhosh/muddle", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Mo Khosh", 14 | "email": "mskhoshnazar@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "spatie/laravel-package-tools": "^1.16", 21 | "illuminate/contracts": "^11.0||^12.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.14", 25 | "nunomaduro/collision": "^8.1.1", 26 | "orchestra/testbench": "^9.0.0||^10.0.0", 27 | "pestphp/pest": "^3.5", 28 | "pestphp/pest-plugin-arch": "^3.0", 29 | "pestphp/pest-plugin-laravel": "^3.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Mokhosh\\Muddle\\": "src/", 34 | "Mokhosh\\Muddle\\Database\\Factories\\": "database/factories/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Mokhosh\\Muddle\\Tests\\": "tests/", 40 | "Workbench\\App\\": "workbench/app/" 41 | } 42 | }, 43 | "scripts": { 44 | "post-autoload-dump": "@composer run prepare", 45 | "clear": "@php vendor/bin/testbench package:purge-muddle --ansi", 46 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 47 | "build": [ 48 | "@composer run prepare", 49 | "@php vendor/bin/testbench workbench:build --ansi" 50 | ], 51 | "start": [ 52 | "Composer\\Config::disableProcessTimeout", 53 | "@composer run build", 54 | "@php vendor/bin/testbench serve" 55 | ], 56 | "analyse": "vendor/bin/phpstan analyse", 57 | "test": "vendor/bin/pest", 58 | "test-coverage": "vendor/bin/pest --coverage", 59 | "format": "vendor/bin/pint" 60 | }, 61 | "config": { 62 | "sort-packages": true, 63 | "allow-plugins": { 64 | "pestphp/pest-plugin": true, 65 | "phpstan/extension-installer": true 66 | } 67 | }, 68 | "extra": { 69 | "laravel": { 70 | "providers": [ 71 | "Mokhosh\\Muddle\\MuddleServiceProvider" 72 | ], 73 | "aliases": { 74 | "Muddle": "Mokhosh\\Muddle\\Facades\\Muddle" 75 | } 76 | } 77 | }, 78 | "minimum-stability": "dev", 79 | "prefer-stable": true 80 | } 81 | -------------------------------------------------------------------------------- /config/muddle.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'text' => \Mokhosh\Muddle\Strategies\Text\Random::class, 15 | 'link' => \Mokhosh\Muddle\Strategies\Link\Random::class, 16 | ], 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /src/Attributes/Unsafe.php: -------------------------------------------------------------------------------- 1 | strategy() 23 | )->link($this->email, $this->title) 24 | ); 25 | } 26 | 27 | abstract protected function strategy(): LinkStrategy; 28 | } 29 | -------------------------------------------------------------------------------- /src/Components/Random.php: -------------------------------------------------------------------------------- 1 | strategy() 19 | )->text($this->email); 20 | } 21 | 22 | abstract protected function strategy(): TextStrategy; 23 | } 24 | -------------------------------------------------------------------------------- /src/Components/TextConcatenation.php: -------------------------------------------------------------------------------- 1 | link->muddle($email, $title); 18 | } 19 | 20 | public function text(string $string): string 21 | { 22 | return $this->text->muddle($string); 23 | } 24 | 25 | public function strategy( 26 | ?TextStrategy $text = null, 27 | ?LinkStrategy $link = null, 28 | ): static { 29 | $this->text = $text ?? $this->text; 30 | $this->link = $link ?? $this->link; 31 | 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MuddleServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('muddle') 35 | ->hasConfigFile() 36 | ->hasViewComponents('muddle', 37 | Append::class, 38 | Concatenation::class, 39 | Encrypt::class, 40 | Entities::class, 41 | Hex::class, 42 | Rotate::class, 43 | Random::class, 44 | Link::class, 45 | Text::class, 46 | TextAppend::class, 47 | TextConcatenation::class, 48 | TextDisplayNone::class, 49 | TextEncrypt::class, 50 | TextEntities::class, 51 | TextHex::class, 52 | TextRotate::class, 53 | TextRandom::class, 54 | ); 55 | } 56 | 57 | public function packageRegistered(): void 58 | { 59 | App::singleton(TextStrategy::class, fn () => new (Config::get('muddle.strategy.text'))); 60 | App::singleton(LinkStrategy::class, fn () => new (Config::get('muddle.strategy.link'))); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Strategies/Concerns/PicksRandomSibling.php: -------------------------------------------------------------------------------- 1 | $directory] = pathinfo($reflection->getFileName()); 17 | $files = scandir($directory); 18 | $namespace = $reflection->getNamespaceName(); 19 | $resolvedStates = []; 20 | 21 | foreach ($files as $file) { 22 | if ($file === '.' || $file === '..') { 23 | continue; 24 | } 25 | 26 | ['filename' => $className] = pathinfo($file); 27 | $stateClass = $namespace.'\\'.$className; 28 | 29 | if (static::class === $stateClass) { 30 | continue; 31 | } 32 | 33 | $reflectionSibling = new ReflectionClass($stateClass); 34 | 35 | if ($reflectionSibling->getAttributes(Unsafe::class)) { 36 | continue; 37 | } 38 | 39 | $resolvedStates[] = $stateClass; 40 | } 41 | 42 | return $resolvedStates; 43 | } 44 | 45 | /** 46 | * @return string|TextStrategy|LinkStrategy 47 | */ 48 | protected static function getRandomSibling(): LinkStrategy|TextStrategy|string 49 | { 50 | $siblings = static::getAllSafeSiblings(); 51 | $keys = (new Randomizer)->pickArrayKeys($siblings, 1); 52 | 53 | return $siblings[$keys[0]]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Strategies/Link/Append.php: -------------------------------------------------------------------------------- 1 | $title 18 | 21 | HTML; 22 | } 23 | 24 | public function unmuddle(string $string): string 25 | { 26 | preg_match('/href \+= \'([^\']+)\'/', $string, $domain); 27 | preg_match('/mailto:([^"]+)"/', $string, $username); 28 | preg_match('/>([^<]+)<\/a>/', $string, $title); 29 | 30 | return <<$title[1] 32 | HTML; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Strategies/Link/Concatenation.php: -------------------------------------------------------------------------------- 1 | document.write('$title') 15 | HTML; 16 | } 17 | 18 | public function unmuddle(string $string): string 19 | { 20 | return str_replace([ 21 | "", 23 | "'", 24 | '+', 25 | ], '', $string); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Strategies/Link/Encrypt.php: -------------------------------------------------------------------------------- 1 | $title 19 | 24 | HTML; 25 | } 26 | 27 | public function unmuddle(string $string): string 28 | { 29 | preg_match('/const plain = \'([^\']+)\'/', $string, $plain); 30 | preg_match('/const cipher = \'([^\']+)\'/', $string, $cipher); 31 | preg_match('/href="([^"]+)"/', $string, $muddled); 32 | preg_match('/>([^<]+)<\/a>/', $string, $title); 33 | $decrypted = strtr($muddled[1], $cipher[1], $plain[1]); 34 | 35 | return <<$title[1] 37 | HTML; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Strategies/Link/Entities.php: -------------------------------------------------------------------------------- 1 | $title 21 | HTML; 22 | } 23 | 24 | public function unmuddle(string $string): string 25 | { 26 | return html_entity_decode($string); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Strategies/Link/Hex.php: -------------------------------------------------------------------------------- 1 | $title 18 | 23 | HTML; 24 | } 25 | 26 | public function unmuddle(string $string): string 27 | { 28 | preg_match('/ \^ (\d+)/', $string, $key); 29 | preg_match('/href="([^"]+)"/', $string, $hexed); 30 | preg_match('/>([^<]+)<\/a>/', $string, $title); 31 | $unhexed = Str::unhex($hexed[1], $key[1]); 32 | 33 | return <<$title[1] 35 | HTML; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Strategies/Link/Plain.php: -------------------------------------------------------------------------------- 1 | $title 18 | HTML; 19 | } 20 | 21 | public function unmuddle(string $string): string 22 | { 23 | return $string; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Strategies/Link/Random.php: -------------------------------------------------------------------------------- 1 | muddle($string, $title); 17 | } 18 | 19 | public function unmuddle(string $string): string 20 | { 21 | return 'Not possible 😎'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Strategies/Link/Rotate.php: -------------------------------------------------------------------------------- 1 | $title 18 | 25 | HTML; 26 | } 27 | 28 | public function unmuddle(string $string): string 29 | { 30 | preg_match('/const number = (\d+)/', $string, $number); 31 | preg_match('/href="([^"]+)"/', $string, $rotated); 32 | preg_match('/>([^<]+)<\/a>/', $string, $title); 33 | $rotated = Str::rotate($rotated[1], -$number[1]); 34 | 35 | return <<$title[1] 37 | HTML; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Strategies/Link/UrlEncode.php: -------------------------------------------------------------------------------- 1 | $title 21 | HTML; 22 | } 23 | 24 | public function unmuddle(string $string): string 25 | { 26 | return urldecode($string); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Strategies/Text/Append.php: -------------------------------------------------------------------------------- 1 | 19 | HTML; 20 | 21 | return "".$username.$script; 22 | } 23 | 24 | public function unmuddle(string $string): string 25 | { 26 | preg_match('/innerText \+= "([^"]+)"/', $string, $domain); 27 | preg_match('/>([^<]+)<\/span>/', $string, $username); 28 | 29 | return $username[1].$domain[1]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Strategies/Text/Comment.php: -------------------------------------------------------------------------------- 1 | "; 20 | 21 | return substr_replace($string, $comment, $offset, 0); 22 | } 23 | 24 | public function unmuddle(string $string): string 25 | { 26 | return strip_tags($string); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Strategies/Text/Concatenation.php: -------------------------------------------------------------------------------- 1 | document.write('".$concatenated."');"; 14 | } 15 | 16 | public function unmuddle(string $string): string 17 | { 18 | return str_replace(["", "'", '+'], '', $string); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Strategies/Text/DisplayNone.php: -------------------------------------------------------------------------------- 1 | $domain"; 15 | $style = ""; 16 | 17 | return substr_replace($string, $hidden, $offset, 0).$style; 18 | } 19 | 20 | public function unmuddle(string $string): string 21 | { 22 | return preg_replace('/<(b|style)[^>]*>[^<]+<\/(b|style)>/', '', $string); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Strategies/Text/Encrypt.php: -------------------------------------------------------------------------------- 1 | 23 | HTML; 24 | 25 | return "".strtr( 26 | $string, 27 | $plain, 28 | $cipher, 29 | ).$script; 30 | } 31 | 32 | public function unmuddle(string $string): string 33 | { 34 | preg_match('/const plain = \'([^\']+)\'/', $string, $plain); 35 | preg_match('/const cipher = \'([^\']+)\'/', $string, $cipher); 36 | preg_match('/>([^<]+)<\/span>/', $string, $muddled); 37 | 38 | return strtr( 39 | $muddled[1], 40 | $cipher[1], 41 | $plain[1], 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Strategies/Text/Entities.php: -------------------------------------------------------------------------------- 1 | 21 | HTML; 22 | 23 | return "".$hexed.$script; 24 | } 25 | 26 | public function unmuddle(string $string): string 27 | { 28 | preg_match('/ \^ (\d+)/', $string, $key); 29 | preg_match('/>([^<]+)<\/span>/', $string, $hexed); 30 | 31 | return Str::unhex($hexed[1], $key[1]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Strategies/Text/Plain.php: -------------------------------------------------------------------------------- 1 | muddle($string); 17 | } 18 | 19 | public function unmuddle(string $string): string 20 | { 21 | return 'Not possible 😎'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Strategies/Text/Rotate.php: -------------------------------------------------------------------------------- 1 | 25 | HTML; 26 | 27 | return "".$rotated.$script; 28 | } 29 | 30 | public function unmuddle(string $string): string 31 | { 32 | preg_match('/const number = (\d+)/', $string, $number); 33 | preg_match('/>([^<]+)<\/span>/', $string, $rotated); 34 | 35 | return Str::rotate($rotated[1], -$number[1]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Support/Str.php: -------------------------------------------------------------------------------- 1 | $carry.static::entitizeChar($current), 25 | '' 26 | ); 27 | } 28 | 29 | public static function entitizeChar(string $char): string 30 | { 31 | if (($ord = ord($char)) > 128) { 32 | return $char; 33 | } 34 | 35 | return match ((new Randomizer)->getInt(1, 3)) { 36 | 1 => '&#'.$ord.';', 37 | 2 => '&#x'.dechex($ord).';', 38 | 3 => $char, 39 | }; 40 | } 41 | 42 | public static function randomDomain() 43 | { 44 | $keys = (new Randomizer)->pickArrayKeys(static::$domains, 1); 45 | 46 | return static::$domains[$keys[0]]; 47 | } 48 | 49 | /** 50 | * Get a random offset within a string, optionally between the 51 | * first occurrence of `$start` and the last occurrence of `$end` 52 | */ 53 | public static function randomOffset(string $string, ?string $start = null, ?string $end = null): int 54 | { 55 | $min = is_null($start) ? 0 : strpos($string, $start); 56 | $max = is_null($end) ? strlen($string) : strrpos($string, $end); 57 | 58 | return random_int($min, $max); 59 | } 60 | 61 | public static function rotate(string $string, int $number = 16): string 62 | { 63 | $number %= 64; 64 | $plain = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890@.'; 65 | $cipher = substr($plain, $number).substr($plain, 0, $number); 66 | 67 | return strtr($string, $plain, $cipher); 68 | } 69 | 70 | public static function id(string $prefix = 'C', ?int $number = null): string 71 | { 72 | $number ??= (new Randomizer)->getInt(100_000, 999_999); 73 | 74 | return $prefix.$number; 75 | } 76 | 77 | /** 78 | * @param int $key Integer between 0 and 255 79 | */ 80 | public static function hex(string $string, int $key = 64): string 81 | { 82 | $hexed = array_map( 83 | fn ($char) => dechex(ord($char) ^ $key), 84 | str_split($string), 85 | ); 86 | 87 | return implode(' ', $hexed); 88 | } 89 | 90 | /** 91 | * @param int $key Integer between 0 and 255 92 | */ 93 | public static function unhex(string $string, int $key = 64): string 94 | { 95 | $unhexed = array_map( 96 | fn ($char) => chr(intval($char, 16) ^ $key), 97 | explode(' ', $string), 98 | ); 99 | 100 | return implode($unhexed); 101 | } 102 | 103 | public static function shuffle(string $string): string 104 | { 105 | return (new Randomizer)->shuffleBytes($string); 106 | } 107 | 108 | public static function urlEncode(string $string): string 109 | { 110 | $hexed = array_map( 111 | fn ($char) => '%'.dechex(ord($char)), 112 | str_split($string), 113 | ); 114 | 115 | return implode($hexed); 116 | } 117 | } 118 | --------------------------------------------------------------------------------