diff --git a/lib/SP/Domain/Account/Search/AccountSearchTokenizer.php b/lib/SP/Domain/Account/Search/AccountSearchTokenizer.php index 3084a9df..d25b70c4 100644 --- a/lib/SP/Domain/Account/Search/AccountSearchTokenizer.php +++ b/lib/SP/Domain/Account/Search/AccountSearchTokenizer.php @@ -32,7 +32,7 @@ use SP\Util\Filter; final class AccountSearchTokenizer { private const SEARCH_REGEX = /** @lang RegExp */ - '/(?(?[a-zа-я_]+):(?!\s]*)"?(?[^":]+)"?/u'; + '/(?(?[a-zа-я_]+):(?[^"\'\s]+|"[^":]+")/u'; private const FILTERS = [ 'condition' => [ @@ -72,13 +72,18 @@ final class AccountSearchTokenizer return null; } - $filtersAndValues = array_filter(array_combine($filters['filter_subject'], $filters['filter_condition'])); + $filtersAndValues = array_filter( + array_combine( + $filters['filter_subject'], + array_map(static fn($value) => str_replace('"', '', $value), $filters['filter_condition']) + ) + ); return new AccountSearchTokens( Filter::safeSearchString(trim($filters['search'][0] ?? '')), $this->getConditions($filtersAndValues), $this->getItems($filtersAndValues), - $this->getOperator($filtersAndValues)[0], + $this->getOperator($filtersAndValues), ); } @@ -100,8 +105,8 @@ final class AccountSearchTokenizer return null; }, - $filters['filter_subject'], - $filters['filter_condition'] + array_keys($filters), + array_values($filters) ) ); } @@ -116,15 +121,13 @@ final class AccountSearchTokenizer $items = array_filter( $filtersAndValues, static function ($value, $key) { - return in_array($key, array_keys(self::FILTERS['items']['subject']), true) && !empty($value); + return array_key_exists($key, self::FILTERS['items']['subject']) && !empty($value); }, ARRAY_FILTER_USE_BOTH ); return array_combine( - array_map(function ($key) { - return self::FILTERS['items']['subject'][$key]; - }, array_keys($items)), + array_map(static fn($key) => self::FILTERS['items']['subject'][$key], array_keys($items)), array_values($items) ); } @@ -132,11 +135,11 @@ final class AccountSearchTokenizer /** * @param array $filtersAndValues * - * @return array + * @return string|null */ - private function getOperator(array $filtersAndValues): array + private function getOperator(array $filtersAndValues): ?string { - return array_filter( + $operator = array_filter( $filtersAndValues, static function ($value, $key) { return in_array($key, self::FILTERS['operator']['subject'], true) @@ -144,5 +147,7 @@ final class AccountSearchTokenizer }, ARRAY_FILTER_USE_BOTH ); + + return array_shift($operator); } } diff --git a/lib/SP/Domain/Account/Search/AccountSearchTokens.php b/lib/SP/Domain/Account/Search/AccountSearchTokens.php index 9f095793..03386973 100644 --- a/lib/SP/Domain/Account/Search/AccountSearchTokens.php +++ b/lib/SP/Domain/Account/Search/AccountSearchTokens.php @@ -32,15 +32,15 @@ final class AccountSearchTokens private string $search; private array $conditions; private array $items; - private string $operator; + private ?string $operator; /** * @param string $search * @param array $conditions * @param array $items - * @param string $operator + * @param string|null $operator */ - public function __construct(string $search, array $conditions, array $items, string $operator) + public function __construct(string $search, array $conditions, array $items, ?string $operator) { $this->search = $search; $this->conditions = $conditions; diff --git a/lib/SP/Domain/Crypt/Services/SecureSessionService.php b/lib/SP/Domain/Crypt/Services/SecureSessionService.php index 77fc3b12..3039affe 100644 --- a/lib/SP/Domain/Crypt/Services/SecureSessionService.php +++ b/lib/SP/Domain/Crypt/Services/SecureSessionService.php @@ -35,6 +35,8 @@ use SP\Domain\Crypt\Ports\SecureSessionServiceInterface; use SP\Http\RequestInterface; use SP\Infrastructure\File\FileCache; use SP\Infrastructure\File\FileException; +use function SP\logger; +use function SP\processException; /** * Class SecureSessionService @@ -67,7 +69,7 @@ final class SecureSessionService extends Service implements SecureSessionService * * @return Key|false */ - public function getKey(UUIDCookie $cookie) + public function getKey(UUIDCookie $cookie): Key|bool { $this->cookie = $cookie; @@ -117,7 +119,7 @@ final class SecureSessionService extends Service implements SecureSessionService * * @return Key|false */ - private function saveKey() + private function saveKey(): Key|bool { try { $securedKey = Key::createNewRandomKey(); diff --git a/lib/SP/Util/Filter.php b/lib/SP/Util/Filter.php index 5448938a..67908b26 100644 --- a/lib/SP/Util/Filter.php +++ b/lib/SP/Util/Filter.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,8 +24,6 @@ namespace SP\Util; -defined('APP_ROOT') || die(); - /** * Class Filter para el filtrado de datos * @@ -65,9 +63,11 @@ final class Filter } /** - * @param string|int $value + * @param int|string $value + * + * @return int|null */ - public static function getInt($value): ?int + public static function getInt(int|string $value): ?int { $filterVar = filter_var($value, FILTER_SANITIZE_NUMBER_INT); @@ -83,4 +83,4 @@ final class Filter { return filter_var(trim($value), FILTER_UNSAFE_RAW); } -} \ No newline at end of file +} diff --git a/tests/SP/Domain/Account/Search/AccountSearchTokenizerTest.php b/tests/SP/Domain/Account/Search/AccountSearchTokenizerTest.php new file mode 100644 index 00000000..4ac912b9 --- /dev/null +++ b/tests/SP/Domain/Account/Search/AccountSearchTokenizerTest.php @@ -0,0 +1,211 @@ +. + */ + +namespace SP\Tests\Domain\Account\Search; + +use Faker\Factory; +use SP\Domain\Account\Search\AccountSearchConstants; +use SP\Domain\Account\Search\AccountSearchTokenizer; +use SP\Tests\UnitaryTestCase; + +/** + * Class AccountSearchTokenizerTest + */ +class AccountSearchTokenizerTest extends UnitaryTestCase +{ + + /** + * @dataProvider searchByItemDataProvider + * + * @param string $search + * @param array $expectedConditions + * + * @return void + */ + public function testTokenizeFromFilterByItems(string $search, array $expectedConditions): void + { + $tokenizer = new AccountSearchTokenizer(); + $out = $tokenizer->tokenizeFrom($search); + + $this->assertNotNull($out); + $this->assertEquals($expectedConditions, $out->getItems()); + } + + /** + * @dataProvider searchByConditionDataProvider + * + * @param string $search + * @param array $expectedConditions + * + * @return void + */ + public function testTokenizeFromFilterByCondition(string $search, array $expectedConditions): void + { + $tokenizer = new AccountSearchTokenizer(); + $out = $tokenizer->tokenizeFrom($search); + + $this->assertNotNull($out); + $this->assertEquals($expectedConditions, $out->getConditions()); + } + + /** + * @dataProvider searchUsingOperatorDataProvider + * + * @param string $search + * @param string $expectedCondition + * + * @return void + */ + public function testTokenizeFromFilterUsingOperator(string $search, string $expectedCondition): void + { + $tokenizer = new AccountSearchTokenizer(); + $out = $tokenizer->tokenizeFrom($search); + + $this->assertNotNull($out); + $this->assertEquals($expectedCondition, $out->getOperator()); + } + + /** + * @dataProvider searchUsingStringDataProvider + * + * @param string $search + * @param string $expectedString + * + * @return void + */ + public function testTokenizeFromFilterUsingSearchString(string $search, string $expectedString): void + { + $tokenizer = new AccountSearchTokenizer(); + $out = $tokenizer->tokenizeFrom($search); + + $this->assertNotNull($out); + $this->assertEquals($expectedString, $out->getSearch()); + } + + /** + * @return void + */ + public function testTokenizeFromFilterUsingSearchStringWithIsNull(): void + { + $tokenizer = new AccountSearchTokenizer(); + $out = $tokenizer->tokenizeFrom('$'); + + $this->assertNull($out); + } + + public function searchByItemDataProvider(): array + { + $faker = Factory::create(); + $id = $faker->randomNumber(); + $name = $faker->userName; + $file = sprintf('%s.%s', $faker->name(), $faker->fileExtension); + + $conditions = [ + sprintf('id:%d', $id), + sprintf('user:"%s"', $name), + sprintf('group:"%s"', $name), + sprintf('file:"%s"', $file), + sprintf('owner:"%s"', $name), + sprintf('maingroup:"%s"', $name), + sprintf('client:"%s"', $name), + sprintf('category:"%s"', $name), + sprintf('name_regex:"^%s$"', $name), + ]; + + return [ + [$conditions[0], [AccountSearchConstants::FILTER_ACCOUNT_ID => $id]], + [$conditions[1], [AccountSearchConstants::FILTER_USER_NAME => $name]], + [$conditions[2], [AccountSearchConstants::FILTER_GROUP_NAME => $name]], + [$conditions[3], [AccountSearchConstants::FILTER_FILE_NAME => $file]], + [$conditions[4], [AccountSearchConstants::FILTER_OWNER => $name]], + [$conditions[5], [AccountSearchConstants::FILTER_MAIN_GROUP => $name]], + [$conditions[6], [AccountSearchConstants::FILTER_CLIENT_NAME => $name]], + [$conditions[7], [AccountSearchConstants::FILTER_CATEGORY_NAME => $name]], + [$conditions[8], [AccountSearchConstants::FILTER_ACCOUNT_NAME_REGEX => sprintf('^%s$', $name)],], + [ + implode(' ', $conditions), + [ + AccountSearchConstants::FILTER_ACCOUNT_ID => $id, + AccountSearchConstants::FILTER_USER_NAME => $name, + AccountSearchConstants::FILTER_GROUP_NAME => $name, + AccountSearchConstants::FILTER_FILE_NAME => $file, + AccountSearchConstants::FILTER_OWNER => $name, + AccountSearchConstants::FILTER_MAIN_GROUP => $name, + AccountSearchConstants::FILTER_CLIENT_NAME => $name, + AccountSearchConstants::FILTER_CATEGORY_NAME => $name, + AccountSearchConstants::FILTER_ACCOUNT_NAME_REGEX => sprintf('^%s$', $name), + ], + ], + ]; + } + + public function searchByConditionDataProvider(): array + { + $conditions = [ + 'is:expired', + 'not:expired', + 'is:private', + 'not:private', + ]; + + return [ + ...array_map(static fn($value) => [$value, [$value]], $conditions), + [implode(' ', $conditions), array_slice($conditions, -2)], + ]; + } + + public function searchUsingOperatorDataProvider(): array + { + $conditions = [ + 'op:and' => 'and', + 'op:or' => 'or', + ]; + + return [ + ...array_map(static fn($key, $value) => [$key, $value], array_keys($conditions), array_values($conditions)), + [implode(' ', array_keys($conditions)), array_pop($conditions)], + ]; + } + + public function searchUsingStringDataProvider(): array + { + $faker = Factory::create(); + + $conditions = [ + $faker->address, + $faker->streetAddress, + $faker->name, + $faker->userName, + $faker->catchPhrase, + $faker->ipv4, + $faker->bankAccountNumber, + $faker->companyEmail, + $faker->domainName + ]; + + return [ + ...array_map(static fn($value) => [$value, $value], $conditions), + ]; + } +}