Skip to content

Commit

Permalink
Merge pull request #3 from midnite81/feature/ensure-identifiers-are-e…
Browse files Browse the repository at this point in the history
…xcaped-correctly

Enhance Guardian identifier handling
  • Loading branch information
midnite81 authored Oct 6, 2024
2 parents 8d170ab + 6193da7 commit de380a2
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 3 deletions.
11 changes: 9 additions & 2 deletions src/Guardian.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Midnite81\Guardian\Exceptions\RulePreventsExecutionException;
use Midnite81\Guardian\Helpers\Arrays;
use Midnite81\Guardian\Helpers\RulesetPreparator;
use Midnite81\Guardian\Helpers\Str;
use Midnite81\Guardian\Rules\ErrorHandlingRule;
use Midnite81\Guardian\Rules\RateLimitRule;
use Midnite81\Guardian\Rulesets\GenericErrorHandlingRuleset;
Expand Down Expand Up @@ -124,7 +125,8 @@ public function setIdentifier(string $identifier, string $prefix = 'guardian'):
throw new IdentifierCannotBeEmptyException('Identifier cannot be empty');
}

$safe = preg_replace('/[^a-zA-Z0-9_-]/', '', $identifier);
$safe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $identifier);
$safePrefix = preg_replace('/[^a-zA-Z0-9_-]/', '_', $prefix);

if (empty($prefix)) {
if (!preg_match('/^[a-zA-Z]/', $safe ?? '')) {
Expand All @@ -136,7 +138,12 @@ public function setIdentifier(string $identifier, string $prefix = 'guardian'):
throw new IdentifierCannotBeEmptyException('Identifier cannot be empty');
}

$this->identifier = ($prefix ? $prefix . '_' : '') . substr($safe, 0, 128);
$this->identifier = Str::of(($safePrefix ? $safePrefix . '_' : '') . substr($safe, 0, 128))
->removeDuplicateCharacters('_')
->removeFinalCharIf('_')
->toLower()
->limit(100)
->toString();
}

/**
Expand Down
114 changes: 114 additions & 0 deletions src/Helpers/Str.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

declare(strict_types=1);

namespace Midnite81\Guardian\Helpers;

/**
* Class Str
*
* A utility class for string manipulation operations.
*
* @internal This class is not part of the public API and may change without notice.
*/
class Str
{
/**
* Constructor.
*
* @param string $string The initial string to manipulate.
*/
public function __construct(protected string $string)
{
}

/**
* Create a new instance of Str with the given string.
*
* @param string $string The string to manipulate.
* @return Str
*/
public static function of(string $string): Str
{
return new Str($string);
}

/**
* Get the current string value.
*
* @return string
*/
public function toString(): string
{
return $this->string;
}

/**
* Convert the string to lowercase.
*
* @return $this
*/
public function toLower(): Str
{
$this->string = strtolower($this->string);

return $this;
}

/**
* Remove duplicate occurrences of specified character(s).
*
* @param string|array<int, string> $characters The character(s) to remove duplicates of.
* @return $this
*/
public function removeDuplicateCharacters(string|array $characters = []): Str
{
if (is_string($characters)) {
$characters = [$characters];
}

$pattern = '/(' . implode('|', array_map('preg_quote', $characters, array_fill(0, count($characters), '/'))) . ')\1+/u';
$this->string = (string) preg_replace($pattern, '$1', $this->string);

return $this;
}

/**
* Remove the final character if it matches the specified character.
*
* @param string $character The character to remove if it's at the end.
* @return $this
*/
public function removeFinalCharIf(string $character): Str
{
$this->string = rtrim($this->string, '_');

return $this;
}

/**
* Limit the string to a specified number of characters.
*
* @param int $numberOfCharacters The maximum number of characters to keep.
* @return $this
*/
public function limit(int $numberOfCharacters): Str
{
$this->string = substr($this->string, 0, $numberOfCharacters);

return $this;
}

/**
* Modify the string using a custom callback function.
*
* @param callable $callback A function that takes a string and returns a modified string.
* @return $this
*/
public function modify(callable $callback): Str
{
$this->string = $callback($this->string);

return $this;
}
}
39 changes: 38 additions & 1 deletion tests/GuardianTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,50 @@
->toThrow(IdentifierCannotBeEmptyException::class, 'Identifier cannot be empty');
});

