From 3826f1a522af29b176ee0d375de2faafc8425396 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Tue, 2 Nov 2021 08:12:09 -0700 Subject: [PATCH] PDO::ATTR_EMULATE_PREPARES at the connection level (#1324) --- source/pdo_sqlsrv/pdo_dbh.cpp | 22 ++- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 1 + ...ment_bindParam_output_emulate_prepare.phpt | 55 ++++--- .../pdo_sqlsrv/pdo_1310_null_varchar.phpt | 4 +- .../pdo_1320_pdo_emulate_prepare.phpt | 62 ++++++++ .../pdo_prepare_emulatePrepare_unicode2.phpt | 137 ++++++++++++++++++ ...dostatement_fetchmode_emulate_prepare.phpt | 10 -- .../sqlsrv/srv_1310_null_varchar.phpt | 3 +- 8 files changed, 247 insertions(+), 47 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdo_1320_pdo_emulate_prepare.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode2.phpt diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index a2e19e1b..bddbd542 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -542,7 +542,8 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo fetch_datetime( false ), format_decimals( false ), decimal_places( NO_CHANGE_DECIMAL_PLACES ), - use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED) + use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED), + emulate_prepare(false) { if( client_buffer_max_size < 0 ) { client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT; @@ -734,7 +735,8 @@ bool pdo_sqlsrv_dbh_prepare(_Inout_ pdo_dbh_t *dbh, _In_ zend_string *sql_zstr, // assign the methods for the statement object. This is necessary even if the // statement fails so the user can retrieve the error information. stmt->methods = &pdo_sqlsrv_stmt_methods; - stmt->supports_placeholders = PDO_PLACEHOLDER_POSITIONAL; // we support parameterized queries with ?, not names + // if not emulate_prepare, we support parameterized queries with ?, not names + stmt->supports_placeholders = (driver_dbh->emulate_prepare) ? PDO_PLACEHOLDER_NONE : PDO_PLACEHOLDER_POSITIONAL; // the statement options may override this later // Initialize the options array to be passed to the core layer ALLOC_HASHTABLE( pdo_stmt_options_ht ); @@ -1288,8 +1290,15 @@ bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_READ_ONLY_DBH_ATTR ); } - // Statement level only case PDO_ATTR_EMULATE_PREPARES: + { + driver_dbh->emulate_prepare = zend_is_true(val); + if (driver_dbh->emulate_prepare && driver_dbh->ce_option.enabled) { + THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED); + } + } + break; + // Statement level only case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: case SQLSRV_ATTR_DATA_CLASSIFICATION: @@ -1362,8 +1371,13 @@ int pdo_sqlsrv_dbh_get_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout_ #endif } - // Statement level only case PDO_ATTR_EMULATE_PREPARES: + { + ZVAL_BOOL(return_value, driver_dbh->emulate_prepare); + break; + } + + // Statement level only case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: case SQLSRV_ATTR_DATA_CLASSIFICATION: diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 7e472744..18a65acb 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -194,6 +194,7 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn { bool format_decimals; short decimal_places; short use_national_characters; + bool emulate_prepare; pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver ); }; diff --git a/test/functional/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt b/test/functional/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt index 35f4197f..bd96f469 100644 --- a/test/functional/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt +++ b/test/functional/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt @@ -1,49 +1,44 @@ --TEST-- Tests error returned when binding output parameter with emulate prepare +--DESCRIPTION-- +The test shows that the option sets in prepared statements overrides the +connection setting of PDO::ATTR_EMULATE_PREPARES --SKIPIF-- --FILE-- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); $count = 0; $query = "select ? = count(* ) from cd_info"; - $stmt = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true)); - $stmt->bindParam(1, $count, PDO::PARAM_STR, 10); - $stmt->execute(); - echo "Result: ".$count."\n"; - - $query = "select bigint_type, int_type, money_type from [test_types] where int_type < 0"; - $stmt1 = $conn->prepare($query); - $stmt1->execute(); - $row = $stmt1->fetch(PDO::FETCH_ASSOC); - print_r($row); - - $int = 0; - $bigint = 100; - $query = "select ? = bigint_type, ? = int_type, ? = money_type from [test_types] where int_type < 0"; - $stmt2 = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true)); - $stmt2->bindparam(1, $bigint, PDO::PARAM_STR, 256); - $stmt2->bindParam(2, $int, PDO::PARAM_INT, 4); - $stmt2->bindParam(3, $money, PDO::PARAM_STR, 1024); - $stmt2->execute(); - echo "Big integer: ".$bigint."\n"; - echo "Integer: ".$int."\n"; - echo "Money: ".$money."\n"; - - //free the statement and connection - unset($stmt); - unset($stmt1); - unset($stmt2); - unset($conn); + $stmt = $conn->prepare($query); } catch (PDOException $e) { print("Error: " . $e->getMessage() . "\n"); } + +try { + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + $int = 0; + $bigint = 100; + $query = "select ? = bigint_type, ? = int_type, ? = money_type from [test_types] where int_type < 0"; + $stmt = $conn->prepare($query, array(PDO::ATTR_EMULATE_PREPARES => true)); +} catch (PDOException $e) { + print("Error: " . $e->getMessage() . "\n"); +} + +// free the statement and connection +unset($stmt); +unset($conn); ?> --EXPECT-- Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters. +Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters. diff --git a/test/functional/pdo_sqlsrv/pdo_1310_null_varchar.phpt b/test/functional/pdo_sqlsrv/pdo_1310_null_varchar.phpt index 47d1b68f..3f0998e4 100644 --- a/test/functional/pdo_sqlsrv/pdo_1310_null_varchar.phpt +++ b/test/functional/pdo_sqlsrv/pdo_1310_null_varchar.phpt @@ -2,6 +2,7 @@ GitHub issue 1310 - bind null field as varchar(max) if not binary --DESCRIPTION-- The test shows null fields are no longer bound as char(1) if not binary such that it solves both issues 1310 and 1102. +Note that this test does not connect with AE enabled because SQLDescribeParam() does not work with these queries. --ENV-- PHPT_EXEC=true --SKIPIF-- @@ -9,10 +10,9 @@ PHPT_EXEC=true --FILE-- setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // Issue 1310 diff --git a/test/functional/pdo_sqlsrv/pdo_1320_pdo_emulate_prepare.phpt b/test/functional/pdo_sqlsrv/pdo_1320_pdo_emulate_prepare.phpt new file mode 100644 index 00000000..c1c886e3 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1320_pdo_emulate_prepare.phpt @@ -0,0 +1,62 @@ +--TEST-- +GitHub issue 1320 - support PDO::ATTR_EMULATE_PREPARES at the connection level +--DESCRIPTION-- +Supports PDO::ATTR_EMULATE_PREPARES at the connection level but setting it to true with column +encryption enabled will fail with an exception. Also, the options in the prepared statement will +override the connection setting. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + echo "setAttribute should have failed because column encryption is enabled.\n\n"; +} catch (PDOException $e) { + echo $e->getMessage() . "\n"; +} + +unset($conn); + +try { + // Connection with column encryption enabled + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => true); + $connectionInfo = "ColumnEncryption = Enabled;"; + $conn = new PDO("sqlsrv:server = $server; database=$databaseName; $connectionInfo", $uid, $pwd, $options); +} catch (PDOException $e) { + echo $e->getMessage() . "\n"; +} + +unset($conn); + +try { + // Connection with column encryption enabled - PDO::ATTR_EMULATE_PREPARES is false by default + $connectionInfo = "ColumnEncryption = Enabled;"; + $conn = new PDO("sqlsrv:server = $server; database=$databaseName; $connectionInfo", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + echo "Connected successfully with column encryption enabled.\n"; + $enabled = $conn->getAttribute(PDO::ATTR_EMULATE_PREPARES); + echo "By default, the emulation of prepared statements is:\n"; + var_dump($enabled); +} catch (PDOException $e) { + echo $e->getMessage() . "\n"; +} + +?> + +--EXPECT-- +SQLSTATE[IMSSP]: Parameterized statement with attribute PDO::ATTR_EMULATE_PREPARES is not supported in a Column Encryption enabled Connection. +SQLSTATE[IMSSP]: Parameterized statement with attribute PDO::ATTR_EMULATE_PREPARES is not supported in a Column Encryption enabled Connection. +Connected successfully with column encryption enabled. +By default, the emulation of prepared statements is: +bool(false) diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode2.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode2.phpt new file mode 100644 index 00000000..93d7efec --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_unicode2.phpt @@ -0,0 +1,137 @@ +--TEST-- +Prepare with emulate prepare and binding uft8 characters +--DESCRIPTION-- +This is the same as pdo_prepare_emulatePrepare_unicode.phpt except that +PDO::ATTR_EMULATE_PREPARES at the connection level +--SKIPIF-- + +--FILE-- +prepare($query, $prepareOptions); + $stmt->bindParam(':name', $name, $dataType, $length, $driverOptions); + } else { + $status = 1; + $stmt = $conn->prepare($query, $prepareOptions); + $stmt->bindParam(':name', $name, $dataType, $length, $driverOptions); + $stmt->bindParam(':status', $status); + } + $stmt->execute(); + return $stmt; +} + +try { + $conn = connect("", array(), PDO::ERRMODE_SILENT); + + $tableName = "users"; + createTable($conn, $tableName, array("name" => "nvarchar(max)", "status" => "int", "age" => "int")); + + if (!isColEncrypted()) { + $conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'Belle', 1, 34)"); + $conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'Абрам', 1, 40)"); + $conn->exec("INSERT INTO [$tableName] (name, status, age) VALUES (N'가각', 1, 30)"); + $query = "SELECT * FROM [$tableName] WHERE name = :name AND status = 1"; + } else { + insertRow($conn, $tableName, array("name" => "Belle", "status" => 1, "age" => 34)); + insertRow($conn, $tableName, array("name" => "Абрам", "status" => 1, "age" => 40)); + insertRow($conn, $tableName, array("name" => "가각", "status" => 1, "age" => 30)); + $query = "SELECT * FROM [$tableName] WHERE name = :name AND status = :status"; + } + + //without emulate prepare + print_r("Prepare without emulate prepare:\n"); + $stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + print_r($row); + + if (!isAEConnected()) { + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + } else { + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + } + + //with emulate prepare and no bind param options + print_r("Prepare with emulate prepare and no bindParam options:\n"); + // This test only makes sense without AE because the default encoding is PDO::SQLSRV_ENCODING_UTF8 + if (!isAEConnected()) { + $stmt = prepareStmt($conn, $query, array()); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($stmt->rowCount() != 0) { + print_r("Do not expect results for this query!\n"); + print_r($row); + } + } + + //with emulate prepare and SQLSRV_ENCODING_UTF8 + print_r("Prepare with emulate prepare and SQLSRV_ENCODING_UTF8:\n"); + $stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + print_r($row); + + //with emulate prepare and SQLSRV_ENCODING_SYSTEM + print_r("Prepare with emulate prepare and and SQLSRV_ENCODING_SYSTEM:\n"); + $stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_SYSTEM); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + // The combination of Column Encryption and Unix platforms support SQLSRV_ENCODING_SYSTEM because: + // With Column Encryption enabled, binding parameters uses exact datatypes as the column definition + // the default encoding in Linux and Mac is UTF8 + $success = true; + if (!(strtoupper( substr( php_uname( 's' ),0,3 ) ) === 'WIN') && isAEConnected()) { + if ($row['name'] != "가각" || $row['status'] != 1 || $row['age'] != 30) { + print_r("Incorrect results retrieved.\n"); + $success = false; + } + } else { + // the default encoding in Windows is non-UTF8, thus binding UTF8 parameters does not work + if ($stmt->rowCount() != 0) { + print_r("Binding UTF8 data when encoding is SQLSRV_ENCODING_SYSTEM should not work.\n"); + $success = false; + } + } + if ($success) { + print_r("Binding UTF8 data with SQLSRV_ENCODING_SYSTEM is tested successfully.\n"); + } + + //with emulate prepare and encoding SQLSRV_ENCODING_BINARY + print_r("Prepare with emulate prepare and encoding SQLSRV_ENCODING_BINARY:\n"); + $stmt = prepareStmt($conn, $query, array(), PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_BINARY); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + print_r($row); + if ($stmt->rowCount() == 0) { + print_r("No results for this query\n"); + } + + dropTable($conn, $tableName); + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> + +--EXPECT-- +Prepare without emulate prepare: +Array +( + [name] => 가각 + [status] => 1 + [age] => 30 +) +Prepare with emulate prepare and no bindParam options: +Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: +Array +( + [name] => 가각 + [status] => 1 + [age] => 30 +) +Prepare with emulate prepare and and SQLSRV_ENCODING_SYSTEM: +Binding UTF8 data with SQLSRV_ENCODING_SYSTEM is tested successfully. +Prepare with emulate prepare and encoding SQLSRV_ENCODING_BINARY: +No results for this query diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt index bc21e3dc..bfa16afd 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchmode_emulate_prepare.phpt @@ -17,15 +17,6 @@ try { $tableName = "pdo_test_table"; createTable($conn1, $tableName, array(new ColumnMeta("int", "ID", "NOT NULL PRIMARY KEY"), "Policy" => "varchar(2)", "Label" => "varchar(10)", "Budget" => "money")); - try { - $res = $conn1->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); - if ($res) { - echo "setAttribute should have failed.\n\n"; - } - } catch (Exception $e) { - echo $e->getMessage() . "\n"; - } - try { $query = "SELECT * FROM [$tableName]"; $stmt = $conn1->query($query); @@ -186,7 +177,6 @@ try { ?> --EXPECTF-- -SQLSTATE[IMSSP]: The given attribute is only supported on the PDOStatement object. SQLSTATE[IMSSP]: An invalid attribute was designated on the PDOStatement object. Start inserting data... ....Done.... diff --git a/test/functional/sqlsrv/srv_1310_null_varchar.phpt b/test/functional/sqlsrv/srv_1310_null_varchar.phpt index b701e3c6..9c91bbb9 100644 --- a/test/functional/sqlsrv/srv_1310_null_varchar.phpt +++ b/test/functional/sqlsrv/srv_1310_null_varchar.phpt @@ -2,6 +2,7 @@ GitHub issue 1310 - bind null field as varchar(max) if not binary --DESCRIPTION-- The test shows null fields are no longer bound as char(1) if not binary such that it solves both issues 1310 and 1102. +Note that this test does not connect with AE enabled because SQLDescribeParam() does not work with these queries. --ENV-- PHPT_EXEC=true --SKIPIF-- @@ -10,7 +11,7 @@ PHPT_EXEC=true