diff --git a/UPGRADING b/UPGRADING index 99280c5f4c946..14dba5243d769 100644 --- a/UPGRADING +++ b/UPGRADING @@ -91,6 +91,9 @@ PHP 8.1 UPGRADE NOTES for details of behavior changes and how to explicitly set this attribute. To keep the old behavior, use mysqli_report(MYSQLI_REPORT_OFF); RFC: https://wiki.php.net/rfc/mysqli_default_errmode + . Classes extending mysqli_stmt::execute() will be required to specify the + additional parameter now. + RFC: https://wiki.php.net/rfc/mysqli_bind_in_execute - MySQLnd: . The mysqlnd.fetch_copy_data ini setting has been removed. However, this @@ -218,6 +221,9 @@ PHP 8.1 UPGRADE NOTES used to specify a directory from which files are allowed to be loaded. It is only meaningful if mysqli.allow_local_infile is not enabled, as all directories are allowed in that case. + . Binding in execute has been added to mysqli prepared statements. + Parameters can now be passed to mysqli_stmt::execute as an array. + RFC: https://wiki.php.net/rfc/mysqli_bind_in_execute - PDO MySQL: . The PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY attribute has been added, which diff --git a/ext/mysqli/mysqli.stub.php b/ext/mysqli/mysqli.stub.php index 1f4f5107ff0fe..56673b3b54bfe 100644 --- a/ext/mysqli/mysqli.stub.php +++ b/ext/mysqli/mysqli.stub.php @@ -505,7 +505,7 @@ public function data_seek(int $offset) {} * @return bool * @alias mysqli_stmt_execute */ - public function execute() {} + public function execute(?array $params = null) {} /** * @return bool|null @@ -642,10 +642,10 @@ function mysqli_error(mysqli $mysql): string {} function mysqli_error_list(mysqli $mysql): array {} -function mysqli_stmt_execute(mysqli_stmt $statement): bool {} +function mysqli_stmt_execute(mysqli_stmt $statement, ?array $params = null): bool {} /** @alias mysqli_stmt_execute */ -function mysqli_execute(mysqli_stmt $statement): bool {} +function mysqli_execute(mysqli_stmt $statement, ?array $params = null): bool {} function mysqli_fetch_field(mysqli_result $result): object|false {} diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index 728e785478a01..88fbb1e3aeb80 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -811,15 +811,57 @@ PHP_FUNCTION(mysqli_stmt_execute) { MY_STMT *stmt; zval *mysql_stmt; + HashTable *input_params = NULL; #ifndef MYSQLI_USE_MYSQLND unsigned int i; #endif - if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O", &mysql_stmt, mysqli_stmt_class_entry) == FAILURE) { + if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O|h!", &mysql_stmt, mysqli_stmt_class_entry, &input_params) == FAILURE) { RETURN_THROWS(); } MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID); + // bind-in-execute + if (input_params) { +#if defined(MYSQLI_USE_MYSQLND) + zval *tmp; + unsigned int index; + unsigned int hash_num_elements; + unsigned int param_count; + MYSQLND_PARAM_BIND *params; + + if (!zend_array_is_list(input_params)) { + zend_argument_value_error(ERROR_ARG_POS(2), "must be a list array"); + RETURN_THROWS(); + } + + hash_num_elements = zend_hash_num_elements(input_params); + param_count = mysql_stmt_param_count(stmt->stmt); + if (hash_num_elements != param_count) { + zend_argument_value_error(ERROR_ARG_POS(2), "must consist of exactly %d elements, %d present", param_count, hash_num_elements); + RETURN_THROWS(); + } + + params = mysqlnd_stmt_alloc_param_bind(stmt->stmt); + ZEND_ASSERT(params); + + index = 0; + ZEND_HASH_FOREACH_VAL(input_params, tmp) { + ZVAL_COPY_VALUE(¶ms[index].zv, tmp); + params[index].type = MYSQL_TYPE_VAR_STRING; + index++; + } ZEND_HASH_FOREACH_END(); + + if (mysqlnd_stmt_bind_param(stmt->stmt, params)) { + MYSQLI_REPORT_STMT_ERROR(stmt->stmt); + RETVAL_FALSE; + } +#else + zend_argument_count_error("Binding parameters in execute is not supported with libmysqlclient"); + RETURN_THROWS(); +#endif + } + #ifndef MYSQLI_USE_MYSQLND if (stmt->param.var_cnt) { int j; diff --git a/ext/mysqli/mysqli_arginfo.h b/ext/mysqli/mysqli_arginfo.h index b01fa82a7933c..4fab978d90d79 100644 --- a/ext/mysqli/mysqli_arginfo.h +++ b/ext/mysqli/mysqli_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 1c01e60c65f87e4f59435c3712296137d265dfdc */ + * Stub hash: 3f3d19da5a2b7c8edc6dba0fde6215b93d10bb32 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_mysqli_affected_rows, 0, 1, MAY_BE_LONG|MAY_BE_STRING) ZEND_ARG_OBJ_INFO(0, mysql, mysqli, 0) @@ -71,6 +71,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_execute, 0, 1, _IS_BOOL, 0) ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null") ZEND_END_ARG_INFO() #define arginfo_mysqli_execute arginfo_mysqli_stmt_execute @@ -300,7 +301,9 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_bind_result, 0, 1, _ ZEND_ARG_VARIADIC_TYPE_INFO(1, vars, IS_MIXED, 0) ZEND_END_ARG_INFO() -#define arginfo_mysqli_stmt_close arginfo_mysqli_stmt_execute +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_close, 0, 1, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0) +ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_data_seek, 0, 2, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0) @@ -351,7 +354,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_more_results, 0, 1, ZEND_END_ARG_INFO() #endif -#define arginfo_mysqli_stmt_next_result arginfo_mysqli_stmt_execute +#define arginfo_mysqli_stmt_next_result arginfo_mysqli_stmt_close #define arginfo_mysqli_stmt_num_rows arginfo_mysqli_stmt_affected_rows @@ -362,7 +365,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_prepare, 0, 2, _IS_B ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0) ZEND_END_ARG_INFO() -#define arginfo_mysqli_stmt_reset arginfo_mysqli_stmt_execute +#define arginfo_mysqli_stmt_reset arginfo_mysqli_stmt_close ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_mysqli_stmt_result_metadata, 0, 1, mysqli_result, MAY_BE_FALSE) ZEND_ARG_OBJ_INFO(0, statement, mysqli_stmt, 0) @@ -374,7 +377,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mysqli_stmt_send_long_data, 0, 3 ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) ZEND_END_ARG_INFO() -#define arginfo_mysqli_stmt_store_result arginfo_mysqli_stmt_execute +#define arginfo_mysqli_stmt_store_result arginfo_mysqli_stmt_close #define arginfo_mysqli_stmt_sqlstate arginfo_mysqli_stmt_error @@ -640,7 +643,9 @@ ZEND_END_ARG_INFO() #define arginfo_class_mysqli_stmt_data_seek arginfo_class_mysqli_result_data_seek -#define arginfo_class_mysqli_stmt_execute arginfo_class_mysqli_character_set_name +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_mysqli_stmt_execute, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() #define arginfo_class_mysqli_stmt_fetch arginfo_class_mysqli_character_set_name diff --git a/ext/mysqli/tests/mysqli_stmt_execute_bind.phpt b/ext/mysqli/tests/mysqli_stmt_execute_bind.phpt new file mode 100644 index 0000000000000..e99203eeaa37e --- /dev/null +++ b/ext/mysqli/tests/mysqli_stmt_execute_bind.phpt @@ -0,0 +1,146 @@ +--TEST-- +mysqli_stmt_execute() - bind in execute +--SKIPIF-- + +--FILE-- +prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$stmt->bind_param('sss', ...[&$abc, 42, $id]); +$stmt->execute(); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +$stmt = null; + +// 1. same as the control case, but skipping the middle-man (bind_param) +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$stmt->execute([&$abc, 42, $id]); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +$stmt = null; + +// 2. param number has to match - missing 1 parameter +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute([&$abc, 42]); +} catch (ValueError $e) { + echo '[001] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 3. Too many parameters +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute([&$abc, null, $id, 24]); +} catch (ValueError $e) { + echo '[002] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 4. param number has to match - missing all parameters +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute([]); +} catch (ValueError $e) { + echo '[003] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 5. param number has to match - missing argument to execute() +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute(); +} catch (mysqli_sql_exception $e) { + echo '[004] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 6. wrong argument to execute() +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute(42); +} catch (TypeError $e) { + echo '[005] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 7. objects are not arrays and are not accepted +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute((object)[&$abc, 42, $id]); +} catch (TypeError $e) { + echo '[006] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 8. arrays by reference work too +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$arr = [&$abc, 42, $id]; +$arr2 = &$arr; +$stmt->execute($arr2); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +$stmt = null; + +// 9. no placeholders in statement. nothing to bind in an empty array +$stmt = $link->prepare('SELECT label FROM test WHERE id=1'); +$stmt->execute([]); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a']); +$stmt = null; + +// 10. once bound the values are persisted. Just like in PDO +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$stmt->execute(['abc', 42, $id]); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +$stmt->execute(); // no argument here. Values are already bound +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +try { + $stmt->execute([]); // no params here. PDO doesn't throw an error, but mysqli does +} catch (ValueError $e) { + echo '[007] '.$e->getMessage()."\n"; +} +$stmt = null; + +// 11. mixing binding styles not possible. Also, NULL should stay NULL when bound as string +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$stmt->bind_param('sss', ...['abc', 42, null]); +$stmt->execute([null, null, $id]); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>null, 'num' => null]); +$stmt = null; + +// 12. Only list arrays are allowed +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute(['A'=>'abc', 2=>42, null=>$id]); +} catch (ValueError $e) { + echo '[008] '.$e->getMessage()."\n"; +} +$stmt = null; + + +mysqli_close($link); +?> +--CLEAN-- + +--EXPECT-- +[001] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 2 present +[002] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 4 present +[003] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 0 present +[004] No data supplied for parameters in prepared statement +[005] mysqli_stmt::execute(): Argument #1 ($params) must be of type ?array, int given +[006] mysqli_stmt::execute(): Argument #1 ($params) must be of type ?array, stdClass given +[007] mysqli_stmt::execute(): Argument #1 ($params) must consist of exactly 3 elements, 0 present +[008] mysqli_stmt::execute(): Argument #1 ($params) must be a list array diff --git a/ext/mysqli/tests/mysqli_stmt_execute_bind_libmysql.phpt b/ext/mysqli/tests/mysqli_stmt_execute_bind_libmysql.phpt new file mode 100644 index 0000000000000..f7ddb05c38962 --- /dev/null +++ b/ext/mysqli/tests/mysqli_stmt_execute_bind_libmysql.phpt @@ -0,0 +1,44 @@ +--TEST-- +mysqli_stmt_execute() - bind in execute - not supported with libmysql +--SKIPIF-- + +--FILE-- +prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +$stmt->bind_param('sss', ...[&$abc, 42, $id]); +$stmt->execute(); +assert($stmt->get_result()->fetch_assoc() === ['label'=>'a', 'anon'=>'abc', 'num' => '42']); +$stmt = null; + +// 1. same as the control case, but skipping the middle-man (bind_param) +$stmt = $link->prepare('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?'); +try { + $stmt->execute([&$abc, 42, $id]); +} catch (ArgumentCountError $e) { + echo '[001] '.$e->getMessage()."\n"; +} +$stmt = null; + +mysqli_close($link); +?> +--CLEAN-- + +--EXPECT-- +[001] Binding parameters in execute is not supported with libmysqlclient