diff --git a/4dev/documentation/DB_Query_Params.md b/4dev/documentation/DB_Query_Params.md new file mode 100644 index 00000000..d441db37 --- /dev/null +++ b/4dev/documentation/DB_Query_Params.md @@ -0,0 +1,31 @@ +# DB Query Params ? and : to $ + +dbReturn* +dbExec + +keep +->query +->params +for reference + +## : named params + +in order for each named found replace with order number: + +```txt +:name, :foo, :bar, :name => +$1, $2, $3, $1 +``` + +```php +$query = str_replace( + [':name', ':foo', ':bar'], + ['$1', '$2', '$3'], + $query +); +``` + +## ? Params + +Foreach ? set $1 to $n and store that in new params array +in QUERY for each ? replace with matching $n diff --git a/www/lib/CoreLibs/Admin/EditBase.php b/www/lib/CoreLibs/Admin/EditBase.php index fa8033ef..d1f6e31f 100644 --- a/www/lib/CoreLibs/Admin/EditBase.php +++ b/www/lib/CoreLibs/Admin/EditBase.php @@ -44,7 +44,7 @@ class EditBase * construct form generator * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[]} $db_config db config array, mandatory + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config db config array, mandatory * @param \CoreLibs\Logging\Logging $log Logging class, null auto set * @param \CoreLibs\Language\L10n $l10n l10n language class, null auto set * @param \CoreLibs\ACL\Login $login login class for ACL settings diff --git a/www/lib/CoreLibs/DB/Extended/ArrayIO.php b/www/lib/CoreLibs/DB/Extended/ArrayIO.php index b0e6f659..40227dd1 100644 --- a/www/lib/CoreLibs/DB/Extended/ArrayIO.php +++ b/www/lib/CoreLibs/DB/Extended/ArrayIO.php @@ -55,7 +55,7 @@ class ArrayIO extends \CoreLibs\DB\IO * primary key name automatically (from array) * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[]} $db_config db connection config + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config db connection config * @param array $table_array table array config * @param string $table_name table name string * @param \CoreLibs\Logging\Logging $log Logging class diff --git a/www/lib/CoreLibs/DB/IO.php b/www/lib/CoreLibs/DB/IO.php index d5c90c97..a4f83395 100644 --- a/www/lib/CoreLibs/DB/IO.php +++ b/www/lib/CoreLibs/DB/IO.php @@ -261,6 +261,7 @@ use CoreLibs\Debug\Support; use CoreLibs\Create\Uids; use CoreLibs\Convert\Json; use CoreLibs\DB\Options\Convert; +use CoreLibs\DB\Support\ConvertPlaceholder; // below no ignore is needed if we want to use PgSql interface checks with PHP 8.0 // as main system. Currently all @var sets are written as object @@ -283,6 +284,8 @@ class IO public const ERROR_HASH_TYPE = 'adler32'; /** @var string regex to get returning with matches at position 1 */ public const REGEX_RETURNING = '/\s+returning\s+(.+\s*(?:.+\s*)+);?$/i'; + /** @var array allowed convert target for placeholder */ + public const DB_CONVERT_PLACEHOLDER_TARGET = ['pg', 'pdo']; // REGEX_SELECT // REGEX_UPDATE // REGEX INSERT @@ -326,10 +329,14 @@ class IO private string $db_ssl; /** @var array flag for converting types from settings */ private array $db_convert_type = []; + /** @var bool convert placeholders from pdo to Pg or the other way around */ + private bool $db_convert_placeholder = false; + /** @var string convert placeholders target, default is 'pg', other allowed is 'pdo' */ + private string $db_convert_placeholder_target = 'pg'; // convert type settings // 0: OFF (CONVERT_OFF) // >0: ON - // 1: convert intN/bool (CONVERT_ON) + // 1: convert int/bool (CONVERT_ON) // 2: convert json/jsonb to array (CONVERT_JSON) // 4: convert numeric/floatN to float (CONVERT_NUMERIC) // 8: convert bytea to string data (CONVERT_BYTEA) @@ -406,7 +413,7 @@ class IO * and failure set on failed connection * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[]} $db_config DB configuration array + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config DB configuration array * @param \CoreLibs\Logging\Logging $log Logging class * @throws \RuntimeException If no DB connection can be established on launch */ @@ -441,8 +448,7 @@ class IO '17' => 'All dbReturn* methods work only with SELECT statements, ' . 'please use dbExec for everything else', '18' => 'Query not found in cache. Nothing has been reset', - '19' => 'Wrong PK name given or no PK name given at all, can\'t ' - . 'get Insert ID', + '19' => 'Wrong PK name given or no PK name given at all, can\'t get Insert ID', '20' => 'Found given Prepare Statement Name in array, ' . 'Query not prepared, will use existing one', '21' => 'Query Prepare failed', @@ -472,7 +478,9 @@ class IO '101' => 'Statement name empty for get prepare cursor', '102' => 'Key empty for get prepare cursir', '103' => 'No prepared cursor with this name', - '104' => 'No Key with this name in the prepared cursor array' + '104' => 'No Key with this name in the prepared cursor array', + // abort on Placeholder convert + '200' => 'Cannot have named, question mark or numbered placeholders in the same query', ]; // load the core DB functions wrapper class @@ -507,7 +515,7 @@ class IO * Setup DB config and options * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[]} $db_config + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config * @return bool */ private function __setConfigOptions(array $db_config): bool @@ -549,6 +557,16 @@ class IO $this->db_convert_type[] = $db_convert_type; $this->__setConvertType($db_convert_type); } + // set placeholder convert flag and target + if (isset($db_config['db_convert_placeholder']) && is_bool($db_config['db_convert_placeholder'])) { + $this->db_convert_placeholder = $db_config['db_convert_placeholder']; + } + if ( + isset($db_config['db_convert_placeholder_target']) && + in_array($db_config['db_convert_placeholder_target'], self::DB_CONVERT_PLACEHOLDER_TARGET) + ) { + $this->db_convert_placeholder_target = $db_config['db_convert_placeholder_target']; + } // return status true: ok, false: options error return true; @@ -1403,6 +1421,14 @@ class IO } // import protection, hash needed $query_hash = $this->dbGetQueryHash($this->query, $this->params); + // QUERY PARAMS: run query params check and rewrite + if ($this->dbGetConvertPlaceholder() === true) { + $convert = ConvertPlaceholder::convertPlaceholderInQuery( + $query, + $params, + $this->dbGetConvertPlaceholderTarget() + ); + } // $this->debug('DB IO', 'Q: ' . $this->query . ', RETURN: ' . $this->returning_id); // for DEBUG, only on first time ;) @@ -2248,6 +2274,15 @@ class IO $this->__dbError(17, false, $this->cursor_ext[$query_hash]['query']); return false; } + // QUERY PARAMS: run query params check and rewrite + if ($this->dbGetConvertPlaceholder() === true) { + $convert = ConvertPlaceholder::convertPlaceholderInQuery( + $query, + $params, + $this->dbGetConvertPlaceholderTarget() + ); + } + // set the query parameters $this->cursor_ext[$query_hash]['params'] = $params; // check if params count matches @@ -2540,7 +2575,7 @@ class IO ): \PgSql\Result|false { $this->__dbErrorReset(); // prepare and check if we can actually run it - if ($this->__dbPrepareExec($query, $params, $pk_name) === false) { + if (($query_hash = $this->__dbPrepareExec($query, $params, $pk_name)) === false) { // bail if no query hash set return false; } @@ -3461,7 +3496,7 @@ class IO } /** - * Undocumented function + * convert db values (set) * * @param Convert $convert * @return void @@ -3472,7 +3507,7 @@ class IO } /** - * Undocumented function + * unsert convert db values flag * * @param Convert $convert * @return void @@ -3495,7 +3530,7 @@ class IO } /** - * Undocumented function + * check if a conert flag is set * * @param Convert $convert * @return bool @@ -3508,6 +3543,52 @@ class IO return false; } + /** + * Set if we want to auto convert PDO/\Pg placeholders + * + * @param bool $flag + * @return void + */ + public function dbSetConvertPlaceholder(bool $flag): void + { + $this->db_convert_placeholder = $flag; + } + + /** + * get the flag status if we want to auto convert placeholders in the query + * + * @return bool + */ + public function dbGetConvertPlaceholder(): bool + { + return $this->db_convert_placeholder; + } + + /** + * Set convert target for placeholders, returns false on error, true on ok + * + * @param string $target 'pg' or 'pdo', defined in DB_CONVERT_PLACEHOLDER_TARGET + * @return bool + */ + public function dbSetConvertPlaceholderTarget(string $target): bool + { + if (in_array($target, self::DB_CONVERT_PLACEHOLDER_TARGET)) { + $this->db_convert_placeholder_target = $target; + return true; + } + return false; + } + + /** + * Get the current placeholder convert target + * + * @return string + */ + public function dbGetConvertPlaceholderTarget(): string + { + return $this->db_convert_placeholder_target; + } + /** * set max query calls, set to -1 to disable loop * protection. this will generate a warning diff --git a/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php new file mode 100644 index 00000000..0c194dde --- /dev/null +++ b/www/lib/CoreLibs/DB/Support/ConvertPlaceholder.php @@ -0,0 +1,205 @@ + $params The parameters that are used for the query, and will be updated + * @param string $convert_to Either pdo or pg, will be converted to lower case for check + * @return array{original:array{query:string,params:array},type:''|'named'|'numbered'|'question_mark',found:int|false,matches:array,params_lookup:array,query:string,params:array} + * @throws \OutOfRangeException 200 + */ + public static function convertPlaceholderInQuery( + string $query, + array $params, + string $convert_to = 'pg' + ): array { + $convert_to = strtolower($convert_to); + $matches = []; + $pattern = '/' + // prefix string part, must match towards + . '(?:\'.*?\')?\s*(?:\?\?|[(=,])\s*' + // match for replace part + . '(?:' + // digit -> ignore + . '\d+|' + // other string -> ignore + . '(?:\'.*?\')|' + // :name named part (PDO) + . '(:\w+)|' + // ? question mark part (PDO) + . '(?:(?:\?\?)?\s*(\?{1}))|' + // $n numbered part (\PG php) + . '(\$[1-9]{1}(?:[0-9]{1,})?)' + // end match + . ')' + // single line -> add line break to matches in "." + . '/s'; + // matches: + // 1: :named + // 2: ? question mark + // 3: $n numbered + $found = preg_match_all($pattern, $query, $matches, PREG_UNMATCHED_AS_NULL); + /** @var array 1: named */ + $named_matches = array_filter($matches[1]); + /** @var array 2: open ? */ + $qmark_matches = array_filter($matches[2]); + /** @var array 3: $n matches */ + $numbered_matches = array_filter($matches[3]); + // throw if mixed + if (count($named_matches) && count($qmark_matches) && count($numbered_matches)) { + throw new \OutOfRangeException('Cannot have named, question mark and numbered in the same query', 200); + } + // rebuild + $matches_return = []; + $type = ''; + $query_new = ''; + $params_new = []; + $params_lookup = []; + if (count($named_matches) && $convert_to == 'pg') { + $type = 'named'; + $matches_return = $named_matches; + // only check for :named + $pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(:\w+))/s'; + // 0: full + // 1: pre part + // 2: keep part UNLESS '3' is set + // 3: replace part :named + $pos = 0; + $query_new = preg_replace_callback( + $pattern_replace, + function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) { + // only count up if $match[3] is not yet in lookup table + if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + $pos++; + $params_lookup[$matches[3]] = '$' . $pos; + $params_new[] = $params[$matches[3]] ?? + throw new \RuntimeException( + 'Cannot lookup ' . $matches[3] . ' 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 + ) + ); + }, + $query + ); + } elseif (count($qmark_matches) && $convert_to == 'pg') { + $type = 'question_mark'; + $matches_return = $qmark_matches; + // order and data stays the same + $params_new = $params; + // only check for ? + $pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(?:(?:\?\?)?\s*(\?{1})))/s'; + // 0: full + // 1: pre part + // 2: keep part UNLESS '3' is set + // 3: replace part ? + $pos = 0; + $query_new = preg_replace_callback( + $pattern_replace, + function ($matches) use (&$pos, &$params_lookup) { + // only count pos up for actual replacements we will do + if (!empty($matches[3])) { + $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 + ); + }, + $query + ); + // for each ?:DTN: -> replace with $1 ... $n, any remaining :DTN: remove + } elseif (count($numbered_matches) && $convert_to == 'pdo') { + // convert numbered to named + $type = 'numbered'; + $matches_return = $numbered_matches; + // only check for $n + $pattern_replace = '/((?:\'.*?\')?\s*(?:\?\?|[(=,])\s*)(\d+|(?:\'.*?\')|(\$[1-9]{1}(?:[0-9]{1,})?))/s'; + // 0: full + // 1: pre part + // 2: keep part UNLESS '3' is set + // 3: replace part $numbered + $pos = 0; + $query_new = preg_replace_callback( + $pattern_replace, + function ($matches) use (&$pos, &$params_new, &$params_lookup, $params) { + // only count up if $match[3] is not yet in lookup table + if (!empty($matches[3]) && empty($params_lookup[$matches[3]])) { + $pos++; + $params_lookup[$matches[3]] = ':' . $pos . '_named'; + $params_new[] = $params[($pos - 1)] ?? + throw new \RuntimeException( + 'Cannot lookup ' . ($pos - 1) . ' in params list', + 220 + ); + } + // 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 + ) + ); + }, + $query + ); + } + // return, old query is always set + return [ + // original + 'original' => [ + 'query' => $query, + 'params' => $params, + ], + // type found, empty if nothing was done + 'type' => $type, + // int|null: found, not found; false: problem + 'found' => $found, + 'matches' => $matches_return, + // old to new lookup check + 'params_lookup' => $params_lookup, + // new + 'query' => $query_new ?? '', + 'params' => $params_new, + ]; + } +} + +// __END__ diff --git a/www/lib/CoreLibs/Output/Form/Generate.php b/www/lib/CoreLibs/Output/Form/Generate.php index a09eab66..d082a8d3 100644 --- a/www/lib/CoreLibs/Output/Form/Generate.php +++ b/www/lib/CoreLibs/Output/Form/Generate.php @@ -310,7 +310,7 @@ class Generate * construct form generator * * phpcs:ignore - * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[]} $db_config db config array, mandatory + * @param array{db_name:string,db_user:string,db_pass:string,db_host:string,db_port:int,db_schema:string,db_encoding:string,db_type:string,db_ssl:string,db_convert_type?:string[],db_convert_placeholder?:bool,db_convert_placeholder_target?:string} $db_config db config array, mandatory * @param \CoreLibs\Logging\Logging $log Logging class * @param \CoreLibs\Language\L10n $l10n l10n language class * @param array $login_acl Login ACL array, diff --git a/www/vendor/composer/autoload_classmap.php b/www/vendor/composer/autoload_classmap.php index 074698d6..4de0d9cc 100644 --- a/www/vendor/composer/autoload_classmap.php +++ b/www/vendor/composer/autoload_classmap.php @@ -42,6 +42,7 @@ return array( 'CoreLibs\\DB\\Options\\Convert' => $baseDir . '/lib/CoreLibs/DB/Options/Convert.php', 'CoreLibs\\DB\\SQL\\Interface\\SqlFunctions' => $baseDir . '/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php', 'CoreLibs\\DB\\SQL\\PgSQL' => $baseDir . '/lib/CoreLibs/DB/SQL/PgSQL.php', + 'CoreLibs\\DB\\Support\\ConvertPlaceholder' => $baseDir . '/lib/CoreLibs/DB/Support/ConvertPlaceholder.php', 'CoreLibs\\Debug\\FileWriter' => $baseDir . '/lib/CoreLibs/Debug/FileWriter.php', 'CoreLibs\\Debug\\Logging' => $baseDir . '/lib/CoreLibs/Debug/Logging.php', 'CoreLibs\\Debug\\LoggingLegacy' => $baseDir . '/lib/CoreLibs/Debug/LoggingLegacy.php', diff --git a/www/vendor/composer/autoload_static.php b/www/vendor/composer/autoload_static.php index d7260d05..01b288b7 100644 --- a/www/vendor/composer/autoload_static.php +++ b/www/vendor/composer/autoload_static.php @@ -70,6 +70,7 @@ class ComposerStaticInit10fe8fe2ec4017b8644d2b64bcf398b9 'CoreLibs\\DB\\Options\\Convert' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/Options/Convert.php', 'CoreLibs\\DB\\SQL\\Interface\\SqlFunctions' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/SQL/Interface/SqlFunctions.php', 'CoreLibs\\DB\\SQL\\PgSQL' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/SQL/PgSQL.php', + 'CoreLibs\\DB\\Support\\ConvertPlaceholder' => __DIR__ . '/../..' . '/lib/CoreLibs/DB/Support/ConvertPlaceholder.php', 'CoreLibs\\Debug\\FileWriter' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/FileWriter.php', 'CoreLibs\\Debug\\Logging' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/Logging.php', 'CoreLibs\\Debug\\LoggingLegacy' => __DIR__ . '/../..' . '/lib/CoreLibs/Debug/LoggingLegacy.php',