it('must make safe an identifer starting with a number', function () {
it('must make safe an identifier starting with a number', function () {
$guardian = new Guardian('test', new LaravelStore(app('cache.store')));

$guardian->setIdentifier('001', '');
expect($guardian->getIdentifier())->toBe('id_001');
});

it('handles various identifier inputs', function () {
$guardian = new Guardian('test', new LaravelStore(app('cache.store')));

// Test with special characters
$guardian->setIdentifier('user@123_action/get');
expect($guardian->getIdentifier())->toBe('guardian_user_123_action_get');

// Test with spaces
$guardian->setIdentifier('user with spaces');
expect($guardian->getIdentifier())->toBe('guardian_user_with_spaces');

// Test with numbers only
$guardian->setIdentifier('12345');
expect($guardian->getIdentifier())->toBe('guardian_12345');

// Test with uppercase letters
$guardian->setIdentifier('USER_UPPERCASE');
expect($guardian->getIdentifier())->toBe('guardian_user_uppercase');

// Test with non-alphanumeric characters
$guardian->setIdentifier('user!@#$%^&*()');
expect($guardian->getIdentifier())->toBe('guardian_user');

// Test with leading and trailing spaces
$guardian->setIdentifier(' trimmed_user ');
expect($guardian->getIdentifier())->toBe('guardian_trimmed_user');

// Test with very long identifier
$longIdentifier = str_repeat('a', 150);
$guardian->setIdentifier($longIdentifier);
expect($guardian->getIdentifier())->toBe('guardian_' . substr($longIdentifier, 0, 91));

// Test with a prefix
$guardian->setIdentifier('user', 'prefix/prefix');
expect($guardian->getIdentifier())->toBe('prefix_prefix_user');
});

it('throws exception by default when no error rules are set', function () {
$guardian = new Guardian('test', new LaravelStore(app('cache.store')));

Expand Down
60 changes: 60 additions & 0 deletions tests/Helpers/StrTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

use Midnite81\Guardian\Helpers\Str;

it('creates a new instance with a given string', function () {
$str = Str::of('Hello World');
expect($str)->toBeInstanceOf(Str::class);
expect($str->toString())->toBe('Hello World');
});

it('converts string to lowercase', function () {
$str = Str::of('HELLO WORLD');
expect($str->toLower()->toString())->toBe('hello world');
});

it('removes duplicate characters', function () {
$str = Str::of('aabbccddee');
expect($str->removeDuplicateCharacters(['a', 'b'])->toString())->toBe('abccddee');

$str = Str::of('aabbccddee');
expect($str->removeDuplicateCharacters('a')->toString())->toBe('abbccddee');

$str = Str::of('a__b__c__');
expect($str->removeDuplicateCharacters('_')->toString())->toBe('a_b_c_');
});

it('removes final character if it matches', function () {
$str = Str::of('Hello_');
expect($str->removeFinalCharIf('_')->toString())->toBe('Hello');

$str = Str::of('Hello');
expect($str->removeFinalCharIf('_')->toString())->toBe('Hello');
});

it('limits the string to specified number of characters', function () {
$str = Str::of('Hello World');
expect($str->limit(5)->toString())->toBe('Hello');

$str = Str::of('Hi');
expect($str->limit(5)->toString())->toBe('Hi');
});

it('modifies the string using a custom callback', function () {
$str = Str::of('hello world');
$result = $str->modify(function ($string) {
return strtoupper($string);
});
expect($result->toString())->toBe('HELLO WORLD');
});

it('chains multiple operations', function () {
$str = Str::of('HELLO__WORLD__');
$result = $str->toLower()
->removeDuplicateCharacters('_')
->removeFinalCharIf('_')
->limit(10);
expect($result->toString())->toBe('hello_worl');
});

0 comments on commit de380a2

Please sign in to comment.