diff --git a/src/Create/Hash.php b/src/Create/Hash.php index e408f7d..9ed734d 100644 --- a/src/Create/Hash.php +++ b/src/Create/Hash.php @@ -49,7 +49,7 @@ class Hash * replacement for __crc32b call * * @param string $string string to hash - * @param bool $use_sha use sha1 instead of crc32b (default false) + * @param bool $use_sha [default=false] use sha1 instead of crc32b * @return string hash of the string * @deprecated use __crc32b() for drop in replacement with default, or sha1Short() for use sha true */ @@ -81,7 +81,7 @@ class Hash * all that create 8 char long hashes * * @param string $string string to hash - * @param string $hash_type hash type (default adler32) + * @param string $hash_type [default=STANDARD_HASH_SHORT] hash type (default adler32) * @return string hash of the string * @deprecated use hashShort() of short hashes with adler 32 or hash() for other hash types */ @@ -92,12 +92,40 @@ class Hash return self::hash($string, $hash_type); } + /** + * check if hash type is valid, returns false if not + * + * @param string $hash_type + * @return bool + */ + public static function isValidHashType(string $hash_type): bool + { + if (!in_array($hash_type, hash_algos())) { + return false; + } + return true; + } + + /** + * check if hash hmac type is valid, returns false if not + * + * @param string $hash_hmac_type + * @return bool + */ + public static function isValidHashHmacType(string $hash_hmac_type): bool + { + if (!in_array($hash_hmac_type, hash_hmac_algos())) { + return false; + } + return true; + } + /** * creates a hash over string if any valid hash given. * if no hash type set use sha256 * - * @param string $string string to ash - * @param string $hash_type hash type (default sha256) + * @param string $string string to hash + * @param string $hash_type [default=STANDARD_HASH] hash type (default sha256) * @return string hash of the string */ public static function hash( @@ -108,12 +136,36 @@ class Hash empty($hash_type) || !in_array($hash_type, hash_algos()) ) { - // fallback to default hash type if none set or invalid + // fallback to default hash type if empty or invalid $hash_type = self::STANDARD_HASH; } return hash($hash_type, $string); } + /** + * creates a hash mac key + * + * @param string $string string to hash mac + * @param string $key key to use + * @param string $hash_type [default=STANDARD_HASH] + * @return string hash mac string + */ + public static function hashHmac( + string $string, + #[\SensitiveParameter] + string $key, + string $hash_type = self::STANDARD_HASH + ): string { + if ( + empty($hash_type) || + !in_array($hash_type, hash_hmac_algos()) + ) { + // fallback to default hash type if e or invalid + $hash_type = self::STANDARD_HASH; + } + return hash_hmac($hash_type, $string, $key); + } + /** * short hash with max length of 8, uses adler32 * diff --git a/src/DB/IO.php b/src/DB/IO.php index a10a089..9f60d38 100644 --- a/src/DB/IO.php +++ b/src/DB/IO.php @@ -4283,6 +4283,17 @@ class IO return $this->field_names[$pos] ?? false; } + /** + * get all the $ placeholders + * + * @param string $query + * @return array + */ + public function dbGetQueryParamPlaceholders(string $query): array + { + return $this->db_functions->__dbGetQueryParams($query); + } + /** * Return a field type for a field name or pos, * will return false if field is not found in list diff --git a/src/DB/SQL/PgSQL.php b/src/DB/SQL/PgSQL.php index a343c8b..352b6a5 100644 --- a/src/DB/SQL/PgSQL.php +++ b/src/DB/SQL/PgSQL.php @@ -978,12 +978,12 @@ class PgSQL implements Interface\SqlFunctions } /** - * Count placeholder queries. $ only + * Get the all the $ params, unique list * * @param string $query - * @return int + * @return array */ - public function __dbCountQueryParams(string $query): int + public function __dbGetQueryParams(string $query): array { $matches = []; // regex for params: only stand alone $number allowed @@ -998,11 +998,22 @@ class PgSQL implements Interface\SqlFunctions // Matches in 1:, must be array_filtered to remove empty, count with array_unique // Regex located in the ConvertPlaceholder class preg_match_all( - ConvertPlaceholder::REGEX_LOOKUP_PLACEHOLDERS, + ConvertPlaceholder::REGEX_LOOKUP_NUMBERED, $query, $matches ); - return count(array_unique(array_filter($matches[3]))); + return array_unique(array_filter($matches[ConvertPlaceholder::MATCHING_POS])); + } + + /** + * Count placeholder queries. $ only + * + * @param string $query + * @return int + */ + public function __dbCountQueryParams(string $query): int + { + return count($this->__dbGetQueryParams($query)); } } diff --git a/src/DB/Support/ConvertPlaceholder.php b/src/DB/Support/ConvertPlaceholder.php index 484b782..9fbbf87 100644 --- a/src/DB/Support/ConvertPlaceholder.php +++ b/src/DB/Support/ConvertPlaceholder.php @@ -14,76 +14,57 @@ namespace CoreLibs\DB\Support; class ConvertPlaceholder { - // NOTE for missing: range */+ are not iplemented in the regex below, but - is for now - // NOTE some combinations are allowed, but the query will fail before this - /** @var string split regex, entries before $ group */ - private const PATTERN_QUERY_SPLIT = - '\?\?|' // UNKNOWN: double ??, is this to avoid something? - . '[\(,]|' // for ',' and '(' mostly in INSERT or ANY() - . '[<>=]|' // general set for <, >, = in any query with any combination - . '\^@|' // text search for start from text with ^@ - . '\|\||' // concats two elements - . '&&|' // array overlap - . '\-\|\-|' // range overlap for array - . '[^-]-{1}|' // single -, used in JSON too - . '->|->>|#>|#>>|@>|<@|@@|@\?|\?{1}|\?\||\?&|#-|' // JSON searches, Array searchs, etc - . 'THEN|ELSE' // command parts (CASE) - ; - /** @var string the main regex including the pattern query split */ - private const PATTERN_ELEMENT = '(?:\'.*?\')?\s*(?:' . self::PATTERN_QUERY_SPLIT . ')\s*'; + /** @var string text block in SQL, single quited + * Note that does not include $$..$$ strings or anything with token name or nested ones + */ + private const PATTERN_TEXT_BLOCK_SINGLE_QUOTE = '(?:\'(?:[^\'\\\\]|\\\\.)*\')'; + /** @var string text block in SQL, dollar quoted + * NOTE: if this is added everything shifts by one lookup number + */ + private const PATTERN_TEXT_BLOCK_DOLLAR = '(?:\$(\w*)\$.*?\$\1\$)'; /** @var string comment regex - * anything that starts with -- and ends with a line break but any character that is not line break inbetween */ - private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)*\s*'; - /** @var string parts to ignore in the SQL */ - private const PATTERN_IGNORE = - // digit -> ignore - '\d+|' - // other string -> ignore - . '(?:\'.*?\')|'; - /** @var string named parameters */ - private const PATTERN_NAMED = '(:\w+)'; - /** @var string question mark parameters */ - private const PATTERN_QUESTION_MARK = '(?:(?:\?\?)?\s*(\?{1}))'; - /** @var string numbered parameters */ + * anything that starts with -- and ends with a line break but any character that is not line break inbetween + * this is the FIRST thing in the line and will skip any further lookups */ + private const PATTERN_COMMENT = '(?:\-\-[^\r\n]*?\r?\n)'; + // below are the params lookups + /** @var string named parameters, must start with single : */ + private const PATTERN_NAMED = '((? add line break to matches in "." . '/s'; + /** @var string lookup for only numbered placeholders */ + public const REGEX_LOOKUP_NUMBERED = '/' + . self::PATTERN_COMMENT . '|' + . self::PATTERN_TEXT_BLOCK_SINGLE_QUOTE . '|' + . self::PATTERN_TEXT_BLOCK_DOLLAR . '|' + // match for replace part + . '(?:' + // $n numbered part (\PG php) [1] + . self::PATTERN_NUMBERED + // end match + . ')' + . '/s'; + /** @var int position for regex in full placeholder lookup: named */ + public const LOOOKUP_NAMED_POS = 2; + /** @var int position for regex in full placeholder lookup: question mark */ + public const LOOOKUP_QUESTION_MARK_POS = 3; + /** @var int position for regex in full placeholder lookup: numbered */ + public const LOOOKUP_NUMBERED_POS = 4; + /** @var int matches position for replacement and single lookup */ + public const MATCHING_POS = 2; /** * Convert PDO type query with placeholders to \PG style and vica versa @@ -132,11 +133,12 @@ class ConvertPlaceholder $found = -1; } /** @var array 1: named */ - $named_matches = array_filter($matches[1]); + $named_matches = array_filter($matches[self::LOOOKUP_NAMED_POS]); /** @var array 2: open ? */ - $qmark_matches = array_filter($matches[2]); + $qmark_matches = array_filter($matches[self::LOOOKUP_QUESTION_MARK_POS]); /** @var array 3: $n matches */ - $numbered_matches = array_filter($matches[3]); + $numbered_matches = array_filter($matches[self::LOOOKUP_NUMBERED_POS]); + // print "**MATCHES**:
" . print_r($matches, true) . "
"; // count matches $count_named = count(array_unique($named_matches)); $count_qmark = count($qmark_matches); @@ -235,38 +237,37 @@ class ConvertPlaceholder $empty_params = $converted_placeholders['original']['empty_params']; switch ($converted_placeholders['type']) { case 'named': - // 0: full - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part :named + // 1: replace part :named $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_NAMED, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { - // only count up if $match[3] is not yet in lookup table - if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 209 + ); + } + $match = $matches[self::MATCHING_POS]; + // only count up if $match[1] is not yet in lookup table + if (empty($params_lookup[$match])) { $pos++; - $params_lookup[$matches[3]] = '$' . $pos; + $params_lookup[$match] = '$' . $pos; // skip params setup if param list is empty if (!$empty_params) { - $params_new[] = $params[$matches[3]] ?? + $params_new[] = $params[$match] ?? throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params list', + 'Cannot lookup ' . $match . ' in params list', 210 ); } } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - $params_lookup[$matches[3]] ?? - throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params lookup list', - 211 - ) - ); + return $params_lookup[$match] ?? + throw new \RuntimeException( + 'Cannot lookup ' . $match . ' in params lookup list', + 211 + ); }, $converted_placeholders['original']['query'] ); @@ -276,61 +277,61 @@ class ConvertPlaceholder // order and data stays the same $params_new = $params ?? []; } - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part ? + // 1: replace part ? $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_QUESTION_MARK, function ($matches) use (&$pos, &$params_lookup) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 229 + ); + } + $match = $matches[self::MATCHING_POS]; // only count pos up for actual replacements we will do - if (!empty($matches[3])) { + if (!empty($match)) { $pos++; $params_lookup[] = '$' . $pos; } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - '$' . $pos - ); + return '$' . $pos; }, $converted_placeholders['original']['query'] ); break; case 'numbered': - // 0: full - // 1: pre part - // 2: keep part UNLESS '3' is set - // 3: replace part $numbered + // 1: replace part $numbered $pos = 0; $query_new = preg_replace_callback( self::REGEX_REPLACE_NUMBERED, function ($matches) use (&$pos, &$params_new, &$params_lookup, $params, $empty_params) { - // only count up if $match[3] is not yet in lookup table - if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + if (!isset($matches[self::MATCHING_POS])) { + throw new \RuntimeException( + 'Cannot lookup ' . self::MATCHING_POS . ' in matches list', + 239 + ); + } + $match = $matches[self::MATCHING_POS]; + // only count up if $match[1] is not yet in lookup table + if (empty($params_lookup[$match])) { $pos++; - $params_lookup[$matches[3]] = ':' . $pos . '_named'; + $params_lookup[$match] = ':' . $pos . '_named'; // skip params setup if param list is empty if (!$empty_params) { $params_new[] = $params[($pos - 1)] ?? throw new \RuntimeException( 'Cannot lookup ' . ($pos - 1) . ' in params list', - 220 + 230 ); } } // add the connectors back (1), and the data sets only if no replacement will be done - return $matches[1] . ( - empty($matches[3]) ? - $matches[2] : - $params_lookup[$matches[3]] ?? - throw new \RuntimeException( - 'Cannot lookup ' . $matches[3] . ' in params lookup list', - 221 - ) - ); + return $params_lookup[$match] ?? + throw new \RuntimeException( + 'Cannot lookup ' . $match . ' in params lookup list', + 231 + ); }, $converted_placeholders['original']['query'] ); diff --git a/test/phpunit/Create/CoreLibsCreateHashTest.php b/test/phpunit/Create/CoreLibsCreateHashTest.php index 4eb7e69..9ce36b3 100644 --- a/test/phpunit/Create/CoreLibsCreateHashTest.php +++ b/test/phpunit/Create/CoreLibsCreateHashTest.php @@ -21,8 +21,10 @@ final class CoreLibsCreateHashTest extends TestCase public function hashData(): array { return [ - 'any string' => [ + 'hash tests' => [ + // this is the string 'text' => 'Some String Text', + // hash list special 'crc32b_reverse' => 'c5c21d91', // crc32b (in revere) 'sha1Short' => '4d2bc9ba0', // sha1Short // via hash @@ -31,6 +33,8 @@ final class CoreLibsCreateHashTest extends TestCase 'fnv132' => '9df444f9', // hash: fnv132 'fnv1a32' => '2c5f91b9', // hash: fnv1a32 'joaat' => '50dab846', // hash: joaat + 'ripemd160' => 'aeae3f041b20136451519edd9361570909300342', // hash: ripemd160, + 'sha256' => '9055080e022f224fa835929b80582b3c71c672206fa3a49a87412c25d9d42ceb', // hash: sha256 ] ]; } @@ -81,7 +85,7 @@ final class CoreLibsCreateHashTest extends TestCase { $list = []; foreach ($this->hashData() as $name => $values) { - foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat'] as $_hash_type) { + foreach ([null, 'crc32b', 'adler32', 'fnv132', 'fnv1a32', 'joaat', 'ripemd160', 'sha256'] as $_hash_type) { // default value test if ($_hash_type === null) { $hash_type = \CoreLibs\Create\Hash::STANDARD_HASH_SHORT; @@ -288,7 +292,7 @@ final class CoreLibsCreateHashTest extends TestCase * Undocumented function * * @covers ::hash - * @testdox hash with invalid type [$_dataName] + * @testdox hash with invalid type * * @return void */ @@ -301,6 +305,122 @@ final class CoreLibsCreateHashTest extends TestCase \CoreLibs\Create\Hash::hash($hash_source, 'DOES_NOT_EXIST') ); } + + /** + * Note: this only tests default sha256 + * + * @covers ::hashHmac + * @testdox hash hmac test + * + * @return void + */ + public function testHashMac(): void + { + $hash_key = 'FIX KEY'; + $hash_source = 'Some String Text'; + $expected = '16479b3ef6fa44e1cdd8b2dcfaadf314d1a7763635e8738f1e7996d714d9b6bf'; + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key) + ); + } + + /** + * Undocumented function + * + * @covers ::hashHmac + * @testdox hash hmac with invalid type + * + * @return void + */ + public function testInvalidHashMacType(): void + { + $hash_key = 'FIX KEY'; + $hash_source = 'Some String Text'; + $expected = hash_hmac(\CoreLibs\Create\Hash::STANDARD_HASH, $hash_source, $hash_key); + $this->assertEquals( + $expected, + \CoreLibs\Create\Hash::hashHmac($hash_source, $hash_key, 'DOES_NOT_EXIST') + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function providerHashTypes(): array + { + return [ + 'Hash crc32b' => [ + 'crc32b', + true, + false, + ], + 'Hash adler32' => [ + 'adler32', + true, + false, + ], + 'HAsh fnv132' => [ + 'fnv132', + true, + false, + ], + 'Hash fnv1a32' => [ + 'fnv1a32', + true, + false, + ], + 'Hash: joaat' => [ + 'joaat', + true, + false, + ], + 'Hash: ripemd160' => [ + 'ripemd160', + true, + true, + ], + 'Hash: sha256' => [ + 'sha256', + true, + true, + ], + 'Hash: invalid' => [ + 'invalid', + false, + false + ] + ]; + } + + /** + * Undocumented function + * + * @covers ::isValidHashType + * @covers ::isValidHashHmacType + * @dataProvider providerHashTypes + * @testdox check if $hash_type is valid for hash $hash_ok and hash hmac $hash_hmac_ok [$_dataName] + * + * @param string $hash_type + * @param bool $hash_ok + * @param bool $hash_hmac_ok + * @return void + */ + public function testIsValidHashAndHashHmacTypes(string $hash_type, bool $hash_ok, bool $hash_hmac_ok): void + { + $this->assertEquals( + $hash_ok, + \CoreLibs\Create\Hash::isValidHashType($hash_type), + 'hash valid' + ); + $this->assertEquals( + $hash_hmac_ok, + \CoreLibs\Create\Hash::isValidHashHmacType($hash_type), + 'hash hmac valid' + ); + } } // __END__ diff --git a/test/phpunit/DB/CoreLibsDBIOTest.php b/test/phpunit/DB/CoreLibsDBIOTest.php index d85de03..4ed9cab 100644 --- a/test/phpunit/DB/CoreLibsDBIOTest.php +++ b/test/phpunit/DB/CoreLibsDBIOTest.php @@ -135,6 +135,7 @@ final class CoreLibsDBIOTest extends TestCase } // check if they already exist, drop them if ($db->dbShowTableMetaData('table_with_primary_key') !== false) { + $db->dbExec("CREATE EXTENSION IF NOT EXISTS pgcrypto"); $db->dbExec("DROP TABLE table_with_primary_key"); $db->dbExec("DROP TABLE table_without_primary_key"); $db->dbExec("DROP TABLE test_meta"); @@ -4744,7 +4745,7 @@ final class CoreLibsDBIOTest extends TestCase $res = $db->dbReturnRowParams($query_select, ['CONVERT_TYPE_TEST']); // all hast to be string foreach ($res as $key => $value) { - $this->assertIsString($value, 'Aseert string for column: ' . $key); + $this->assertIsString($value, 'Assert string for column: ' . $key); } // convert base only $db->dbSetConvertFlag(Convert::on); @@ -4757,10 +4758,10 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4774,13 +4775,13 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4794,17 +4795,17 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; case 'json': case 'jsonb': - $this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); + $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -4818,25 +4819,25 @@ final class CoreLibsDBIOTest extends TestCase } switch ($type_layout[$name]) { case 'int': - $this->assertIsInt($value, 'Aseert int for column: ' . $key . '/' . $name); + $this->assertIsInt($value, 'Assert int for column: ' . $key . '/' . $name); break; case 'float': - $this->assertIsFloat($value, 'Aseert float for column: ' . $key . '/' . $name); + $this->assertIsFloat($value, 'Assert float for column: ' . $key . '/' . $name); break; case 'json': case 'jsonb': - $this->assertIsArray($value, 'Aseert array for column: ' . $key . '/' . $name); + $this->assertIsArray($value, 'Assert array for column: ' . $key . '/' . $name); break; case 'bytea': // for hex types it must not start with \x $this->assertStringStartsNotWith( '\x', $value, - 'Aseert bytes not starts with \x for column: ' . $key . '/' . $name + 'Assert bytes not starts with \x for column: ' . $key . '/' . $name ); break; default: - $this->assertIsString($value, 'Aseert string for column: ' . $key . '/' . $name); + $this->assertIsString($value, 'Assert string for column: ' . $key . '/' . $name); break; } } @@ -5235,6 +5236,9 @@ final class CoreLibsDBIOTest extends TestCase $3 -- comment 3 , $4 + -- ignore $5, $6 + -- $7, $8 + -- digest($9, 10) ) SQL, 'count' => 4, @@ -5305,8 +5309,57 @@ final class CoreLibsDBIOTest extends TestCase SQL, 'count' => 2, 'convert' => false, + ], + // special $$ string case + 'text string, with $ placehoders that could be seen as $$ string' => [ + 'query' => << 6, + 'convert' => false, + ], + // NOTE, in SQL heredoc we cannot write $$ strings parts + 'text string, with $ placehoders are in $$ strings' => [ + 'query' => ' + SELECT row_int + FROM table_with_primary_key + WHERE + row_varchar = $$some string$$ OR + row_varchar = $tag$some string$tag$ OR + row_varchar = $btag$some $1 string$btag$ OR + row_varchar = $btag$some $1 $subtag$ something $subtag$string$btag$ OR + row_varchar = $1 + ', + 'count' => 1, + 'convert' => false, + ], + // a text string with escaped quite + 'text string, with escaped quote' => [ + 'query' => << 2, + 'convert' => false, ] ]; + $string = <<