diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 63e0598ce..3ec12cb79 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -420,7 +420,6 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( SQLHANDLE h, error_callback e, void* driver TSRM direct_query( false ), query_timeout( QUERY_TIMEOUT_INVALID ), client_buffer_max_size( PDO_SQLSRV_G( client_buffer_max_size )), - bind_param_encoding( SQLSRV_ENCODING_CHAR ), fetch_numeric( false ) { if( client_buffer_max_size < 0 ) { @@ -589,6 +588,8 @@ int pdo_sqlsrv_dbh_prepare( pdo_dbh_t *dbh, const char *sql, sqlsrv_malloc_auto_ptr sql_rewrite; size_t sql_rewrite_len = 0; sqlsrv_malloc_auto_ptr driver_stmt; + hash_auto_ptr placeholders; + sqlsrv_malloc_auto_ptr sql_parser; pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( dbh->driver_data ); SQLSRV_ASSERT(( driver_dbh != NULL ), "pdo_sqlsrv_dbh_prepare: dbh->driver_data was null"); @@ -660,8 +661,17 @@ int pdo_sqlsrv_dbh_prepare( pdo_dbh_t *dbh, const char *sql, } // else if stmt->support_placeholders == PDO_PLACEHOLDER_NONE means that stmt->active_query_string will be // set to the substituted query + if ( stmt->supports_placeholders == PDO_PLACEHOLDER_NONE ) { + // parse placeholders in the sql query into the placeholders ht + ALLOC_HASHTABLE( placeholders ); + core::sqlsrv_zend_hash_init( *driver_dbh, placeholders, 5, ZVAL_PTR_DTOR /* dtor */, 0 /* persistent */ TSRMLS_CC ); + sql_parser = new ( sqlsrv_malloc( sizeof( sql_string_parser ))) sql_string_parser( *driver_dbh, stmt->query_string, + static_cast(stmt->query_stringlen), placeholders ); + sql_parser->parse_sql_string( TSRMLS_C ); + driver_stmt->placeholders = placeholders; + placeholders.transferred(); + } - stmt->driver_data = driver_stmt; driver_stmt.transferred(); } @@ -1279,13 +1289,67 @@ int pdo_sqlsrv_dbh_quote( pdo_dbh_t* dbh, const char* unquoted, size_t unquoted_ PDO_VALIDATE_CONN; PDO_LOG_DBH_ENTRY; - pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( dbh->driver_data ); - SQLSRV_ENCODING encoding = driver_dbh->bind_param_encoding; + SQLSRV_ENCODING encoding = SQLSRV_ENCODING_CHAR; + + // get the current object in PHP; this distinguishes pdo_sqlsrv_dbh_quote being called from: + // 1. PDO::quote() - object name is PDO + // 2. PDOStatement::execute() - object name is PDOStatement + zend_execute_data* execute_data = EG( current_execute_data ); + zval *object = getThis(); + + // iterate through parents to find "PDOStatement" + bool is_statement = false; + zend_class_entry* curr_class = ( Z_OBJ_P( object ))->ce; + while ( curr_class != NULL ) { + if ( strcmp( reinterpret_cast( curr_class->name->val ), "PDOStatement" ) == 0 ) { + is_statement = true; + break; + } + curr_class = curr_class->parent; + } + + // only change the encoding if quote is called from the statement level (which should only be called when a statement + // is prepared with emulate prepared on) + if ( is_statement ) { + pdo_stmt_t *stmt = Z_PDO_STMT_P( object ); + // set the encoding to be the encoding of the statement otherwise set to be the encoding of the dbh + pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); + if ( driver_stmt->encoding() != SQLSRV_ENCODING_INVALID ) { + encoding = driver_stmt->encoding(); + } + else { + pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( stmt->driver_data ); + encoding = driver_dbh->encoding(); + } + // get the placeholder at the current position in driver_stmt->placeholders ht + zval* placeholder = NULL; + if (( placeholder = zend_hash_get_current_data( driver_stmt->placeholders )) != NULL && zend_hash_move_forward( driver_stmt->placeholders ) == SUCCESS ) { + pdo_bound_param_data* param = NULL; + if ( Z_TYPE_P( placeholder ) == IS_STRING ) { + param = reinterpret_cast( zend_hash_find_ptr( stmt->bound_params, Z_STR_P( placeholder ))); + } + else if ( Z_TYPE_P( placeholder ) == IS_LONG) { + param = reinterpret_cast( zend_hash_index_find_ptr( stmt->bound_params, Z_LVAL_P( placeholder ))); + } + if ( NULL != param ) { + SQLSRV_ENCODING param_encoding = static_cast( Z_LVAL( param->driver_params )); + if ( param_encoding != SQLSRV_ENCODING_INVALID ) { + encoding = param_encoding; + } + } + } + } if ( encoding == SQLSRV_ENCODING_BINARY ) { // convert from char* to hex digits using os std::basic_ostringstream os; for ( size_t index = 0; index < unquoted_len && unquoted[ index ] != '\0'; ++index ) { + // when an int is < 16 and is appended to os, its hex representation which starts + // with '0' does not get appended properly (the starting '0' does not get appended) + // thus append '0' first + if (( int )unquoted[index] < 16 ) { + os << '0'; + } os << std::hex << ( int )unquoted[ index ]; } std::basic_string str_hex = os.str(); diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index 74c1a222b..6a735018f 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -24,17 +24,27 @@ // Constructor conn_string_parser:: conn_string_parser( sqlsrv_context& ctx, const char* dsn, int len, _Inout_ HashTable* conn_options_ht ) { - this->conn_str = dsn; + this->orig_str = dsn; this->len = len; - this->conn_options_ht = conn_options_ht; + this->element_ht = conn_options_ht; this->pos = -1; this->ctx = &ctx; this->current_key = 0; this->current_key_name = NULL; } +sql_string_parser:: sql_string_parser( sqlsrv_context& ctx, const char* sql_str, int len, _Inout_ HashTable* placeholders_ht ) +{ + this->orig_str = sql_str; + this->len = len; + this->element_ht = placeholders_ht; + this->pos = -1; + this->ctx = &ctx; + this->current_key = 0; +} + // Move to the next character -inline bool conn_string_parser::next( void ) +inline bool string_parser::next( void ) { // if already at the end then return false if( this->is_eos() ) { @@ -55,7 +65,7 @@ inline bool conn_string_parser::next( void ) } // Check for end of string. -inline bool conn_string_parser::is_eos( void ) +inline bool string_parser::is_eos( void ) { if( this->pos == len ) { @@ -68,7 +78,7 @@ inline bool conn_string_parser::is_eos( void ) } // Check for white space. -inline bool conn_string_parser::is_white_space( char c ) +inline bool string_parser::is_white_space( char c ) { if( c == ' ' || c == '\r' || c == '\n' || c == '\t' ) { return true; @@ -91,14 +101,14 @@ int conn_string_parser::discard_trailing_white_spaces( const char* str, int len } // Discard white spaces. -bool conn_string_parser::discard_white_spaces() +bool string_parser::discard_white_spaces() { if( this->is_eos() ) { return false; } - while( this->is_white_space( this->conn_str[ pos ] )) { + while( this->is_white_space( this->orig_str[ pos ] )) { if( !next() ) return false; @@ -107,8 +117,8 @@ bool conn_string_parser::discard_white_spaces() return true; } -// Add a key-value pair to the hashtable of connection options. -void conn_string_parser::add_key_value_pair( const char* value, int len TSRMLS_DC ) +// Add a key-value pair to the hashtable +void string_parser::add_key_value_pair( const char* value, int len TSRMLS_DC ) { zval value_z; ZVAL_UNDEF( &value_z ); @@ -122,7 +132,15 @@ void conn_string_parser::add_key_value_pair( const char* value, int len TSRMLS_D ZVAL_STRINGL( &value_z, const_cast( value ), len ); } - core::sqlsrv_zend_hash_index_update( *ctx, this->conn_options_ht, this->current_key, &value_z TSRMLS_CC ); + core::sqlsrv_zend_hash_index_update( *ctx, this->element_ht, this->current_key, &value_z TSRMLS_CC ); +} + +// Add a key-value pair to the hashtable with int value +void sql_string_parser::add_key_int_value_pair( unsigned int value TSRMLS_DC ) { + zval value_z; + ZVAL_LONG( &value_z, value ); + + core::sqlsrv_zend_hash_index_update( *ctx, this->element_ht, this->current_key, &value_z TSRMLS_CC ); } // Validate a given DSN keyword. @@ -151,6 +169,15 @@ void conn_string_parser::validate_key(const char *key, int key_len TSRMLS_DC ) THROW_PDO_ERROR( this->ctx, PDO_SQLSRV_ERROR_INVALID_DSN_KEY, static_cast( key_name ) ); } +inline bool sql_string_parser::is_placeholder_char( char c ) +{ + // placeholder only accepts numbers, upper and lower case alphabets and underscore + if (( c >= '0' && c <= '9' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= 'a' && c <= 'z' ) || c == '_' ) { + return true; + } + return false; +} + // Primary function which parses the connection string/DSN. void conn_string_parser:: parse_conn_string( TSRMLS_D ) { @@ -180,7 +207,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) start_pos = this->pos; // read the key name - while( this->conn_str[ pos ] != '=' ) { + while( this->orig_str[ pos ] != '=' ) { if( !next() ) { @@ -188,7 +215,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) } } - this->validate_key( &( this->conn_str[ start_pos ] ), ( pos - start_pos ) TSRMLS_CC ); + this->validate_key( &( this->orig_str[ start_pos ] ), ( pos - start_pos ) TSRMLS_CC ); state = Value; @@ -197,13 +224,13 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) case Value: { - SQLSRV_ASSERT(( this->conn_str[ pos ] == '=' ), "conn_string_parser:: parse_conn_string: " + SQLSRV_ASSERT(( this->orig_str[ pos ] == '=' ), "conn_string_parser:: parse_conn_string: " "Equal was expected" ); next(); // skip "=" // if EOS encountered after 0 or more spaces OR semi-colon encountered. - if( !discard_white_spaces() || this->conn_str[ pos ] == ';' ) { + if( !discard_white_spaces() || this->orig_str[ pos ] == ';' ) { add_key_value_pair( NULL, 0 TSRMLS_CC ); @@ -213,13 +240,13 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) } else { - // this->conn_str[ pos ] == ';' + // this->orig_str[ pos ] == ';' state = NextKeyValuePair; } } // if LCB - else if( this->conn_str[ pos ] == '{' ) { + else if( this->orig_str[ pos ] == '{' ) { start_pos = this->pos; // starting character is LCB state = ValueContent1; @@ -237,7 +264,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) case ValueContent1: { - while ( this->conn_str[ pos ] != '}' ) { + while ( this->orig_str[ pos ] != '}' ) { if ( ! next() ) { @@ -253,7 +280,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) case ValueContent2: { - while( this->conn_str[ pos ] != ';' ) { + while( this->orig_str[ pos ] != ';' ) { if( ! next() ) { @@ -261,13 +288,13 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) } } - if( !this->is_eos() && this->conn_str[ pos ] == ';' ) { + if( !this->is_eos() && this->orig_str[ pos ] == ';' ) { // semi-colon encountered, so go to next key-value pair state = NextKeyValuePair; } - add_key_value_pair( &( this->conn_str[ start_pos ] ), this->pos - start_pos TSRMLS_CC ); + add_key_value_pair( &( this->orig_str[ start_pos ] ), this->pos - start_pos TSRMLS_CC ); SQLSRV_ASSERT((( state == NextKeyValuePair ) || ( this->is_eos() )), "conn_string_parser::parse_conn_string: Invalid state encountered " ); @@ -282,14 +309,14 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) if( !next() ) { // EOS - add_key_value_pair( &( this->conn_str[ start_pos ] ), this->pos - start_pos TSRMLS_CC ); + add_key_value_pair( &( this->orig_str[ start_pos ] ), this->pos - start_pos TSRMLS_CC ); break; } SQLSRV_ASSERT( !this->is_eos(), "conn_string_parser::parse_conn_string: Unexpected EOS encountered" ); // if second RCB encountered than go back to ValueContent1 - if( this->conn_str[ pos ] == '}' ) { + if( this->orig_str[ pos ] == '}' ) { if( !next() ) { @@ -304,20 +331,20 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) int end_pos = this->pos; // discard any trailing white-spaces. - if( this->is_white_space( this->conn_str[ pos ] )) { + if( this->is_white_space( this->orig_str[ pos ] )) { if( ! this->discard_white_spaces() ) { //EOS - add_key_value_pair( &( this->conn_str[ start_pos ] ), end_pos - start_pos TSRMLS_CC ); + add_key_value_pair( &( this->orig_str[ start_pos ] ), end_pos - start_pos TSRMLS_CC ); break; } } // if semi-colon than go to next key-value pair - if ( this->conn_str[ pos ] == ';' ) { + if ( this->orig_str[ pos ] == ';' ) { - add_key_value_pair( &( this->conn_str[ start_pos ] ), end_pos - start_pos TSRMLS_CC ); + add_key_value_pair( &( this->orig_str[ start_pos ] ), end_pos - start_pos TSRMLS_CC ); state = NextKeyValuePair; break; } @@ -328,7 +355,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) } case NextKeyValuePair: { - SQLSRV_ASSERT(( this->conn_str[ pos ] == ';' ), + SQLSRV_ASSERT(( this->orig_str[ pos ] == ';' ), "conn_string_parser::parse_conn_string: semi-colon was expected." ); // Call next() to skip the semi-colon. @@ -338,7 +365,7 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) break; } - if( this->conn_str[ pos ] == ';' ) { + if( this->orig_str[ pos ] == ';' ) { // a second semi-colon is error case. THROW_PDO_ERROR( this->ctx, PDO_SQLSRV_ERROR_EXTRA_SEMI_COLON_IN_DSN_STRING, this->pos ); @@ -360,3 +387,57 @@ void conn_string_parser:: parse_conn_string( TSRMLS_D ) } } +// Primary function which parses out the named placeholders from a sql string. +void sql_string_parser::parse_sql_string( TSRMLS_D ) { + try { + while ( !this->is_eos() ) { + int start_pos = -1; + + // skip until a '"', '\'', ':' or '?' + char sym; + while ( this->orig_str[pos] != '"' && this->orig_str[pos] != '\'' && this->orig_str[pos] != ':' && this->orig_str[pos] != '?' && !this->is_eos() ) { + next(); + } + sym = this->orig_str[pos]; + // if '"' or '\'', skip until the next '"' or '\'' respectively + if ( sym == '"' || sym == '\'' ) { + next(); + while ( this->orig_str[pos] != sym && !this->is_eos() ) { + next(); + } + } + // if ':', store string placeholder in the placeholders hashtable + else if ( sym == ':' ) { + start_pos = this->pos; + next(); + // keep going until the next space or line break + // while (!is_white_space(this->orig_str[pos]) && !this->is_eos()) { + while ( is_placeholder_char( this->orig_str[pos] )) { + next(); + } + add_key_value_pair( &( this->orig_str[start_pos] ), this->pos - start_pos TSRMLS_CC ); + discard_white_spaces(); + // if an '=' is right after a placeholder, it means the placeholder is for output parameters + // and emulate prepare does not support output parameters + if (this->orig_str[pos] == '=') { + THROW_PDO_ERROR(this->ctx, PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED); + } + this->current_key++; + } + // if '?', store long placeholder into the placeholders hashtable + else if ( sym == '?' ) { + next(); + // add dummy value to placeholders ht to keep count of the number of placeholders + add_key_int_value_pair( this->current_key ); + discard_white_spaces(); + if (this->orig_str[pos] == '=') { + THROW_PDO_ERROR(this->ctx, PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED); + } + this->current_key++; + } + } + } + catch ( pdo::PDOException& ) { + throw; + } +} \ No newline at end of file diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index b2dc45eaa..cd44895bb 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -486,9 +486,9 @@ int pdo_sqlsrv_stmt_describe_col(pdo_stmt_t *stmt, int colno TSRMLS_DC) // *stmt - pointer to current statement // Return: // 1 for success. -int pdo_sqlsrv_stmt_dtor(pdo_stmt_t *stmt TSRMLS_DC) +int pdo_sqlsrv_stmt_dtor( pdo_stmt_t *stmt TSRMLS_DC ) { - sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); + pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); LOG( SEV_NOTICE, "pdo_sqlsrv_stmt_dtor: entering" ); @@ -498,7 +498,13 @@ int pdo_sqlsrv_stmt_dtor(pdo_stmt_t *stmt TSRMLS_DC) return 1; } - driver_stmt->~sqlsrv_stmt(); + if ( driver_stmt->placeholders != NULL ) { + zend_hash_destroy( driver_stmt->placeholders ); + FREE_HASHTABLE( driver_stmt->placeholders ); + driver_stmt->placeholders = NULL; + } + + (( sqlsrv_stmt* )driver_stmt )->~sqlsrv_stmt(); sqlsrv_free( driver_stmt ); @@ -547,6 +553,8 @@ int pdo_sqlsrv_stmt_execute(pdo_stmt_t *stmt TSRMLS_DC) // if the user is using prepare emulation (PDO::ATTR_EMULATE_PREPARES), set the query to the // subtituted query provided by PDO if( stmt->supports_placeholders == PDO_PLACEHOLDER_NONE ) { + // reset the placeholders hashtable internal in case the user reexecutes a statement + zend_hash_internal_pointer_reset(driver_stmt->placeholders); query = stmt->active_query_string; query_len = static_cast( stmt->active_query_stringlen ); @@ -1106,10 +1114,9 @@ int pdo_sqlsrv_stmt_param_hook(pdo_stmt_t *stmt, // since the param isn't reliable, we don't do anything here case PDO_PARAM_EVT_ALLOC: - // if emulate prepare is on, set the bind_param_encoding so it can be used in PDO::quote when binding parameters on the client side - if ( stmt->supports_placeholders == PDO_PLACEHOLDER_NONE ) { - pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( stmt->dbh->driver_data ); - driver_dbh->bind_param_encoding = static_cast( Z_LVAL( param->driver_params )); + if ( stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && (param->param_type & PDO_PARAM_INPUT_OUTPUT )) { + sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); + THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED ); } break; case PDO_PARAM_EVT_FREE: diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index a67b194e2..227cc83be 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -373,6 +373,10 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_BUFFER_LIMIT_EXCEEDED, { IMSSP, (SQLCHAR*) "Memory limit of %1!d! KB exceeded for buffered query", -71, true } }, + { + PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED, + { IMSSP, (SQLCHAR*) "Statement with emulate prepare on does not support output or input_output parameters.", -72, false } + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 973c3c3b2..1b5234f5f 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -125,12 +125,29 @@ PHP_MINFO_FUNCTION(pdo_sqlsrv); extern zend_module_entry g_pdo_sqlsrv_module_entry; // describes the extension to PHP +// Basic string parser +class string_parser +{ + protected: + const char* orig_str; + sqlsrv_context* ctx; + int len; + int pos; + unsigned int current_key; + HashTable* element_ht; + inline bool next(void); + inline bool is_eos(void); + inline bool is_white_space(char c); + bool discard_white_spaces(void); + void add_key_value_pair(const char* value, int len TSRMLS_DC); +}; + //********************************************************************************************************************************* // PDO DSN Parser //********************************************************************************************************************************* // Parser class used to parse DSN connection string. -class conn_string_parser +class conn_string_parser : private string_parser { enum States { @@ -144,26 +161,30 @@ class conn_string_parser }; private: - const char* conn_str; - sqlsrv_context* ctx; - int len; - int pos; - unsigned int current_key; const char* current_key_name; - HashTable* conn_options_ht; - inline bool next( void ); - inline bool is_eos( void ); - inline bool is_white_space( char c ); - bool discard_white_spaces( void ); - int discard_trailing_white_spaces( const char* str, int len ); - void validate_key( const char *key, int key_len TSRMLS_DC ); - void add_key_value_pair( const char* value, int len TSRMLS_DC ); + int discard_trailing_white_spaces(const char* str, int len); + void validate_key(const char *key, int key_len TSRMLS_DC); public: conn_string_parser( sqlsrv_context& ctx, const char* dsn, int len, _Inout_ HashTable* conn_options_ht ); void parse_conn_string( TSRMLS_D ); }; +//********************************************************************************************************************************* +// PDO Query Parser +//********************************************************************************************************************************* + +// Parser class used to parse DSN named placeholders. +class sql_string_parser : private string_parser +{ + private: + bool is_placeholder_char(char); + public: + void add_key_int_value_pair(unsigned int value TSRMLS_DC); + sql_string_parser(sqlsrv_context& ctx, const char* sql_str, int len, _Inout_ HashTable* placeholder_ht); + void parse_sql_string(TSRMLS_D); +}; + //********************************************************************************************************************************* // Connection //********************************************************************************************************************************* @@ -178,7 +199,6 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn { bool direct_query; long query_timeout; zend_long client_buffer_max_size; - SQLSRV_ENCODING bind_param_encoding; bool fetch_numeric; pdo_sqlsrv_dbh( SQLHANDLE h, error_callback e, void* driver TSRMLS_DC ); @@ -228,6 +248,7 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { direct_query( false ), direct_query_subst_string( NULL ), direct_query_subst_string_len( 0 ), + placeholders(NULL), bound_column_param_types( NULL ), fetch_numeric( false ) { @@ -245,6 +266,7 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { bool direct_query; // flag set if the query should be executed directly or prepared const char* direct_query_subst_string; // if the query is direct, hold the substitution string if using named parameters size_t direct_query_subst_string_len; // length of query string used for direct queries + HashTable* placeholders; // hashtable of named placeholders to keep track of params ordering in emulate prepare // meta data for current result set std::vector > current_meta_data; @@ -367,7 +389,7 @@ enum PDO_ERROR_CODES { PDO_SQLSRV_ERROR_INVALID_COLUMN_INDEX, PDO_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE, PDO_SQLSRV_ERROR_INVALID_CURSOR_WITH_SCROLL_TYPE, - + PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED, }; extern pdo_error PDO_ERRORS[]; diff --git a/test/pdo_sqlsrv/pdoStatement_bindParam_inout_emulate_prepare.phpt b/test/pdo_sqlsrv/pdoStatement_bindParam_inout_emulate_prepare.phpt new file mode 100644 index 000000000..71d495ea3 --- /dev/null +++ b/test/pdo_sqlsrv/pdoStatement_bindParam_inout_emulate_prepare.phpt @@ -0,0 +1,30 @@ +--TEST-- +Tests error returned when binding input/output parameter with emulate prepare +--SKIPIF-- +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $dbh->query("IF OBJECT_ID('sp_ReverseString', 'P') IS NOT NULL DROP PROCEDURE sp_ReverseString"); + $dbh->query("CREATE PROCEDURE sp_ReverseString @String as VARCHAR(2048) OUTPUT as SELECT @String = REVERSE(@String)"); + $stmt = $dbh->prepare("EXEC sp_ReverseString ?", array(PDO::ATTR_EMULATE_PREPARES => true)); + $string = "123456789"; + $stmt->bindParam(1, $string, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 2048); + $stmt->execute(); + print "Result: ".$string; + + //free the statement and connection + $stmt = null; + $dbh = null; +} +catch(PDOException $e) { + print("Error: " . $e->getMessage() . "\n"); +} +?> +--EXPECT-- +Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters. \ No newline at end of file diff --git a/test/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt b/test/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt new file mode 100644 index 000000000..f69f81658 --- /dev/null +++ b/test/pdo_sqlsrv/pdoStatement_bindParam_output_emulate_prepare.phpt @@ -0,0 +1,52 @@ +--TEST-- +Tests error returned when binding output parameter with emulate prepare +--SKIPIF-- +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $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 + $stmt = null; + $stmt1 = null; + $stmt2 = null; + $conn = null; + +} +catch(PDOException $e) { + print("Error: " . $e->getMessage() . "\n"); +} +?> +--EXPECT-- +Error: SQLSTATE[IMSSP]: Statement with emulate prepare on does not support output or input_output parameters. \ No newline at end of file diff --git a/test/pdo_sqlsrv/pdo_092_emulate_prepare_statement_utf8.phpt b/test/pdo_sqlsrv/pdo_092_emulate_prepare_statement_utf8.phpt new file mode 100644 index 000000000..de6ccbb50 --- /dev/null +++ b/test/pdo_sqlsrv/pdo_092_emulate_prepare_statement_utf8.phpt @@ -0,0 +1,69 @@ +--TEST-- +Test emulate prepare utf8 encoding set at the statement level +--SKIPIF-- +--FILE-- + +prepare("DROP TABLE TEST", $pdo_options); + $st->execute(); +} +catch(\Exception $e) {} + +// Recreate +$st = $connection->prepare("CREATE TABLE TEST([id] [int] IDENTITY(1,1) NOT NULL, [name] nvarchar(max))", $pdo_options); +$st->execute(); + +$prefix = '가각'; +$name = '가각ácasa'; +$name2 = '가각sample2'; + +$pdo_options[PDO::ATTR_EMULATE_PREPARES] = FALSE; +$st = $connection->prepare("INSERT INTO TEST(name) VALUES(:p0)", $pdo_options); +$st->execute(['p0' => $name]); + +$pdo_options[PDO::ATTR_EMULATE_PREPARES] = TRUE; +$st = $connection->prepare("INSERT INTO TEST(name) VALUES(:p0)", $pdo_options); +$st->execute(['p0' => $name2]); + +$statement = $connection->prepare("SELECT * FROM TEST WHERE NAME LIKE :p0", $pdo_options); +$statement->execute(['p0' => "$prefix%"]); +foreach ($statement as $row) { + echo "\n" . 'FOUND: ' . $row['name']; +} + +$pdo_options = array(); +$pdo_options[PDO::ATTR_EMULATE_PREPARES] = FALSE; +$pdo_options[PDO::SQLSRV_ATTR_DIRECT_QUERY] = TRUE; +$pdo_options[PDO::SQLSRV_ATTR_ENCODING] = PDO::SQLSRV_ENCODING_UTF8; +$statement = $connection->prepare("SELECT * FROM TEST WHERE NAME LIKE :p0", $pdo_options); +$statement->execute(['p0' => "$prefix%"]); +foreach ($statement as $row) { + echo "\n" . 'FOUND: ' . $row['name']; +} +$stmt = NULL; +$connection = NULL; + +?> +--EXPECT-- +FOUND: 가각ácasa +FOUND: 가각sample2 +FOUND: 가각ácasa +FOUND: 가각sample2 \ No newline at end of file diff --git a/test/pdo_sqlsrv/pdo_140_emulate_prepare_mix_binary.phpt b/test/pdo_sqlsrv/pdo_140_emulate_prepare_mix_binary.phpt new file mode 100644 index 000000000..13b13b635 --- /dev/null +++ b/test/pdo_sqlsrv/pdo_140_emulate_prepare_mix_binary.phpt @@ -0,0 +1,186 @@ +--TEST-- +Test emulate prepare with mix bound param encodings including binary data +--SKIPIF-- +--FILE-- + &$field_value) { + $placeholder = $placeholder_prefix . $max_placeholder++; + $blob_key = $placeholder . $blob_suffix; + if (isset($columnInformation['blobs'][$field_name])) { + $blobs[$blob_key] = fopen('php://memory', 'a'); + fwrite($blobs[$blob_key], $field_value); + rewind($blobs[$blob_key]); + $this->bindParam($placeholder, $blobs[$blob_key], PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + } + else { + // Even though not a blob, make sure we retain a copy of these values. + $blobs[$blob_key] = $field_value; + $this->bindParam($placeholder, $blobs[$blob_key], PDO::PARAM_STR); + } + } + } +} + +//******************************************************* +// TEST BEGIN +//******************************************************* + +$connection_options['pdo'] = array(); +$connection_options['pdo'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + +$database = "tempdb"; +$cnn = new PDO("sqlsrv:Server=$serverName;Database=$database", $username, $password, $connection_options['pdo']); +$cnn->setAttribute(PDO::ATTR_STATEMENT_CLASS, [MyStatement::class]); + +// Drop +try { + $pdo_options = array(); + $pdo_options[PDO::ATTR_EMULATE_PREPARES] = TRUE; + $pdo_options[PDO::SQLSRV_ATTR_DIRECT_QUERY] = TRUE; + $pdo_options[PDO::ATTR_CURSOR] = PDO::CURSOR_SCROLL; + $pdo_options[PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE] = PDO::SQLSRV_CURSOR_BUFFERED; + $st = $cnn->prepare('DROP TABLE WATCHDOG', $pdo_options); + + $st->execute(); +} +catch(\Exception $e) {} + +$tablescript = <<prepare($tablescript, $pdo_options); +$st->execute(); + +$query = <<prepare($query, $pdo_options); + +$st->BindValues($values, $blobs, ':db_insert', $columnInformation); +$st->execute(); + +$st = $cnn->query("SELECT * FROM [watchdog]"); +var_dump($st->fetchAll(PDO::FETCH_ASSOC)); + +$st = NULL; +$cnn = NULL; + +?> +--EXPECT-- +array(1) { + [0]=> + array(11) { + ["wid"]=> + string(1) "1" + ["uid"]=> + string(1) "0" + ["type"]=> + string(3) "php" + ["message"]=> + string(51) "%type: @message in %function (line %line of %file)." + ["variables"]=> + string(2188) "a:5:{s:5:"%type";s:45:"Drupal\Core\Database\DatabaseExceptionWrapper";s:8:"@message";s:1913:"SQLSTATE[IMSSP]: An error occurred translating the query string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. +.: MERGE INTO [cache_data] _target +USING (SELECT T.* FROM (values(:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6)) as T([cid], [expire], [created], [tags], [checksum], [data], [serialized])) _source +ON _target.[cid] = _source.[cid] +WHEN MATCHED THEN UPDATE SET _target.[expire] = _source.[expire], _target.[created] = _source.[created], _target.[tags] = _source.[tags], _target.[checksum] = _source.[checksum], _target.[data] = _source.[data], _target.[serialized] = _source.[serialized] +WHEN NOT MATCHED THEN INSERT ([cid], [expire], [created], [tags], [checksum], [data], [serialized]) VALUES (_source.[cid], _source.[expire], _source.[created], _source.[tags], _source.[checksum], _source.[data], _source.[serialized]) +OUTPUT $action;; Array +( + [:db_insert_placeholder_0] => Array + ( + [value] => route:/:XDEBUG_SESSION_START=58E1C1C4 + [datatype] => 2 + ) + + [:db_insert_placeholder_1] => Array + ( + [value] => -1 + [datatype] => 2 + ) + + [:db_insert_placeholder_2] => Array + ( + [value] => 1470205773.7 + [datatype] => 2 + ) + + [:db_insert_placeholder_3] => Array + ( + [value] => route_match + [datatype] => 2 + ) + + [:db_insert_placeholder_4] => Array + ( + [value] => 4 + [datatype] => 2 + ) + + [:db_insert_placeholder_5] => Array + ( + [value] => Resource id #4 + [datatype] => 3 + ) + + [:db_insert_placeholder_6] => Array + ( + [value] => 1 + [datatype] => 2 + ) + +) +";s:9:"%function";s:65:"Drupal\Core\Routing\RouteProvider->getRouteCollectionForRequest()";s:5:"%file";s:52:"D:\d8\core\lib\Drupal\Core\Routing\RouteProvider.php";s:5:"%line";i:167;}" + ["severity"]=> + string(1) "3" + ["link"]=> + string(0) "" + ["location"]=> + string(64) "http://local.d7test.com/index.php/?XDEBUG_SESSION_START=58E1C1C4" + ["referer"]=> + string(0) "" + ["hostname"]=> + string(9) "127.0.0.1" + ["timestamp"]=> + string(10) "1470205774" + } +} \ No newline at end of file diff --git a/test/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt b/test/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt new file mode 100644 index 000000000..cf3abf554 --- /dev/null +++ b/test/pdo_sqlsrv/pdo_140_emulate_prepare_pos_placehodlers.phpt @@ -0,0 +1,82 @@ +--TEST-- +Test emulate prepare with mix bound param encodings and positional placeholders (i.e., using '?' as placeholders) +--SKIPIF-- +--FILE-- +prepare('DROP TABLE WATCHDOG', $pdo_options); + + $st->execute(); +} +catch(\Exception $e) {} + +$tablescript = <<prepare($tablescript, $pdo_options); +$st->execute(); + +$query = <<prepare($query, $pdo_options); + +$system_param = 'system encoded string'; +$utf8_param = '가각ácasa'; +$binary_param = fopen('php://memory', 'a'); +fwrite($binary_param, 'asdgasdgasdgsadg'); +rewind($binary_param); + +$st->bindParam(1, $system_param, PDO::PARAM_STR); +$st->bindParam(2, $utf8_param, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8); +$st->bindParam(3, $binary_param, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + +$st->execute(); + +$st = $cnn->query("SELECT * FROM [watchdog]"); +var_dump($st->fetchAll()); + +$st = NULL; +$cnn = NULL; + +?> +--EXPECT-- +array(1) { + [0]=> + array(6) { + ["system_encoding"]=> + string(21) "system encoded string" + [0]=> + string(21) "system encoded string" + ["utf8_encoding"]=> + string(12) "가각ácasa" + [1]=> + string(12) "가각ácasa" + ["binary_encoding"]=> + string(16) "asdgasdgasdgsadg" + [2]=> + string(16) "asdgasdgasdgsadg" + } +} \ No newline at end of file