diff --git a/ext/mysqli/mysqli.stub.php b/ext/mysqli/mysqli.stub.php index 288882a2a50ad..e4f03705bf054 100644 --- a/ext/mysqli/mysqli.stub.php +++ b/ext/mysqli/mysqli.stub.php @@ -208,6 +208,11 @@ public function debug(string $options) {} // TODO make return type void */ public function get_charset(): ?object {} + /** + * @alias mysqli_execute_query + */ + public function execute_query(string $query, ?array $params = null): mysqli_result|bool {} + /** * @tentative-return-type * @alias mysqli_get_client_info @@ -793,6 +798,8 @@ function mysqli_stmt_execute(mysqli_stmt $statement, ?array $params = null): boo /** @alias mysqli_stmt_execute */ function mysqli_execute(mysqli_stmt $statement, ?array $params = null): bool {} +function mysqli_execute_query(mysqli $mysql, string $query, ?array $params = null): mysqli_result|bool {} + /** @refcount 1 */ function mysqli_fetch_field(mysqli_result $result): object|false {} diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index fb0f6b43d2a5c..99fb0154b4bfa 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -476,6 +476,146 @@ PHP_FUNCTION(mysqli_stmt_execute) } /* }}} */ +void close_stmt_and_copy_errors(MY_STMT *stmt, MY_MYSQL *mysql) +{ + /* mysql_stmt_close() clears errors, so we have to store them temporarily */ + MYSQLND_ERROR_INFO error_info = *stmt->stmt->data->error_info; + stmt->stmt->data->error_info->error_list.head = NULL; + stmt->stmt->data->error_info->error_list.tail = NULL; + stmt->stmt->data->error_info->error_list.count = 0; + + /* we also remember affected_rows which gets cleared too */ + uint64_t affected_rows = mysql->mysql->data->upsert_status->affected_rows; + + mysqli_stmt_close(stmt->stmt, false); + stmt->stmt = NULL; + php_clear_stmt_bind(stmt); + + /* restore error messages, but into the mysql object */ + zend_llist_clean(&mysql->mysql->data->error_info->error_list); + *mysql->mysql->data->error_info = error_info; + mysql->mysql->data->upsert_status->affected_rows = affected_rows; +} + +PHP_FUNCTION(mysqli_execute_query) +{ + MY_MYSQL *mysql; + MY_STMT *stmt; + char *query = NULL; + size_t query_len; + zval *mysql_link; + HashTable *input_params = NULL; + MYSQL_RES *result; + MYSQLI_RESOURCE *mysqli_resource; + + if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "Os|h!", &mysql_link, mysqli_link_class_entry, &query, &query_len, &input_params) == FAILURE) { + RETURN_THROWS(); + } + MYSQLI_FETCH_RESOURCE_CONN(mysql, mysql_link, MYSQLI_STATUS_VALID); + + stmt = (MY_STMT *)ecalloc(1,sizeof(MY_STMT)); + + if (!(stmt->stmt = mysql_stmt_init(mysql->mysql))) { + MYSQLI_REPORT_MYSQL_ERROR(mysql->mysql); + efree(stmt); + RETURN_FALSE; + } + + if (FAIL == mysql_stmt_prepare(stmt->stmt, query, query_len)) { + MYSQLI_REPORT_STMT_ERROR(stmt->stmt); + + close_stmt_and_copy_errors(stmt, mysql); + RETURN_FALSE; + } + + /* The bit below, which is copied from mysqli_prepare, is needed for bad index exceptions */ + /* don't initialize stmt->query with NULL, we ecalloc()-ed the memory */ + /* Get performance boost if reporting is switched off */ + if (query_len && (MyG(report_mode) & MYSQLI_REPORT_INDEX)) { + stmt->query = estrdup(query); + } + + // bind-in-execute + // It's very similar to the mysqli_stmt::execute, but it uses different error handling + if (input_params) { + 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)) { + mysqli_stmt_close(stmt->stmt, false); + stmt->stmt = NULL; + efree(stmt); + zend_argument_value_error(ERROR_ARG_POS(3), "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) { + mysqli_stmt_close(stmt->stmt, false); + stmt->stmt = NULL; + efree(stmt); + zend_argument_value_error(ERROR_ARG_POS(3), "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)) { + close_stmt_and_copy_errors(stmt, mysql); + RETURN_FALSE; + } + + } + + if (mysql_stmt_execute(stmt->stmt)) { + MYSQLI_REPORT_STMT_ERROR(stmt->stmt); + + if (MyG(report_mode) & MYSQLI_REPORT_INDEX) { + php_mysqli_report_index(stmt->query, mysqli_stmt_server_status(stmt->stmt)); + } + + close_stmt_and_copy_errors(stmt, mysql); + RETURN_FALSE; + } + + if (!mysql_stmt_field_count(stmt->stmt)) { + /* no result set - not a SELECT */ + close_stmt_and_copy_errors(stmt, mysql); + RETURN_TRUE; + } + + if (MyG(report_mode) & MYSQLI_REPORT_INDEX) { + php_mysqli_report_index(stmt->query, mysqli_stmt_server_status(stmt->stmt)); + } + + /* get result */ + if (!(result = mysqlnd_stmt_get_result(stmt->stmt))) { + MYSQLI_REPORT_STMT_ERROR(stmt->stmt); + + close_stmt_and_copy_errors(stmt, mysql); + RETURN_FALSE; + } + + mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE)); + mysqli_resource->ptr = (void *)result; + mysqli_resource->status = MYSQLI_STATUS_VALID; + MYSQLI_RETVAL_RESOURCE(mysqli_resource, mysqli_result_class_entry); + + close_stmt_and_copy_errors(stmt, mysql); +} + /* {{{ mixed mysqli_stmt_fetch_mysqlnd */ void mysqli_stmt_fetch_mysqlnd(INTERNAL_FUNCTION_PARAMETERS) { @@ -1188,9 +1328,7 @@ PHP_FUNCTION(mysqli_prepare) /* don't initialize stmt->query with NULL, we ecalloc()-ed the memory */ /* Get performance boost if reporting is switched off */ if (stmt->stmt && query_len && (MyG(report_mode) & MYSQLI_REPORT_INDEX)) { - stmt->query = (char *)emalloc(query_len + 1); - memcpy(stmt->query, query, query_len); - stmt->query[query_len] = '\0'; + stmt->query = estrdup(query); } /* don't join to the previous if because it won't work if mysql_stmt_prepare_fails */ diff --git a/ext/mysqli/mysqli_arginfo.h b/ext/mysqli/mysqli_arginfo.h index 6e0a7f5bb1545..8935946bfeb4a 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: e528bb1e05a85d3d764272c5f3f4256d2608da6c */ + * Stub hash: 2dae4302d825a7f5da3c4e00ce87aebc5502a8af */ 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) @@ -78,6 +78,12 @@ ZEND_END_ARG_INFO() #define arginfo_mysqli_execute arginfo_mysqli_stmt_execute +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_mysqli_execute_query, 0, 2, mysqli_result, MAY_BE_BOOL) + ZEND_ARG_OBJ_INFO(0, mysql, mysqli, 0) + ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_mysqli_fetch_field, 0, 1, MAY_BE_OBJECT|MAY_BE_FALSE) ZEND_ARG_OBJ_INFO(0, result, mysqli_result, 0) ZEND_END_ARG_INFO() @@ -458,6 +464,11 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_mysqli_get_charset, 0, 0, IS_OBJECT, 1) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_mysqli_execute_query, 0, 1, mysqli_result, MAY_BE_BOOL) + ZEND_ARG_TYPE_INFO(0, query, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, params, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + #define arginfo_class_mysqli_get_client_info arginfo_class_mysqli_character_set_name ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_mysqli_get_connection_stats, 0, 0, IS_ARRAY, 0) @@ -713,6 +724,7 @@ ZEND_FUNCTION(mysqli_errno); ZEND_FUNCTION(mysqli_error); ZEND_FUNCTION(mysqli_error_list); ZEND_FUNCTION(mysqli_stmt_execute); +ZEND_FUNCTION(mysqli_execute_query); ZEND_FUNCTION(mysqli_fetch_field); ZEND_FUNCTION(mysqli_fetch_fields); ZEND_FUNCTION(mysqli_fetch_field_direct); @@ -827,6 +839,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(mysqli_error_list, arginfo_mysqli_error_list) ZEND_FE(mysqli_stmt_execute, arginfo_mysqli_stmt_execute) ZEND_FALIAS(mysqli_execute, mysqli_stmt_execute, arginfo_mysqli_execute) + ZEND_FE(mysqli_execute_query, arginfo_mysqli_execute_query) ZEND_FE(mysqli_fetch_field, arginfo_mysqli_fetch_field) ZEND_FE(mysqli_fetch_fields, arginfo_mysqli_fetch_fields) ZEND_FE(mysqli_fetch_field_direct, arginfo_mysqli_fetch_field_direct) @@ -935,6 +948,7 @@ static const zend_function_entry class_mysqli_methods[] = { ZEND_ME_MAPPING(dump_debug_info, mysqli_dump_debug_info, arginfo_class_mysqli_dump_debug_info, ZEND_ACC_PUBLIC) ZEND_ME_MAPPING(debug, mysqli_debug, arginfo_class_mysqli_debug, ZEND_ACC_PUBLIC) ZEND_ME_MAPPING(get_charset, mysqli_get_charset, arginfo_class_mysqli_get_charset, ZEND_ACC_PUBLIC) + ZEND_ME_MAPPING(execute_query, mysqli_execute_query, arginfo_class_mysqli_execute_query, ZEND_ACC_PUBLIC) ZEND_ME_MAPPING(get_client_info, mysqli_get_client_info, arginfo_class_mysqli_get_client_info, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) ZEND_ME_MAPPING(get_connection_stats, mysqli_get_connection_stats, arginfo_class_mysqli_get_connection_stats, ZEND_ACC_PUBLIC) ZEND_ME_MAPPING(get_server_info, mysqli_get_server_info, arginfo_class_mysqli_get_server_info, ZEND_ACC_PUBLIC) diff --git a/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt b/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt index 793d5b1150835..3e5d439a538fe 100644 --- a/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt +++ b/ext/mysqli/tests/mysqli_class_mysqli_interface.phpt @@ -29,6 +29,7 @@ require_once('skipifconnectfailure.inc'); 'connect' => true, 'dump_debug_info' => true, 'escape_string' => true, + 'execute_query' => true, 'get_charset' => true, 'get_client_info' => true, 'get_server_info' => true, diff --git a/ext/mysqli/tests/mysqli_execute_query.phpt b/ext/mysqli/tests/mysqli_execute_query.phpt new file mode 100644 index 0000000000000..64b7fbcf3e770 --- /dev/null +++ b/ext/mysqli/tests/mysqli_execute_query.phpt @@ -0,0 +1,97 @@ +--TEST-- +mysqli_execute_query() +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +fetch_assoc() === ['a' => '42', 'b' => 'foo', 'c' => null]); + +// OO style +if (!($tmp = $link->execute_query("SELECT ? AS a, ? AS b, ? AS c", [42, "foo", null]))) { + printf("[004] [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +assert($tmp->fetch_assoc() === ['a' => '42', 'b' => 'foo', 'c' => null]); + +// prepare error +if (!($tmp = $link->execute_query("some random gibberish", [1, "foo"]))) { + printf("[005] [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +// stmt error - duplicate key +if (!$link->execute_query("INSERT INTO test(id, label) VALUES (?, ?)", [1, "foo"])) { + printf("[006] [%d] %s\n", mysqli_errno($link), mysqli_error($link)); +} + +// successful update returns true +if (!($tmp = $link->execute_query("UPDATE test SET label=? WHERE id=?", ["baz", 1]))) { + printf("[007] Expecting true, got %s/%s\n", gettype($tmp), $tmp); +} +if ($link->affected_rows <= 0) { + printf("[008] Expecting positive non-zero integer for affected_rows, got %s/%s\n", gettype($link->affected_rows), $link->affected_rows); +} + +mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + +// check if value error is properly reported +try { + $link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo', 42]); +} catch (ValueError $e) { + echo '[009] '.$e->getMessage()."\n"; +} +try { + $link->execute_query('SELECT label, ? AS anon, ? AS num FROM test WHERE id=?', ['foo' => 42]); +} catch (ValueError $e) { + echo '[010] '.$e->getMessage()."\n"; +} + +// check if insert_id is copied +$link->execute_query("ALTER TABLE test MODIFY id INT NOT NULL AUTO_INCREMENT"); +$link->execute_query("INSERT INTO test(label) VALUES (?)", ["foo"]); +if ($link->insert_id <= 0) { + printf("[011] Expecting positive non-zero integer for insert_id, got %s/%s\n", gettype($link->insert_id), $link->insert_id); +} + +// bad index +mysqli_report(MYSQLI_REPORT_ALL); +try { + $link->execute_query("SELECT id FROM test WHERE label = ?", ["foo"]); +} catch (mysqli_sql_exception $e) { + echo '[012] '.$e->getMessage()."\n"; +} + +print "done!"; +?> +--CLEAN-- + +--EXPECTF-- +[005] [1064] You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'some random gibberish' at line 1 +[006] [1062] Duplicate entry '1' for key '%s' +[009] mysqli::execute_query(): Argument #2 ($params) must consist of exactly 3 elements, 2 present +[010] mysqli::execute_query(): Argument #2 ($params) must be a list array +[012] No index used in query/prepared statement SELECT id FROM test WHERE label = ? +done!