From cdc107b7b1ed3c42db4416371aa0790f722dc10b Mon Sep 17 00:00:00 2001 From: David Puglielli Date: Thu, 21 Feb 2019 17:39:02 -0800 Subject: [PATCH 1/7] Master (#936) 5.6.0 RTW --- CHANGELOG.md | 46 ++ LICENSE | 2 +- Linux-mac-install.md | 84 ++- README.md | 7 +- buildscripts/builddrivers.py | 53 +- buildscripts/buildtools.py | 3 +- buildscripts/indexsymbols.py | 145 ++++ source/pdo_sqlsrv/config.m4 | 3 +- source/pdo_sqlsrv/config.w32 | 2 +- source/pdo_sqlsrv/pdo_dbh.cpp | 8 +- source/pdo_sqlsrv/pdo_init.cpp | 30 +- source/pdo_sqlsrv/pdo_parser.cpp | 8 +- source/pdo_sqlsrv/pdo_stmt.cpp | 8 +- source/pdo_sqlsrv/pdo_util.cpp | 14 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 378 +---------- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 409 ++++++++++++ source/pdo_sqlsrv/template.rc | 2 +- source/shared/FormattedPrint.cpp | 2 +- source/shared/FormattedPrint.h | 2 +- source/shared/StringFunctions.cpp | 2 +- source/shared/StringFunctions.h | 2 +- source/shared/core_conn.cpp | 49 +- source/shared/core_init.cpp | 19 +- source/shared/core_results.cpp | 2 +- source/shared/core_sqlsrv.h | 4 +- source/shared/core_stmt.cpp | 13 +- source/shared/core_stream.cpp | 2 +- source/shared/core_util.cpp | 2 +- source/shared/globalization.h | 2 +- source/shared/interlockedatomic.h | 2 +- source/shared/interlockedatomic_gcc.h | 2 +- source/shared/interlockedslist.h | 2 +- source/shared/localization.hpp | 2 +- source/shared/localizationimpl.cpp | 10 +- source/shared/msodbcsql.h | 2 +- source/shared/sal_def.h | 2 +- source/shared/typedefs_for_linux.h | 2 +- source/shared/version.h | 6 +- source/shared/xplat.h | 2 +- source/shared/xplat_intsafe.h | 2 +- source/shared/xplat_winerror.h | 2 +- source/shared/xplat_winnls.h | 2 +- source/sqlsrv/config.m4 | 7 +- source/sqlsrv/config.w32 | 6 +- source/sqlsrv/conn.cpp | 8 +- source/sqlsrv/init.cpp | 30 +- source/sqlsrv/php_sqlsrv.h | 626 ++---------------- source/sqlsrv/php_sqlsrv_int.h | 468 +++++++++++++ source/sqlsrv/stmt.cpp | 9 +- source/sqlsrv/template.rc | 2 +- source/sqlsrv/util.cpp | 14 +- .../pdo_sqlsrv/MsCommon_mid-refactor.inc | 27 + .../pdo_sqlsrv/PDO81_MemoryCheck.phpt | 2 +- ...ment_bindParam_output_emulate_prepare.phpt | 1 + ...28_setAttribute_clientbuffermaxkbsize.phpt | 32 +- ...etConnAttribute_clientbuffermaxkbsize.phpt | 94 +++ .../pdo_sqlsrv/pdo_574_next_rowset.phpt | 1 + .../pdo_900_output_param_memory_data.phpt | 80 +++ .../pdo_azure_ad_authentication.phpt | 69 +- .../pdo_azure_ad_managed_identity.phpt | 114 ++++ test/functional/pdo_sqlsrv/pdo_passwords.phpt | 57 -- .../pdo_sqlsrv/pdo_prepare_attribute.phpt | 1 + .../functional/pdo_sqlsrv/skipif_azure_dw.inc | 10 + test/functional/setup/168256.sql | 3 - test/functional/setup/cd_info.sql | 5 +- test/functional/setup/cleanup_dbs.py | 7 +- test/functional/setup/create_db.sql | 18 +- test/functional/setup/create_logins_azure.sql | 18 - test/functional/setup/create_users_azure.sql | 21 - test/functional/setup/drop_db.sql | 9 +- test/functional/setup/exec_sql_scripts.py | 39 +- test/functional/setup/setup_dbs.py | 58 +- test/functional/setup/test_password.sql | 31 - test/functional/setup/test_types.sql | 31 +- test/functional/setup/tracks.sql | 3 - test/functional/sqlsrv/0013.phpt | 2 +- test/functional/sqlsrv/0022.phpt | 2 +- test/functional/sqlsrv/53_0021.phpt | 2 +- test/functional/sqlsrv/MsCommon.inc | 22 + test/functional/sqlsrv/TC81_MemoryCheck.phpt | 4 +- .../sqlsrv/bugfix_dataCorruption.phpt | 2 +- test/functional/sqlsrv/fix_test_168256-2.phpt | 2 +- test/functional/sqlsrv/fix_test_168256.phpt | 2 +- test/functional/sqlsrv/fix_test_182741.phpt | 1 + test/functional/sqlsrv/skipif_azure_dw.inc | 12 + .../sqlsrv/sqlsrv_574_next_result.phpt | 1 + .../sqlsrv_900_output_param_memory_data.phpt | 77 +++ .../sqlsrv_azure_ad_authentication.phpt | 73 +- .../sqlsrv_azure_ad_managed_identity.phpt | 88 +++ .../functional/sqlsrv/sqlsrv_data_to_str.phpt | 2 +- test/functional/sqlsrv/sqlsrv_get_field.phpt | 2 +- test/functional/sqlsrv/sqlsrv_metadata.phpt | 2 +- test/functional/sqlsrv/sqlsrv_readStream.phpt | 2 +- .../sqlsrv/srv_223_sqlsrv_fetch_absolute.phpt | 3 +- .../srv_228_sqlsrv_clientbuffermaxkbsize.phpt | 45 +- ...8_sqlsrv_clientbuffermaxkbsize_option.phpt | 109 +++ .../sqlsrv/test_closeConnection.phpt | 2 +- test/functional/sqlsrv/test_fetch.phpt | 2 +- test/functional/sqlsrv/test_fetch2.phpt | 2 +- test/functional/sqlsrv/test_insert_null.phpt | 2 +- .../sqlsrv/test_insert_nullStr.phpt | Bin 2663 -> 2672 bytes test/functional/sqlsrv/test_largeData.phpt | 2 +- test/functional/sqlsrv/test_newError_msg.phpt | 2 +- .../sqlsrv/test_non_alpha_password.phpt | 61 -- .../sqlsrv/test_sqlsrv_phptype_stream.phpt | Bin 10374 -> 10383 bytes test/functional/sqlsrv/test_stream.phpt | 2 +- .../sqlsrv/test_warning_errors2.phpt | 2 +- 107 files changed, 2258 insertions(+), 1516 deletions(-) create mode 100644 buildscripts/indexsymbols.py create mode 100644 source/pdo_sqlsrv/php_pdo_sqlsrv_int.h create mode 100644 source/sqlsrv/php_sqlsrv_int.h create mode 100644 test/functional/pdo_sqlsrv/pdo_228_setConnAttribute_clientbuffermaxkbsize.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_900_output_param_memory_data.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt delete mode 100644 test/functional/pdo_sqlsrv/pdo_passwords.phpt create mode 100644 test/functional/pdo_sqlsrv/skipif_azure_dw.inc delete mode 100644 test/functional/setup/create_logins_azure.sql delete mode 100644 test/functional/setup/create_users_azure.sql delete mode 100644 test/functional/setup/test_password.sql create mode 100644 test/functional/sqlsrv/skipif_azure_dw.inc create mode 100644 test/functional/sqlsrv/sqlsrv_900_output_param_memory_data.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt create mode 100644 test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt delete mode 100644 test/functional/sqlsrv/test_non_alpha_password.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a46a7f..946ce626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,52 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.6.0 - 2019-02-15 +Updated PECL release packages. Here is the list of updates: + +### Added +- Added support for PHP 7.3 +- Added support for Linux SUSE 15, Ubuntu 18.10 and mac OS Mojave +- Feature Request [#415](https://github.com/Microsoft/msphpsql/pull/886) - new options at connection and statement levels for both drivers for formatting decimal values in the fetched results +- Added support for Azure AD Access Token (in Linux / macOS this requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) and [unixODBC](http://www.unixodbc.org/) 2.3.6+) +- Added support for Authentication with Azure Active Directory using Managed Identity for Azure Resources (requires [MS ODBC Driver 17.3+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server)) +- Feature Request [#842](https://github.com/Microsoft/msphpsql/pull/842) - new PDO_STMT_OPTION_FETCHES_DATETIME_TYPE flag for pdo_sqlsrv to return datetime as objects +- Feature Request [#844](https://github.com/Microsoft/msphpsql/pull/844) - add ReturnDatesAsStrings option to statement level for sqlsrv + +### Removed +- Dropped support for Ubuntu 17.10 +- Dropped support for PHP 7.0 - [Version 5.3](https://docs.microsoft.com/sql/connect/php/system-requirements-for-the-php-sql-driver?view=sql-server-2017) is the last to support PHP 7.0. + +### Fixed +- Issue [#434](https://github.com/Microsoft/msphpsql/issues/434) - To avoid possible crashes, before freeing stmt in the destructor check if its dbh driver data is NULL +- Pull Request [#833](https://github.com/Microsoft/msphpsql/pull/833) - Streamlined the error handling to remove a potential cause of crash +- Pull Request [#836](https://github.com/Microsoft/msphpsql/pull/836) - Modified the config files to enable Spectre Mitigations (use /Qspectre switch) for PHP 7.2 (see related Request [#878](https://github.com/Microsoft/msphpsql/pull/878)) +- Pull Request [#854](https://github.com/Microsoft/msphpsql/pull/854) - Clear Azure Key Vault data after connection attributes are successfully set or when exception is thrown +- Pull Request [#855](https://github.com/Microsoft/msphpsql/pull/855) - Improved performance by saving meta data before fetching and skipping unnecessary conversions for numeric data +- Pull Request [#865](https://github.com/Microsoft/msphpsql/pull/865) - Corrected the way SQLPutData and SQLParamData are used when sending stream data to the server +- Pull Request [#878](https://github.com/Microsoft/msphpsql/pull/878) - Modified the config files to enable Spectre Mitigations for PHP 7.1 (see related Request [#836](https://github.com/Microsoft/msphpsql/pull/836)) +- Pull Request [#891](https://github.com/Microsoft/msphpsql/pull/891) - Improved performance of Unicode conversions +- Pull Request [#892](https://github.com/Microsoft/msphpsql/pull/892) - Removed warning messages while compiling extensions +- Pull Request [#904](https://github.com/Microsoft/msphpsql/pull/904) - Enabled compiling extensions statically into PHP +- Pull Request [#907](https://github.com/Microsoft/msphpsql/pull/907) - Initialized output param buffer when allocating extra space +- Pull Request [#919](https://github.com/Microsoft/msphpsql/pull/919) - Initialized a boolean variable before passing it by reference into a function that will modify its value + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) +- With ColumnEncryption enabled, calling stored procedures with XML parameters does not work (Issue [#674](https://github.com/Microsoft/msphpsql/issues/674)) + ## 5.5.0-preview - 2018-12-07 Updated PECL release packages. Here is the list of updates: diff --git a/LICENSE b/LICENSE index 13fab611..6fa366d5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright(c) 2018 Microsoft Corporation +Copyright(c) 2019 Microsoft Corporation All rights reserved. MIT License diff --git a/Linux-mac-install.md b/Linux-mac-install.md index c83bec3f..2a7357f4 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,37 +1,32 @@ # Linux and macOS Installation Tutorial for the Microsoft Drivers for PHP for SQL Server -The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, Apache, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 18.10, RedHat 7, Debian 8 and 9, Suse 12, and macOS 10.11, 10.12, 10.13, and 10.14. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver.md##loading-the-driver-at-php-startup). +The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, Apache, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 18.10, RedHat 7, Debian 8 and 9, Suse 12 and 15, and macOS 10.11, 10.12, 10.13, and 10.14. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver##loading-the-driver-at-php-startup). -These instructions install PHP 7.2 by default -- see the notes at the beginning of each section to install PHP 7.0 or 7.1. +These instructions install PHP 7.3 by default. Note that some supported Linux distros default to PHP 7.0 or earlier, which is not supported for the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.1 or 7.2 instead. ## Contents of this page: - [Installing the drivers on Ubuntu 16.04, 18.04, and 18.10](#installing-the-drivers-on-ubuntu-1604-1804-and-1810) - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) - [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) -- [Installing the drivers on Suse 12](#installing-the-drivers-on-suse-12) +- [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) - [Installing the drivers on macOS El Capitan, Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-el-capitan-sierra-high-sierra-and-mojave) ## Installing the drivers on Ubuntu 16.04, 18.04, and 18.10 > [!NOTE] -> To install PHP 7.0, 7.1, or 7.3, replace `7.2` with `7.0`, `7.1`, or `7.3` in the following commands. +> To install PHP 7.1 or 7.2, replace 7.3 with 7.1 or 7.2 in the following commands. ### Step 1. Install PHP ``` sudo su add-apt-repository ppa:ondrej/php -y apt-get update -apt-get install php7.2 php7.2-dev php7.2-xml -y --allow-unauthenticated +apt-get install php7.3 php7.3-dev php7.3-xml -y --allow-unauthenticated ``` ### Step 2. Install prerequisites Install the ODBC driver for Ubuntu by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). -For Ubuntu 18.10, follow the above steps for Ubuntu 18.04 except replace `18.04` by `18.10` and download ODBC 17.3 preview [here](https://www.microsoft.com/download/details.aspx?id=57341). - ### Step 3. Install the PHP drivers for Microsoft SQL Server - -> [!NOTE] -> If using PHP 7.3, replace `sqlsrv` and `pdo_sqlsrv` in the following commands with `sqlsrv-5.4.0preview` and `pdo_sqlsrv-5.4.0preview` or later, as earlier versions are not compatible with PHP 7.3. ``` sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv @@ -46,9 +41,9 @@ sudo su apt-get install libapache2-mod-php7.2 apache2 a2dismod mpm_event a2enmod mpm_prefork -a2enmod php7.2 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.2/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.2/apache2/conf.d/20-sqlsrv.ini +a2enmod php7.3 +echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini +echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini exit ``` ### Step 5. Restart Apache and test the sample script @@ -60,7 +55,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Red Hat 7 > [!NOTE] -> To install PHP 7.0, 7.1, or 7.3, replace `remi-php72` with `remi-php70`, `remi-php71`, or `remi-php73` respectively in the following commands. +> To install PHP 7.1 or 7.2, replace remi-php73 with remi-php71 or remi-php72 respectively in the following commands. ### Step 1. Install PHP @@ -71,23 +66,20 @@ wget https://rpms.remirepo.net/enterprise/remi-release-7.rpm rpm -Uvh remi-release-7.rpm epel-release-latest-7.noarch.rpm subscription-manager repos --enable=rhel-7-server-optional-rpms yum install yum-utils -yum-config-manager --enable remi-php72 +yum-config-manager --enable remi-php73 yum update yum install php php-pdo php-xml php-pear php-devel re2c gcc-c++ gcc ``` ### Step 2. Install prerequisites Install the ODBC driver for Red Hat 7 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). -Compiling the PHP drivers with PECL with PHP 7.2 requires a more recent GCC than the default: +Compiling the PHP drivers with PECL with PHP 7.2 or 7.3 requires a more recent GCC than the default: ``` sudo yum-config-manager --enable rhel-server-rhscl-7-rpms sudo yum install devtoolset-7 scl enable devtoolset-7 bash ``` ### Step 3. Install the PHP drivers for Microsoft SQL Server - -> [!NOTE] -> If using PHP 7.3, replace `sqlsrv` and `pdo_sqlsrv` in the following commands with `sqlsrv-5.4.0preview` and `pdo_sqlsrv-5.4.0preview` or later, as earlier versions are not compatible with PHP 7.3. ``` sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv @@ -99,8 +91,8 @@ exit An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): ``` pecl download sqlsrv -tar xvzf sqlsrv-5.3.0.tgz -cd sqlsrv-5.3.0/ +tar xvzf sqlsrv-5.6.0.tgz +cd sqlsrv-5.6.0/ phpize ./configure --with-php-config=/usr/bin/php-config make @@ -127,7 +119,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Debian 8 and 9 > [!NOTE] -> To install PHP 7.0, 7.1, or 7.3, replace `7.2` with `7.0`, `7.1`, or `7.3` in the following commands. +> To install PHP 7.1 or 7.2, replace 7.3 in the following commands with 7.1 or 7.2. ### Step 1. Install PHP ``` @@ -136,7 +128,7 @@ apt-get install curl apt-transport-https wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list apt-get update -apt-get install -y php7.2 php7.2-dev php7.2-xml +apt-get install -y php7.3 php7.3-dev php7.3-xml ``` ### Step 2. Install prerequisites Install the ODBC driver for Debian by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). @@ -149,9 +141,6 @@ locale-gen ``` ### Step 3. Install the PHP drivers for Microsoft SQL Server - -> [!NOTE] -> If using PHP 7.3, replace `sqlsrv` and `pdo_sqlsrv` in the following commands with `sqlsrv-5.4.0preview` and `pdo_sqlsrv-5.4.0preview` or later, as earlier versions are not compatible with PHP 7.3. ``` sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv @@ -163,12 +152,12 @@ exit ### Step 4. Install Apache and configure driver loading ``` sudo su -apt-get install libapache2-mod-php7.2 apache2 +apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork -a2enmod php7.2 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.2/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.2/apache2/conf.d/20-sqlsrv.ini +a2enmod php7.3 +echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini +echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini ``` ### Step 5. Restart Apache and test the sample script ``` @@ -176,27 +165,32 @@ sudo service apache2 restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Suse 12 +## Installing the drivers on Suse 12 and 15 > [!NOTE] -> To install PHP 7.0 or 7.1, replace the repository URL below with one of the following URLs: -`https://download.opensuse.org/repositories/devel:languages:php:php70/SLE_12_SP3/devel:languages:php:php70.repo` -`https://download.opensuse.org/repositories/devel:languages:php:php71/SLE_12_SP3/devel:languages:php:php71.repo` +> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1, and similarly for other versions. Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. + +> [!NOTE] +> Packages for PHP 7.3 are not available for Suse 12. +> To install PHP 7.1, replace the repository URL below with the following URL: + `https://download.opensuse.org/repositories/devel:/languages:/php:/php71//devel:languages:php:php71.repo`. +> To install PHP 7.2, replace the repository URL below with the following URL: + `https://download.opensuse.org/repositories/devel:/languages:/php:/php72//devel:languages:php:php72.repo`. ### Step 1. Install PHP ``` sudo su -zypper -n ar -f https://download.opensuse.org/repositories/devel:languages:php/SLE_12_SP3/devel:languages:php.repo +zypper -n ar -f https://download.opensuse.org/repositories/devel:languages:php//devel:languages:php.repo zypper --gpg-auto-import-keys refresh -zypper -n install php7 php7-pear php7-devel +zypper -n install php7 php7-pear php7-devel php7-openssl ``` ### Step 2. Install prerequisites -Install the ODBC driver for Suse 12 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). +Install the ODBC driver for Suse by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). ### Step 3. Install the PHP drivers for Microsoft SQL Server - > [!NOTE] -> If using PHP 7.3, replace `sqlsrv` and `pdo_sqlsrv` in the following commands with `sqlsrv-5.4.0preview` and `pdo_sqlsrv-5.4.0preview` or later, as earlier versions are not compatible with PHP 7.3. +> If you get an error message saying `Connection to 'pecl.php.net:443' failed: Unable to find the socket transport "ssl"`, edit the pecl script at /usr/bin/pecl and remove the `-n` switch in the last line. This switch prevents PECL from loading ini files when PHP is called, which prevents the OpenSSL extension from loading. + ``` sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv @@ -228,18 +222,18 @@ If you do not already have it, install brew as follows: ``` > [!NOTE] -> To install PHP 7.0, 7.1, or 7.3, replace `php@7.2` with `php@7.0`, `php@7.1`, or `php@7.3` respectively in the following commands. +> To install PHP 7.1 or 7.2, replace php@7.3 with php@7.1 or php@7.2 respectively in the following commands. ### Step 1. Install PHP ``` brew tap brew tap homebrew/core -brew install php@7.2 +brew install php@7.3 ``` PHP should now be in your path -- run `php -v` to verify that you are running the correct version of PHP. If PHP is not in your path or it is not the correct version, run the following: ``` -brew link --force --overwrite php@7.2 +brew link --force --overwrite php@7.3 ``` ### Step 2. Install prerequisites @@ -251,9 +245,6 @@ brew install autoconf automake libtool ``` ### Step 3. Install the PHP drivers for Microsoft SQL Server - -> [!NOTE] -> If using PHP 7.3, replace `sqlsrv` and `pdo_sqlsrv` in the following commands with `sqlsrv-5.4.0preview` and `pdo_sqlsrv-5.4.0preview` or later, as earlier versions are not compatible with PHP 7.3. ``` sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv @@ -329,5 +320,4 @@ function formatErrors($errors) } ?> ``` -Point your browser to https://localhost/testsql.php (https://localhost:8080/testsql.php on macOS). You should now be able to connect to your SQL Server/Azure SQL database. - +Point your browser to https://localhost/testsql.php (https://localhost:8080/testsql.php on macOS). You should now be able to connect to your SQL Server/Azure SQL database. \ No newline at end of file diff --git a/README.md b/README.md index bd2b2349..84952fe3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ On the server side, Microsoft SQL Server 2008 R2 and above on Windows are suppor The drivers are distributed as pre-compiled extensions for PHP found on the [releases page](https://github.com/Microsoft/msphpsql/releases). They are available in thread-safe and non thread-safe versions, and in 32-bit and 64-bit versions. The source code for the drivers is also available, and you can compile them as thread safe or non-thread safe versions. The thread safety configuration of your web server will determine which version you need. -If you choose to build the drivers, you must be able to build PHP 7 without including these extensions. For help building PHP on Windows, see the [official PHP website][phpbuild]. For details on compiling the drivers, see the [documentation](https://github.com/Microsoft/msphpsql/tree/dev/buildscripts#windows) -- an example buildscript is provided, but you can also compile the drivers manually. +If you choose to build the drivers, you must be able to build PHP 7.* without including these extensions. For help building PHP on Windows, see the [official PHP website][phpbuild]. For details on compiling the drivers, see the [documentation](https://github.com/Microsoft/msphpsql/tree/dev/buildscripts#windows) -- an example buildscript is provided, but you can also compile the drivers manually. To load the drivers, make sure that the driver is in your PHP extension directory and enable it in your PHP installation's php.ini file by adding `extension=php_sqlsrv.dll` and/or `extension=php_pdo_sqlsrv.dll` to it. If necessary, specify the extension directory using `extension_dir`, for example: `extension_dir = "C:\PHP\ext"`. Note that the precompiled binaries have different names -- substitute accordingly in php.ini. For more details on loading the drivers, see [Loading the PHP SQL Driver](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver) on Microsoft Docs. @@ -88,6 +88,7 @@ The version number may have trailing pre-release version identifiers to indicate ## Future Plans - Expand SQL Server 2016 feature support (example: Azure Active Directory) - Add more verification/fundamental tests +- Improve performance - Bug fixes ## Guidelines for Reporting Issues @@ -99,14 +100,14 @@ We appreciate you taking the time to test the driver, provide feedback and repor Thank you! -## FAQs +## Questions **Q:** Can we get dates for any of the Future Plans listed above? **A:** At this time, Microsoft is not able to announce dates. We are working hard to release future versions of the driver and will share future plans as appropriate. **Q:** What's next? -**A:** On July 20, 2018 we released the production release version 5.3.0 of our PHP Driver. We will continue working on our future plans and releasing previews of upcoming releases. +**A:** We will continue working on our future plans and releasing previews of upcoming [releases](https://github.com/Microsoft/msphpsql/releases) **Q:** Is Microsoft taking pull requests for this project? diff --git a/buildscripts/builddrivers.py b/buildscripts/builddrivers.py index e9ac02dc..ff301cc5 100644 --- a/buildscripts/builddrivers.py +++ b/buildscripts/builddrivers.py @@ -24,7 +24,9 @@ import sys import shutil import os.path import argparse +import subprocess from buildtools import BuildUtil +from indexsymbols import * class BuildDriver(object): """Build sqlsrv and/or pdo_sqlsrv drivers with PHP source with the following properties: @@ -38,6 +40,8 @@ class BuildDriver(object): make_clean # a boolean flag - whether make clean is necessary source_path # path to a local source folder testing # whether the user has turned on testing mode + srctool_path # path to source indexing tools (empty string by default) + tag_version # tag version for source indexing (empty string by default) """ def __init__(self, phpver, driver, arch, thread, debug, repo, branch, source, path, testing, no_rename): @@ -49,6 +53,8 @@ class BuildDriver(object): self.testing = testing self.rebuild = False self.make_clean = False + self.srctool_path = '' + self.tag_version = '' def show_config(self): print() @@ -112,6 +118,34 @@ class BuildDriver(object): print("The path provided is invalid. Please re-enter.") return source + def index_all_symbols(self, ext_dir, srctool_path, tag_version): + """This takes care of indexing all the symbols + + :param ext_dir: the directory where we can find the built extension(s) + :param srctool_path: the path to the tools for source indexing + :param tag_version: tag version for source indexing + :outcome: all symbols will be source indexed + """ + work_dir = os.path.dirname(os.path.realpath(__file__)) + os.chdir(srctool_path) + + if self.util.driver == 'all': + driver = 'sqlsrv' + pdbfile = os.path.join(ext_dir, self.util.driver_name(driver, '.pdb')) + print('Indexing this symbol: ', pdbfile) + run_indexing_tools(pdbfile, driver, tag_version) + driver = 'pdo_sqlsrv' + pdbfile = os.path.join(ext_dir, self.util.driver_name(driver, '.pdb')) + print('Indexing this symbol: ', pdbfile) + run_indexing_tools(pdbfile, driver, tag_version) + else: + driver = self.util.driver + pdbfile = os.path.join(ext_dir, self.util.driver_name(driver, '.pdb')) + print('Indexing this symbol: ', pdbfile) + run_indexing_tools(pdbfile, driver, tag_version) + + os.chdir(work_dir) + def build_extensions(self, root_dir, logfile): """This takes care of getting the drivers' source files, building the drivers. If dest_path is defined, the binaries will be copied to the designated destinations. @@ -151,6 +185,12 @@ class BuildDriver(object): # ext_dir is the directory where we can find the built extension(s) ext_dir = self.util.build_drivers(self.make_clean, dest, logfile) + # Do source indexing only if the tag and tools path are both specified + if self.tag_version is not '' and self.srctool_path is not '': + print('Source indexing begins...') + self.index_all_symbols(ext_dir, self.srctool_path, self.tag_version) + print('Source indexing done') + # Copy the binaries if a destination path is defined if self.dest_path is not None: dest_drivers = os.path.join(self.dest_path, self.util.major_version(), self.util.arch) @@ -172,9 +212,12 @@ class BuildDriver(object): return ext_dir - def build(self): + def build(self, srctool_path, tag_version): """This is the main entry point of building drivers for PHP. For development, this will loop till the user decides to quit. + + :param srctool_path: the path to the tools for source indexing + :param tag_version: tag version for source indexing """ self.show_config() @@ -191,6 +234,10 @@ class BuildDriver(object): logfile = self.util.get_logfile_name() + # Save source indexing details + self.srctool_path = srctool_path + self.tag_version = tag_version + try: ext_dir = self.build_extensions(root_dir, logfile) print('Build Completed') @@ -244,6 +291,8 @@ if __name__ == '__main__': parser.add_argument('--TESTING', action='store_true', help="turns on testing mode (default: False)") parser.add_argument('--DESTPATH', default=None, help="an alternative destination for the drivers (default: None)") parser.add_argument('--NO_RENAME', action='store_true', help="drivers will not be renamed(default: False)") + parser.add_argument('--SRCIDX_PATH', default='', help="the path to the tools for source indexing (default: '')") + parser.add_argument('--TAG_VERSION', default='', help="the tag version for source indexing (default: '')") args = parser.parse_args() @@ -305,4 +354,4 @@ if __name__ == '__main__': path, testing, no_rename) - builder.build() + builder.build(args.SRCIDX_PATH, args.TAG_VERSION) diff --git a/buildscripts/buildtools.py b/buildscripts/buildtools.py index 79b36923..59f8b8f5 100644 --- a/buildscripts/buildtools.py +++ b/buildscripts/buildtools.py @@ -336,7 +336,6 @@ class BuildUtil(object): is complete. """ work_dir = os.path.dirname(os.path.realpath(__file__)) - # First, update the driver source file contents source_dir = os.path.join(work_dir, 'Source') if self.driver == 'all': @@ -402,6 +401,7 @@ class BuildUtil(object): # Final step, copy the binaries to the right place ext_dir = self.copy_binaries(sdk_dir, copy_to_ext) + return ext_dir def rename_binary(self, path, driver): @@ -454,7 +454,6 @@ class BuildUtil(object): shutil.copy(os.path.join(phpsrc, 'php.ini-production'), php_ini_file) # Copy run-tests.php as well - phpsrc = self.phpsrc_root(sdk_dir) shutil.copy(os.path.join(phpsrc, 'run-tests.php'), build_dir) print('Copying the binaries from', build_dir) diff --git a/buildscripts/indexsymbols.py b/buildscripts/indexsymbols.py new file mode 100644 index 00000000..bcf6ffbd --- /dev/null +++ b/buildscripts/indexsymbols.py @@ -0,0 +1,145 @@ +#!/usr/bin/python3 +######################################################################################### +# +# Description: This contains helper methods for source indexing +# +# Requirement: +# python 3.x +# srctool.exe and pdbstr.exe +# +############################################################################################# + +import os.path +import argparse +import subprocess +from subprocess import Popen, PIPE + +def write_index(index_filename, tag_version): + """This writes to a temporary index file for the pdbstr tool + + For example + + SRCSRV: ini ------------------------------------------------ + VERSION=1 + SRCSRV: variables ------------------------------------------ + PATH=%var2% + SRCSRVTRG=%TARG%\%PDBVERSION%\%fnbksl%(%var2%) + SRCURL=https://raw.githubusercontent.com/Microsoft/msphpsql/%SRCVERSION%/source/%PATH% + SRCSRVCMD=powershell -Command "$r=New-Object -ComObject Msxml2.XMLHTTP; $r.open('GET', '%SRCURL%', $false); $r.send(); [io.file]::WriteAllBytes('%SRCSRVTRG%', $r.responseBody)" + SRCVERSION=v5.6.0 + PDBVERSION=v5.6.0 + For example + """ + with open(index_filename, 'w') as f: + f.write('SRCSRV: ini ------------------------------------------------' + os.linesep) + f.write('VERSION=1' + os.linesep) + f.write('SRCSRV: variables ------------------------------------------' + os.linesep) + f.write('PATH=%var2%' + os.linesep) + f.write('SRCSRVTRG=%TARG%\%PDBVERSION%\%fnbksl%(%var2%)' + os.linesep) + f.write('SRCURL=https://raw.githubusercontent.com/Microsoft/msphpsql/%SRCVERSION%/source/%PATH%' + os.linesep) + f.write('SRCSRVCMD=powershell -Command ') + f.write('\"$r=New-Object -ComObject Msxml2.XMLHTTP; ') + f.write('$r.open(\'GET\', \'%SRCURL%\', $false); ') + f.write('$r.send(); [io.file]::WriteAllBytes(\'%SRCSRVTRG%\', $r.responseBody)\"' + os.linesep) + f.write('SRCVERSION=' + tag_version + os.linesep) + f.write('PDBVERSION=' + tag_version + os.linesep) + +def append_source_filess(index_filename, source_files, driver): + """This appends the paths to different source files to the temporary index file + + For example + + SRCSRV: source files --------------------------------------- + c:\php-sdk\phpdev\vc15\x86\php-7.2.14-src\ext\pdo_sqlsrv\pdo_dbh.cpp*pdo_sqlsrv/pdo_dbh.cpp + c:\php-sdk\phpdev\vc15\x86\php-7.2.14-src\ext\pdo_sqlsrv\pdo_init.cpp*pdo_sqlsrv/pdo_init.cpp + ... ... + c:\php-sdk\phpdev\vc15\x86\php-7.2.14-src\ext\pdo_sqlsrv\shared\core_stream.cpp*shared/core_stream.cpp + c:\php-sdk\phpdev\vc15\x86\php-7.2.14-src\ext\pdo_sqlsrv\shared\core_util.cpp*shared/core_util.cpp + SRCSRV: end ------------------------------------------------ + """ + failed = False + with open(index_filename, 'a') as idx_file: + idx_file.write('SRCSRV: source files ---------------------------------------' + os.linesep) + with open(source_files, 'r') as src_file: + for line in src_file: + pos = line.find('shared') + if (pos > 0): # it's a nested folder, so it must be positive + relative_path = line[pos:] + src_line = line[:-1] + '*' + relative_path.replace('\\', '/') + else: # not a file in the shared folder + pos = line.find(driver) + if (pos <= 0): + print('ERROR: Expected to find', driver, 'in', line) + failed = True + break + else: + relative_path = line[pos:] + src_line = line[:-1] + '*' + relative_path.replace('\\', '/') + idx_file.write(src_line) + idx_file.write('SRCSRV: end ------------------------------------------------' + os.linesep) + return failed + +def run_indexing_tools(pdbfile, driver, tag_version): + """This invokes the source indexing tools, srctool.exe and pdbstr.exe + + :param pdbfile: the absolute path to the symbol file + :param driver: either sqlsrv or pdo_sqlsrv + :param tag_version: tag version for source indexing + :outcome: the driver pdb file will be source indexed + """ + # run srctool.exe to get all driver's source files from the PDB file + # srctool.exe -r | find "\" | sort > files.txt + batch_filename = 'runsrctool.bat' + index_filename = 'idx.txt' + source_files = 'files.txt' + + with open(batch_filename, 'w') as batch_file: + batch_file.write('@ECHO OFF' + os.linesep) + batch_file.write('@CALL srctool -r %1 | find "%2\\" | sort > ' + source_files + ' 2>&1' + os.linesep) + + get_source_filess = batch_filename + ' {0} {1} ' + get_source_filess_cmd = get_source_filess.format(pdbfile, driver) + subprocess.call(get_source_filess_cmd) + + # create an index file using the above inputs for pdbstr.exe + write_index(index_filename, tag_version) + failed = append_source_filess(index_filename, source_files, driver) + + if failed: + print("ERROR: Failed to prepare the temporary index file for the pdbstr tool") + exit(1) + + # run pdbstr.exe to insert the information into the PDB file + # pdbstr.exe -w -p: -i:idx.txt -s:srcsrv + pdbstr_str = 'pdbstr.exe -w -p:{0} -i:{1} -s:srcsrv' + pdbstr_cmd = pdbstr_str.format(pdbfile, index_filename) + subprocess.call(pdbstr_cmd) + + os.remove(batch_filename) + os.remove(index_filename) + os.remove(source_files) + +################################### Main Function ################################### +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('PDBFILE', help="the path to the pdb file for source indexing") + parser.add_argument('DRIVER', choices=['sqlsrv', 'pdo_sqlsrv'], help="driver name of this pdb file") + parser.add_argument('TAG_VERSION', help="the tag version for source indexing (e.g. v5.6.0)") + parser.add_argument('TOOLS_PATH',help="the path to the source indexing tools") + + args = parser.parse_args() + + srctool_exe = os.path.join(args.TOOLS_PATH, 'srctool.exe') + pdbstr_exe = os.path.join(args.TOOLS_PATH, 'pdbstr.exe') + if not os.path.exists(srctool_exe) or not os.path.exists(pdbstr_exe): + print('ERROR: Missing the required source indexing tools') + exit(1) + + work_dir = os.path.dirname(os.path.realpath(__file__)) + os.chdir(args.TOOLS_PATH) + + print('Source indexing begins...') + run_indexing_tools(args.PDBFILE, args.DRIVER.lower(), args.TAG_VERSION) + print('Source indexing done') + + os.chdir(work_dir) diff --git a/source/pdo_sqlsrv/config.m4 b/source/pdo_sqlsrv/config.m4 index 82aa69e2..68dbea0e 100644 --- a/source/pdo_sqlsrv/config.m4 +++ b/source/pdo_sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.5 for PHP for SQL Server +dnl Microsoft Drivers 5.6 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License @@ -95,4 +95,3 @@ if test "$PHP_PDO_SQLSRV" != "no"; then PHP_ADD_EXTENSION_DEP(pdo_sqlsrv, pdo) PHP_ADD_BUILD_DIR([$ext_builddir/shared], 1) fi - diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index 14362748..95bff567 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index d1f76e49..669bc62b 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDO object for PDO_SQLSRV // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -17,7 +17,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_pdo_sqlsrv.h" +extern "C" { + #include "php_pdo_sqlsrv.h" +} + +#include "php_pdo_sqlsrv_int.h" #include #include diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index 6b7b54e3..bee4cfc5 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -3,7 +3,7 @@ // // Contents: initialization routines for PDO_SQLSRV // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -17,12 +17,18 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_pdo_sqlsrv.h" +extern "C" { + #include "php_pdo_sqlsrv.h" +} +#include "php_pdo_sqlsrv_int.h" + +#ifdef COMPILE_DL_PDO_SQLSRV #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE(); #endif ZEND_GET_MODULE(g_pdo_sqlsrv) +#endif extern "C" { @@ -308,3 +314,23 @@ namespace { { NULL , 0 } // terminate the table }; } + +// DllMain for the extension. +#ifdef _WIN32 +// Only needed if extension is built shared +#ifdef COMPILE_DL_PDO_SQLSRV +BOOL WINAPI DllMain( _In_ HINSTANCE hinstDLL, _In_ DWORD fdwReason, LPVOID ) +{ + switch( fdwReason ) { + case DLL_PROCESS_ATTACH: + // store the module handle for use by client_info and server_info + g_sqlsrv_hmodule = hinstDLL; + break; + default: + break; + } + + return TRUE; +} +#endif +#endif diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index 984f983c..edd333e4 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -5,7 +5,7 @@ // // Copyright Microsoft Corporation // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -19,7 +19,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_pdo_sqlsrv.h" +extern "C" { + #include "php_pdo_sqlsrv.h" +} + +#include "php_pdo_sqlsrv_int.h" // Constructor conn_string_parser:: conn_string_parser( _In_ sqlsrv_context& ctx, _In_ const char* dsn, _In_ int len, _In_ HashTable* conn_options_ht ) diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 3f7e72b8..cd8c697f 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDOStatement object for the PDO_SQLSRV // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -17,7 +17,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_pdo_sqlsrv.h" +extern "C" { + #include "php_pdo_sqlsrv.h" +} + +#include "php_pdo_sqlsrv_int.h" // *** internal variables and constants *** diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 91efcb90..e399fda1 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -3,7 +3,7 @@ // // Contents: Utility functions used by both connection or statement functions // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -17,7 +17,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_pdo_sqlsrv.h" +extern "C" { + #include "php_pdo_sqlsrv.h" +} + +#include "php_pdo_sqlsrv_int.h" #include "zend_exceptions.h" @@ -379,7 +383,7 @@ pdo_error PDO_ERRORS[] = { }, { PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - { IMSSP, (SQLCHAR*) "Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported.", -73, false } + { IMSSP, (SQLCHAR*) "Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported.", -73, false } }, { SQLSRV_ERROR_CE_DRIVER_REQUIRED, @@ -441,6 +445,10 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_INVALID_DECIMAL_PLACES, { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -92, false} }, + { + SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, + { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -93, false} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index f31da7c7..3e013f95 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Declarations for the extension // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -20,55 +20,12 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "core_sqlsrv.h" -#include "version.h" - -extern "C" { - -#include "pdo/php_pdo.h" -#include "pdo/php_pdo_driver.h" - -} - -#include -#include - - -//********************************************************************************************************************************* -// Constants and Types -//********************************************************************************************************************************* - -// sqlsrv driver specific PDO attributes -enum PDO_SQLSRV_ATTR { - - // The custom attributes for this driver: - SQLSRV_ATTR_ENCODING = PDO_ATTR_DRIVER_SPECIFIC, - SQLSRV_ATTR_QUERY_TIMEOUT, - SQLSRV_ATTR_DIRECT_QUERY, - SQLSRV_ATTR_CURSOR_SCROLL_TYPE, - SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, - SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, - SQLSRV_ATTR_FETCHES_DATETIME_TYPE, - SQLSRV_ATTR_FORMAT_DECIMALS, - SQLSRV_ATTR_DECIMAL_PLACES -}; - -// valid set of values for TransactionIsolation connection option -namespace PDOTxnIsolationValues { - - const char READ_UNCOMMITTED[] = "READ_UNCOMMITTED"; - const char READ_COMMITTED[] = "READ_COMMITTED"; - const char REPEATABLE_READ[] = "REPEATABLE_READ"; - const char SERIALIZABLE[] = "SERIALIZABLE"; - const char SNAPSHOT[] = "SNAPSHOT"; -} +#include "php.h" //********************************************************************************************************************************* // Global variables //********************************************************************************************************************************* -extern "C" { - // request level variables ZEND_BEGIN_MODULE_GLOBALS(pdo_sqlsrv) @@ -79,32 +36,6 @@ ZEND_END_MODULE_GLOBALS(pdo_sqlsrv) ZEND_EXTERN_MODULE_GLOBALS(pdo_sqlsrv); -} - -// macros used to access the global variables. Use these to make global variable access agnostic to threads -#ifdef ZTS -#define PDO_SQLSRV_G(v) TSRMG(pdo_sqlsrv_globals_id, zend_pdo_sqlsrv_globals *, v) -#else -#define PDO_SQLSRV_G(v) pdo_sqlsrv_globals.v -#endif - -// INI settings and constants -// (these are defined as macros to allow concatenation as we do below) -#define INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE "client_buffer_max_kb_size" -#define INI_PDO_SQLSRV_LOG "log_severity" -#define INI_PREFIX "pdo_sqlsrv." - -PHP_INI_BEGIN() - STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, log_severity, - zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) - STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE , INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, - client_buffer_max_size, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) -PHP_INI_END() - -// henv context for creating connections -extern sqlsrv_context* g_pdo_henv_cp; -extern sqlsrv_context* g_pdo_henv_ncp; - //********************************************************************************************************************************* // Initialization @@ -128,309 +59,4 @@ 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( _In_ char c ); - bool discard_white_spaces(void); - void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC ); -}; - -//********************************************************************************************************************************* -// PDO DSN Parser -//********************************************************************************************************************************* - -// Parser class used to parse DSN connection string. -class conn_string_parser : private string_parser -{ - enum States - { - FirstKeyValuePair, - Key, - Value, - ValueContent1, - ValueContent2, - RCBEncountered, - NextKeyValuePair, - }; - - private: - const char* current_key_name; - int discard_trailing_white_spaces( _In_reads_(len) const char* str, _Inout_ int len ); - void validate_key( _In_reads_(key_len) const char *key, _Inout_ int key_len TSRMLS_DC); - - protected: - void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC); - - public: - conn_string_parser( _In_ sqlsrv_context& ctx, _In_ const char* dsn, _In_ int len, _In_ 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( _In_ unsigned int value TSRMLS_DC ); - sql_string_parser(_In_ sqlsrv_context& ctx, _In_ const char* sql_str, _In_ int len, _In_ HashTable* placeholder_ht); - void parse_sql_string(TSRMLS_D); -}; - -//********************************************************************************************************************************* -// Connection -//********************************************************************************************************************************* -extern const connection_option PDO_CONN_OPTS[]; - -int pdo_sqlsrv_db_handle_factory( _Inout_ pdo_dbh_t *dbh, _In_opt_ zval *driver_options TSRMLS_DC); - -// a core layer pdo dbh object. This object inherits and overrides the statement factory -struct pdo_sqlsrv_dbh : public sqlsrv_conn { - - zval* stmts; - bool direct_query; - long query_timeout; - zend_long client_buffer_max_size; - bool fetch_numeric; - bool fetch_datetime; - bool format_decimals; - short decimal_places; - - pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ); -}; - - -//********************************************************************************************************************************* -// Statement -//********************************************************************************************************************************* - -struct stmt_option_encoding : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_pdo_scrollable : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_direct_query : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_cursor_scroll_type : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_emulate_prepares : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_fetch_numeric : public stmt_option_functor { - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -struct stmt_option_fetch_datetime : public stmt_option_functor { - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -extern struct pdo_stmt_methods pdo_sqlsrv_stmt_methods; - -// a core layer pdo stmt object. This object inherits and overrides the callbacks necessary -struct pdo_sqlsrv_stmt : public sqlsrv_stmt { - - pdo_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_ void* drv TSRMLS_DC ) : - sqlsrv_stmt( c, handle, e, drv TSRMLS_CC ), - direct_query( false ), - direct_query_subst_string( NULL ), - direct_query_subst_string_len( 0 ), - placeholders(NULL), - bound_column_param_types( NULL ), - fetch_numeric( false ), - fetch_datetime( false ) - { - pdo_sqlsrv_dbh* db = static_cast( c ); - direct_query = db->direct_query; - fetch_numeric = db->fetch_numeric; - fetch_datetime = db->fetch_datetime; - format_decimals = db->format_decimals; - decimal_places = db->decimal_places; - } - - virtual ~pdo_sqlsrv_stmt( void ); - - // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants - // for PDO, everything is a string, so we return SQLSRV_PHPTYPE_STRING for all SQL types - virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); - - 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 - - pdo_param_type* bound_column_param_types; - bool fetch_numeric; - bool fetch_datetime; -}; - - -//********************************************************************************************************************************* -// Error Handling Functions -//********************************************************************************************************************************* - -// represents the mapping between an error_code and the corresponding error message. -struct pdo_error { - - unsigned int error_code; - sqlsrv_error_const sqlsrv_error; -}; - -// called when an error occurs in the core layer. These routines are set as the error_callback in a -// context. The context is passed to this function since it contains the function - -bool pdo_sqlsrv_handle_env_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, - _In_opt_ va_list* print_args ); -bool pdo_sqlsrv_handle_dbh_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, - _In_opt_ va_list* print_args ); -bool pdo_sqlsrv_handle_stmt_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, - _In_opt_ va_list* print_args ); - -// common routine to transfer a sqlsrv_context's error to a PDO zval -void pdo_sqlsrv_retrieve_context_error( _In_ sqlsrv_error const* last_error, _Out_ zval* pdo_zval ); - -// reset the errors from the last operation -inline void pdo_reset_dbh_error( _Inout_ pdo_dbh_t* dbh TSRMLS_DC ) -{ - strcpy_s( dbh->error_code, sizeof( dbh->error_code ), "00000" ); // 00000 means no error - - // release the last statement from the dbh so that error handling won't have a statement passed to it - if( dbh->query_stmt ) { - dbh->query_stmt = NULL; - zval_ptr_dtor( &dbh->query_stmt_zval ); - } - - // if the driver isn't valid, just return (PDO calls close sometimes more than once?) - if( dbh->driver_data == NULL ) { - return; - } - - // reset the last error on the sqlsrv_context - sqlsrv_context* ctx = static_cast( dbh->driver_data ); - - if( ctx->last_error() ) { - ctx->last_error().reset(); - } -} - -#define PDO_RESET_DBH_ERROR pdo_reset_dbh_error( dbh TSRMLS_CC ); - -inline void pdo_reset_stmt_error( _Inout_ pdo_stmt_t* stmt ) -{ - strcpy_s( stmt->error_code, sizeof( stmt->error_code ), "00000" ); // 00000 means no error - - // if the driver isn't valid, just return (PDO calls close sometimes more than once?) - if( stmt->driver_data == NULL ) { - return; - } - - // reset the last error on the sqlsrv_context - sqlsrv_context* ctx = static_cast( stmt->driver_data ); - - if( ctx->last_error() ) { - ctx->last_error().reset(); - } -} - -#define PDO_RESET_STMT_ERROR pdo_reset_stmt_error( stmt ); - -// validate the driver objects -#define PDO_VALIDATE_CONN if( dbh->driver_data == NULL ) { DIE( "Invalid driver data in PDO object." ); } -#define PDO_VALIDATE_STMT if( stmt->driver_data == NULL ) { DIE( "Invalid driver data in PDOStatement object." ); } - - -//********************************************************************************************************************************* -// Utility Functions -//********************************************************************************************************************************* - -// List of PDO specific error messages. -enum PDO_ERROR_CODES { - - PDO_SQLSRV_ERROR_INVALID_DBH_ATTR = SQLSRV_ERROR_DRIVER_SPECIFIC, - PDO_SQLSRV_ERROR_INVALID_STMT_ATTR, - PDO_SQLSRV_ERROR_INVALID_ENCODING, - PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM, - PDO_SQLSRV_ERROR_PDO_STMT_UNSUPPORTED, - PDO_SQLSRV_ERROR_UNSUPPORTED_DBH_ATTR, - PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR, - PDO_SQLSRV_ERROR_READ_ONLY_DBH_ATTR, - PDO_SQLSRV_ERROR_INVALID_STMT_OPTION, - PDO_SQLSRV_ERROR_INVALID_CURSOR_TYPE, - PDO_SQLSRV_ERROR_FUNCTION_NOT_IMPLEMENTED, - PDO_SQLSRV_ERROR_PARAM_PARSE, - PDO_SQLSRV_ERROR_LAST_INSERT_ID, - PDO_SQLSRV_ERROR_INVALID_COLUMN_DRIVER_DATA, - PDO_SQLSRV_ERROR_COLUMN_TYPE_DOES_NOT_SUPPORT_ENCODING, - PDO_SQLSRV_ERROR_INVALID_DRIVER_COLUMN_ENCODING, - PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM_TYPE, - PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM_ENCODING, - PDO_SQLSRV_ERROR_INVALID_PARAM_DIRECTION, - PDO_SQLSRV_ERROR_INVALID_OUTPUT_STRING_SIZE, - PDO_SQLSRV_ERROR_CURSOR_ATTR_AT_PREPARE_ONLY, - PDO_SQLSRV_ERROR_INVALID_DSN_STRING, - PDO_SQLSRV_ERROR_INVALID_DSN_KEY, - PDO_SQLSRV_ERROR_INVALID_DSN_VALUE, - PDO_SQLSRV_ERROR_SERVER_NOT_SPECIFIED, - PDO_SQLSRV_ERROR_DSN_STRING_ENDED_UNEXPECTEDLY, - PDO_SQLSRV_ERROR_EXTRA_SEMI_COLON_IN_DSN_STRING, - SQLSRV_ERROR_UNESCAPED_RIGHT_BRACE_IN_DSN, - PDO_SQLSRV_ERROR_RCB_MISSING_IN_DSN_VALUE, - PDO_SQLSRV_ERROR_DQ_ATTR_AT_PREPARE_ONLY, - 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, - PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - PDO_SQLSRV_ERROR_CE_DIRECT_QUERY_UNSUPPORTED, - PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED -}; - -extern pdo_error PDO_ERRORS[]; - -#define THROW_PDO_ERROR( ctx, custom, ... ) \ - call_error_handler( ctx, custom TSRMLS_CC, false, ## __VA_ARGS__ ); \ - throw pdo::PDOException(); - -namespace pdo { - - // an error which occurred in our PDO driver, NOT an exception thrown by PDO - struct PDOException : public core::CoreException { - - PDOException() : CoreException() - { - } - }; - -} // namespace pdo - -// logger for pdo_sqlsrv called by the core layer when it wants to log something with the LOG macro -void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); - #endif /* PHP_PDO_SQLSRV_H */ - diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h new file mode 100644 index 00000000..4b50bbc7 --- /dev/null +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -0,0 +1,409 @@ +#ifndef PHP_PDO_SQLSRV_INT_H +#define PHP_PDO_SQLSRV_INT_H + +//--------------------------------------------------------------------------------------------------------------------------------- +// File: php_pdo_sqlsrv_int.h +// +// Contents: Internal declarations for the extension +// +// Microsoft Drivers 5.6 for PHP for SQL Server +// Copyright(c) Microsoft Corporation +// All rights reserved. +// MIT License +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +//--------------------------------------------------------------------------------------------------------------------------------- + +#include "core_sqlsrv.h" +#include "version.h" + +extern "C" { + #include "pdo/php_pdo.h" + #include "pdo/php_pdo_driver.h" +} + +#include +#include + +//********************************************************************************************************************************* +// Global variables +//********************************************************************************************************************************* + +// henv context for creating connections +extern sqlsrv_context* g_pdo_henv_cp; +extern sqlsrv_context* g_pdo_henv_ncp; + +// used for getting the version information +extern HMODULE g_sqlsrv_hmodule; + +// macros used to access the global variables. Use these to make global variable access agnostic to threads +#ifdef ZTS +#define PDO_SQLSRV_G(v) TSRMG(pdo_sqlsrv_globals_id, zend_pdo_sqlsrv_globals *, v) +#else +#define PDO_SQLSRV_G(v) pdo_sqlsrv_globals.v +#endif + +// INI settings and constants +// (these are defined as macros to allow concatenation as we do below) +#define INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE "client_buffer_max_kb_size" +#define INI_PDO_SQLSRV_LOG "log_severity" +#define INI_PREFIX "pdo_sqlsrv." + +PHP_INI_BEGIN() + STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, log_severity, + zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) + STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE , INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, + client_buffer_max_size, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) +PHP_INI_END() + + +//********************************************************************************************************************************* +// Constants and Types +//********************************************************************************************************************************* + +// sqlsrv driver specific PDO attributes +enum PDO_SQLSRV_ATTR { + + // The custom attributes for this driver: + SQLSRV_ATTR_ENCODING = PDO_ATTR_DRIVER_SPECIFIC, + SQLSRV_ATTR_QUERY_TIMEOUT, + SQLSRV_ATTR_DIRECT_QUERY, + SQLSRV_ATTR_CURSOR_SCROLL_TYPE, + SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, + SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, + SQLSRV_ATTR_FETCHES_DATETIME_TYPE, + SQLSRV_ATTR_FORMAT_DECIMALS, + SQLSRV_ATTR_DECIMAL_PLACES +}; + +// valid set of values for TransactionIsolation connection option +namespace PDOTxnIsolationValues { + + const char READ_UNCOMMITTED[] = "READ_UNCOMMITTED"; + const char READ_COMMITTED[] = "READ_COMMITTED"; + const char REPEATABLE_READ[] = "REPEATABLE_READ"; + const char SERIALIZABLE[] = "SERIALIZABLE"; + const char SNAPSHOT[] = "SNAPSHOT"; +} + + +//********************************************************************************************************************************* +// Initialization +//********************************************************************************************************************************* + +// 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( _In_ char c ); + bool discard_white_spaces(void); + void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC ); +}; + + +//********************************************************************************************************************************* +// PDO DSN Parser +//********************************************************************************************************************************* + +// Parser class used to parse DSN connection string. +class conn_string_parser : private string_parser +{ + enum States + { + FirstKeyValuePair, + Key, + Value, + ValueContent1, + ValueContent2, + RCBEncountered, + NextKeyValuePair, + }; + + private: + const char* current_key_name; + int discard_trailing_white_spaces( _In_reads_(len) const char* str, _Inout_ int len ); + void validate_key( _In_reads_(key_len) const char *key, _Inout_ int key_len TSRMLS_DC); + + protected: + void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC); + + public: + conn_string_parser( _In_ sqlsrv_context& ctx, _In_ const char* dsn, _In_ int len, _In_ 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( _In_ unsigned int value TSRMLS_DC ); + sql_string_parser(_In_ sqlsrv_context& ctx, _In_ const char* sql_str, _In_ int len, _In_ HashTable* placeholder_ht); + void parse_sql_string(TSRMLS_D); +}; + + +//********************************************************************************************************************************* +// Connection +//********************************************************************************************************************************* + +extern const connection_option PDO_CONN_OPTS[]; + +int pdo_sqlsrv_db_handle_factory( _Inout_ pdo_dbh_t *dbh, _In_opt_ zval *driver_options TSRMLS_DC); + +// a core layer pdo dbh object. This object inherits and overrides the statement factory +struct pdo_sqlsrv_dbh : public sqlsrv_conn { + + zval* stmts; + bool direct_query; + long query_timeout; + zend_long client_buffer_max_size; + bool fetch_numeric; + bool fetch_datetime; + bool format_decimals; + short decimal_places; + + pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ); +}; + + +//********************************************************************************************************************************* +// Statement +//********************************************************************************************************************************* + +struct stmt_option_encoding : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_pdo_scrollable : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_direct_query : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_cursor_scroll_type : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_emulate_prepares : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_fetch_numeric : public stmt_option_functor { + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +struct stmt_option_fetch_datetime : public stmt_option_functor { + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +extern struct pdo_stmt_methods pdo_sqlsrv_stmt_methods; + +// a core layer pdo stmt object. This object inherits and overrides the callbacks necessary +struct pdo_sqlsrv_stmt : public sqlsrv_stmt { + + pdo_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_ void* drv TSRMLS_DC ) : + sqlsrv_stmt( c, handle, e, drv TSRMLS_CC ), + direct_query( false ), + direct_query_subst_string( NULL ), + direct_query_subst_string_len( 0 ), + placeholders(NULL), + bound_column_param_types( NULL ), + fetch_numeric( false ), + fetch_datetime( false ) + { + pdo_sqlsrv_dbh* db = static_cast( c ); + direct_query = db->direct_query; + fetch_numeric = db->fetch_numeric; + fetch_datetime = db->fetch_datetime; + format_decimals = db->format_decimals; + decimal_places = db->decimal_places; + } + + virtual ~pdo_sqlsrv_stmt( void ); + + // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants + // for PDO, everything is a string, so we return SQLSRV_PHPTYPE_STRING for all SQL types + virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); + + 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 + + pdo_param_type* bound_column_param_types; + bool fetch_numeric; + bool fetch_datetime; +}; + + +//********************************************************************************************************************************* +// Error Handling Functions +//********************************************************************************************************************************* + +// represents the mapping between an error_code and the corresponding error message. +struct pdo_error { + + unsigned int error_code; + sqlsrv_error_const sqlsrv_error; +}; + +// called when an error occurs in the core layer. These routines are set as the error_callback in a +// context. The context is passed to this function since it contains the function + +bool pdo_sqlsrv_handle_env_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, + _In_opt_ va_list* print_args ); +bool pdo_sqlsrv_handle_dbh_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, + _In_opt_ va_list* print_args ); +bool pdo_sqlsrv_handle_stmt_error( _Inout_ sqlsrv_context& ctx, _In_opt_ unsigned int sqlsrv_error_code, _In_opt_ bool warning TSRMLS_DC, + _In_opt_ va_list* print_args ); + +// common routine to transfer a sqlsrv_context's error to a PDO zval +void pdo_sqlsrv_retrieve_context_error( _In_ sqlsrv_error const* last_error, _Out_ zval* pdo_zval ); + +// reset the errors from the last operation +inline void pdo_reset_dbh_error( _Inout_ pdo_dbh_t* dbh TSRMLS_DC ) +{ + strcpy_s( dbh->error_code, sizeof( dbh->error_code ), "00000" ); // 00000 means no error + + // release the last statement from the dbh so that error handling won't have a statement passed to it + if( dbh->query_stmt ) { + dbh->query_stmt = NULL; + zval_ptr_dtor( &dbh->query_stmt_zval ); + } + + // if the driver isn't valid, just return (PDO calls close sometimes more than once?) + if( dbh->driver_data == NULL ) { + return; + } + + // reset the last error on the sqlsrv_context + sqlsrv_context* ctx = static_cast( dbh->driver_data ); + + if( ctx->last_error() ) { + ctx->last_error().reset(); + } +} + +#define PDO_RESET_DBH_ERROR pdo_reset_dbh_error( dbh TSRMLS_CC ); + +inline void pdo_reset_stmt_error( _Inout_ pdo_stmt_t* stmt ) +{ + strcpy_s( stmt->error_code, sizeof( stmt->error_code ), "00000" ); // 00000 means no error + + // if the driver isn't valid, just return (PDO calls close sometimes more than once?) + if( stmt->driver_data == NULL ) { + return; + } + + // reset the last error on the sqlsrv_context + sqlsrv_context* ctx = static_cast( stmt->driver_data ); + + if( ctx->last_error() ) { + ctx->last_error().reset(); + } +} + +#define PDO_RESET_STMT_ERROR pdo_reset_stmt_error( stmt ); + +// validate the driver objects +#define PDO_VALIDATE_CONN if( dbh->driver_data == NULL ) { DIE( "Invalid driver data in PDO object." ); } +#define PDO_VALIDATE_STMT if( stmt->driver_data == NULL ) { DIE( "Invalid driver data in PDOStatement object." ); } + + +//********************************************************************************************************************************* +// Utility Functions +//********************************************************************************************************************************* + +// List of PDO specific error messages. +enum PDO_ERROR_CODES { + + PDO_SQLSRV_ERROR_INVALID_DBH_ATTR = SQLSRV_ERROR_DRIVER_SPECIFIC, + PDO_SQLSRV_ERROR_INVALID_STMT_ATTR, + PDO_SQLSRV_ERROR_INVALID_ENCODING, + PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM, + PDO_SQLSRV_ERROR_PDO_STMT_UNSUPPORTED, + PDO_SQLSRV_ERROR_UNSUPPORTED_DBH_ATTR, + PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR, + PDO_SQLSRV_ERROR_READ_ONLY_DBH_ATTR, + PDO_SQLSRV_ERROR_INVALID_STMT_OPTION, + PDO_SQLSRV_ERROR_INVALID_CURSOR_TYPE, + PDO_SQLSRV_ERROR_FUNCTION_NOT_IMPLEMENTED, + PDO_SQLSRV_ERROR_PARAM_PARSE, + PDO_SQLSRV_ERROR_LAST_INSERT_ID, + PDO_SQLSRV_ERROR_INVALID_COLUMN_DRIVER_DATA, + PDO_SQLSRV_ERROR_COLUMN_TYPE_DOES_NOT_SUPPORT_ENCODING, + PDO_SQLSRV_ERROR_INVALID_DRIVER_COLUMN_ENCODING, + PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM_TYPE, + PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM_ENCODING, + PDO_SQLSRV_ERROR_INVALID_PARAM_DIRECTION, + PDO_SQLSRV_ERROR_INVALID_OUTPUT_STRING_SIZE, + PDO_SQLSRV_ERROR_CURSOR_ATTR_AT_PREPARE_ONLY, + PDO_SQLSRV_ERROR_INVALID_DSN_STRING, + PDO_SQLSRV_ERROR_INVALID_DSN_KEY, + PDO_SQLSRV_ERROR_INVALID_DSN_VALUE, + PDO_SQLSRV_ERROR_SERVER_NOT_SPECIFIED, + PDO_SQLSRV_ERROR_DSN_STRING_ENDED_UNEXPECTEDLY, + PDO_SQLSRV_ERROR_EXTRA_SEMI_COLON_IN_DSN_STRING, + SQLSRV_ERROR_UNESCAPED_RIGHT_BRACE_IN_DSN, + PDO_SQLSRV_ERROR_RCB_MISSING_IN_DSN_VALUE, + PDO_SQLSRV_ERROR_DQ_ATTR_AT_PREPARE_ONLY, + 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, + PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, + PDO_SQLSRV_ERROR_CE_DIRECT_QUERY_UNSUPPORTED, + PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED +}; + +extern pdo_error PDO_ERRORS[]; + +#define THROW_PDO_ERROR( ctx, custom, ... ) \ + call_error_handler( ctx, custom TSRMLS_CC, false, ## __VA_ARGS__ ); \ + throw pdo::PDOException(); + +namespace pdo { + + // an error which occurred in our PDO driver, NOT an exception thrown by PDO + struct PDOException : public core::CoreException { + + PDOException() : CoreException() + { + } + }; + +} // namespace pdo + +// logger for pdo_sqlsrv called by the core layer when it wants to log something with the LOG macro +void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); + + +#endif /* PHP_PDO_SQLSRV_INT_H */ diff --git a/source/pdo_sqlsrv/template.rc b/source/pdo_sqlsrv/template.rc index 7b74950a..fdbeaa57 100644 --- a/source/pdo_sqlsrv/template.rc +++ b/source/pdo_sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.cpp b/source/shared/FormattedPrint.cpp index 0e3beadd..b664159c 100644 --- a/source/shared/FormattedPrint.cpp +++ b/source/shared/FormattedPrint.cpp @@ -6,7 +6,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.h b/source/shared/FormattedPrint.h index 0c29e81f..907b1647 100644 --- a/source/shared/FormattedPrint.h +++ b/source/shared/FormattedPrint.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.cpp b/source/shared/StringFunctions.cpp index 6f494dc4..6aac5a05 100644 --- a/source/shared/StringFunctions.cpp +++ b/source/shared/StringFunctions.cpp @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.h b/source/shared/StringFunctions.h index bcd250b6..a2bfceaa 100644 --- a/source/shared/StringFunctions.h +++ b/source/shared/StringFunctions.h @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 8ec79e0c..3d195fe9 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use connection handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -730,7 +730,7 @@ bool core_is_authentication_option_valid( _In_z_ const char* value, _In_ size_t if (value_len <= 0) return false; - if( ! stricmp( value, AzureADOptions::AZURE_AUTH_SQL_PASSWORD ) || ! stricmp( value, AzureADOptions::AZURE_AUTH_AD_PASSWORD ) ) { + if (!stricmp(value, AzureADOptions::AZURE_AUTH_SQL_PASSWORD) || !stricmp(value, AzureADOptions::AZURE_AUTH_AD_PASSWORD) || !stricmp(value, AzureADOptions::AZURE_AUTH_AD_MSI)) { return true; } @@ -769,16 +769,18 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou bool mars_mentioned = false; connection_option const* conn_opt; bool access_token_used = false; + bool authentication_option_used = zend_hash_index_exists(options, SQLSRV_CONN_OPTION_AUTHENTICATION); try { - // First of all, check if access token is specified. If so, check if UID,PWD,Authentication exist + // Since connection options access token and authentication cannot coexist, check if both of them are used. + // If access token is specified, check UID andPWD as well. // No need to check the keyword Trusted_Connectionbecause it is not among the acceptable options for SQLSRV drivers if (zend_hash_index_exists(options, SQLSRV_CONN_OPTION_ACCESS_TOKEN)) { bool invalidOptions = false; // UID and PWD have to be NULLs... throw an exception as long as the user has specified any of them in the connection string, // even if they may be empty strings. Likewise if the keyword Authentication exists - if (uid != NULL || pwd != NULL || zend_hash_index_exists(options, SQLSRV_CONN_OPTION_AUTHENTICATION)) { + if (uid != NULL || pwd != NULL || authentication_option_used) { invalidOptions = true; } @@ -789,11 +791,44 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou access_token_used = true; } + // Check if Authentication is ActiveDirectoryMSI + // https://docs.microsoft.com/en-ca/azure/active-directory/managed-identities-azure-resources/overview + bool activeDirectoryMSI = false; + if (authentication_option_used) { + zval* auth_option = NULL; + auth_option = zend_hash_index_find(options, SQLSRV_CONN_OPTION_AUTHENTICATION); + + char* option = Z_STRVAL_P(auth_option); + + if (!stricmp(option, AzureADOptions::AZURE_AUTH_AD_MSI)) { + activeDirectoryMSI = true; + + // There are two types of managed identities: + // (1) A system-assigned managed identity: UID must be NULL + // (2) A user-assigned managed identity: UID defined but must not be an empty string + // In both cases, PWD must be NULL + + bool invalid = false; + if (pwd != NULL) { + invalid = true; + } else { + if (uid != NULL && strnlen_s(uid) == 0) { + invalid = true; + } + } + + CHECK_CUSTOM_ERROR(invalid, conn, SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL ) { + throw core::CoreException(); + } + } + } + // Add the server name common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC ); - - // if uid is not present then we use trusted connection -- but not when access token is used, because they are incompatible - if (!access_token_used) { + + // If uid is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, + // because they are incompatible + if (!access_token_used && !activeDirectoryMSI) { if (uid == NULL || strnlen_s(uid) == 0) { connection_string += CONNECTION_OPTION_NO_CREDENTIALS; // "Trusted_Connection={Yes};" } diff --git a/source/shared/core_init.cpp b/source/shared/core_init.cpp index ea604e79..504a145b 100644 --- a/source/shared/core_init.cpp +++ b/source/shared/core_init.cpp @@ -3,7 +3,7 @@ // // Contents: common initialization routines shared by PDO and sqlsrv // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -159,20 +159,3 @@ void core_sqlsrv_mshutdown( _Inout_ sqlsrv_context& henv_cp, _Inout_ sqlsrv_cont return; } - -// DllMain for the extension. -#ifdef _WIN32 -BOOL WINAPI DllMain( _In_ HINSTANCE hinstDLL, _In_ DWORD fdwReason, LPVOID ) -{ - switch( fdwReason ) { - case DLL_PROCESS_ATTACH: - // store the module handle for use by client_info and server_info - g_sqlsrv_hmodule = hinstDLL; - break; - default: - break; - } - - return TRUE; -} -#endif diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index f4f00b37..93427bd7 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -3,7 +3,7 @@ // // Contents: Result sets // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 400e9ea2..886d5e82 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Core routines and constants shared by the Microsoft Drivers for PHP for SQL Server // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -191,6 +191,7 @@ const int SQL_SERVER_2008_DEFAULT_DATETIME_SCALE = 7; namespace AzureADOptions { const char AZURE_AUTH_SQL_PASSWORD[] = "SqlPassword"; const char AZURE_AUTH_AD_PASSWORD[] = "ActiveDirectoryPassword"; + const char AZURE_AUTH_AD_MSI[] = "ActiveDirectoryMsi"; } // the message returned by ODBC Driver for SQL Server @@ -1777,6 +1778,7 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_INVALID_OPTION_WITH_ACCESS_TOKEN, SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, SQLSRV_ERROR_INVALID_DECIMAL_PLACES, + SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 3b263fe2..9cac96a5 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use statement handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -2770,10 +2770,10 @@ void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* { SQLSRV_ASSERT( column_size != SQLSRV_UNKNOWN_SIZE, "column size should be set to a known value." ); buffer_len = Z_STRLEN_P( param_z ); + SQLLEN original_len = buffer_len; SQLLEN expected_len; SQLLEN buffer_null_extra; SQLLEN elem_size; - SQLLEN without_null_len; // calculate the size of each 'element' represented by column_size. WCHAR is of course 2, // as is a n(var)char/ntext field being returned as a binary field. @@ -2801,9 +2801,6 @@ void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* // binary fields aren't null terminated, so we need to account for that in our buffer length calcuations buffer_null_extra = (c_type == SQL_C_BINARY) ? elem_size : 0; - // this is the size of the string for Zend and for the StrLen parameter to SQLBindParameter - without_null_len = field_size * elem_size; - // increment to include the null terminator since the Zend length doesn't include the null terminator buffer_len += elem_size; @@ -2821,8 +2818,10 @@ void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* // A zval string len doesn't include the null. This calculates the length it should be // regardless of whether the ODBC type contains the NULL or not. - // null terminate the string to avoid a warning in debug PHP builds - ZSTR_VAL(param_z_string)[without_null_len] = '\0'; + // initialize the newly allocated space + char *p = ZSTR_VAL(param_z_string); + p = p + original_len; + memset(p, '\0', expected_len - original_len); ZVAL_NEW_STR(param_z, param_z_string); // buffer_len is the length passed to SQLBindParameter. It must contain the space for NULL in the diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index 826fcf2e..d822d4a8 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -3,7 +3,7 @@ // // Contents: Implementation of PHP streams for reading SQL Server data // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 4aa2b37f..515eb38b 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/globalization.h b/source/shared/globalization.h index f3545de2..4ddccc52 100644 --- a/source/shared/globalization.h +++ b/source/shared/globalization.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic.h b/source/shared/interlockedatomic.h index 953c2580..f46e2b1d 100644 --- a/source/shared/interlockedatomic.h +++ b/source/shared/interlockedatomic.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic_gcc.h b/source/shared/interlockedatomic_gcc.h index ba5087b5..8a1f3732 100644 --- a/source/shared/interlockedatomic_gcc.h +++ b/source/shared/interlockedatomic_gcc.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedslist.h b/source/shared/interlockedslist.h index 76e2eda8..cd71452a 100644 --- a/source/shared/interlockedslist.h +++ b/source/shared/interlockedslist.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, singly // linked list. // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index 6328f1fb..a572ab02 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -3,7 +3,7 @@ // // Contents: Contains portable classes for localization // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index 38ee64b7..e67bbf22 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -5,7 +5,7 @@ // Must be included in one c/cpp file per binary // A build error will occur if this inclusion policy is not followed // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -638,7 +638,7 @@ size_t SystemLocale::ToUtf16( UINT srcCodePage, const char * src, SSIZE_T cchSrc return 0; } size_t cchSrcActual = (cchSrc < 0 ? (1+strnlen_s(src)) : cchSrc); - bool hasLoss; + bool hasLoss = false; return cvt.Convert( dest, cchDest, src, cchSrcActual, false, &hasLoss, pErrorCode ); } @@ -664,7 +664,7 @@ size_t SystemLocale::ToUtf16Strict( UINT srcCodePage, const char * src, SSIZE_T return 0; } size_t cchSrcActual = (cchSrc < 0 ? (1+strnlen_s(src)) : cchSrc); - bool hasLoss; + bool hasLoss = false; return cvt.Convert( dest, cchDest, src, cchSrcActual, true, &hasLoss, pErrorCode ); } @@ -952,7 +952,7 @@ size_t SystemLocale::FromUtf16( UINT destCodePage, const WCHAR * src, SSIZE_T cc return 0; } size_t cchSrcActual = (cchSrc < 0 ? (1+mplat_wcslen(src)) : cchSrc); - bool hasLoss; + bool hasLoss = false; return cvt.Convert( dest, cchDest, src, cchSrcActual, false, &hasLoss, pErrorCode ); } @@ -972,7 +972,7 @@ size_t SystemLocale::FromUtf16Strict(UINT destCodePage, const WCHAR * src, SSIZE return 0; } size_t cchSrcActual = (cchSrc < 0 ? (1 + mplat_wcslen(src)) : cchSrc); - bool hasLoss; + bool hasLoss = false; return cvt.Convert(dest, cchDest, src, cchSrcActual, true, &hasLoss, pErrorCode); } diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index b440a95a..2392c5e3 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -20,7 +20,7 @@ // pecuniary loss) arising out of the use of or inability to use // this SDK, even if Microsoft has been advised of the possibility // of such damages. -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/sal_def.h b/source/shared/sal_def.h index b000b640..053fb199 100644 --- a/source/shared/sal_def.h +++ b/source/shared/sal_def.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/typedefs_for_linux.h b/source/shared/typedefs_for_linux.h index d035e366..390d03cc 100644 --- a/source/shared/typedefs_for_linux.h +++ b/source/shared/typedefs_for_linux.h @@ -1,7 +1,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- // File: typedefs_for_linux.h // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/version.h b/source/shared/version.h index 256fb51d..c7c55481 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -4,7 +4,7 @@ // File: version.h // Contents: Version number constants // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -26,12 +26,12 @@ // Increase Minor with backward compatible new functionalities and API changes. // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 -#define SQLVERSION_MINOR 5 +#define SQLVERSION_MINOR 6 #define SQLVERSION_PATCH 0 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 -#define PREVIEW 1 +#define PREVIEW 0 #define SEMVER_PRERELEASE // Semantic versioning build metadata, build meta data is not counted in precedence order. diff --git a/source/shared/xplat.h b/source/shared/xplat.h index bb4888ea..1b195639 100644 --- a/source/shared/xplat.h +++ b/source/shared/xplat.h @@ -3,7 +3,7 @@ // // Contents: include for definition of Windows types for non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_intsafe.h b/source/shared/xplat_intsafe.h index 571c62b2..91db2652 100644 --- a/source/shared/xplat_intsafe.h +++ b/source/shared/xplat_intsafe.h @@ -4,7 +4,7 @@ // Contents: This module defines helper functions to prevent // integer overflow bugs. // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winerror.h b/source/shared/xplat_winerror.h index 88a97baa..34e7ffb5 100644 --- a/source/shared/xplat_winerror.h +++ b/source/shared/xplat_winerror.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winnls.h b/source/shared/xplat_winnls.h index 7bdac15e..bc81cf58 100644 --- a/source/shared/xplat_winnls.h +++ b/source/shared/xplat_winnls.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index 5af9e7cd..20d4d858 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.5 for PHP for SQL Server +dnl Microsoft Drivers 5.6 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License @@ -40,7 +40,10 @@ if test "$PHP_SQLSRV" != "no"; then shared/StringFunctions.cpp \ " AC_MSG_CHECKING([for SQLSRV headers]) - if test -f $srcdir/ext/sqlsrv/shared/core_sqlsrv.h; then + if test -f $srcdir/ext/pdo_sqlsrv/shared/core_sqlsrv.h && test "$PHP_PDO_SQLSRV" != "no" && test "$PHP_PDO_SQLSRV_SHARED" == "no"; then + pdo_sqlsrv_inc_path=$srcdir/ext/pdo_sqlsrv/shared/ + shared_src_class="" + elif test -f $srcdir/ext/sqlsrv/shared/core_sqlsrv.h; then sqlsrv_inc_path=$srcdir/ext/sqlsrv/shared/ elif test -f $srcdir/shared/core_sqlsrv.h; then sqlsrv_inc_path=$srcdir/shared/ diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index dc9f0b5f..5a2477e9 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -27,7 +27,9 @@ if( PHP_SQLSRV != "no" ) { if (CHECK_LIB("odbc32.lib", "sqlsrv") && CHECK_LIB("odbccp32.lib", "sqlsrv") && CHECK_LIB("version.lib", "sqlsrv") && CHECK_LIB("psapi.lib", "sqlsrv")&& CHECK_HEADER_ADD_INCLUDE( "core_sqlsrv.h", "CFLAGS_SQLSRV", configure_module_dirname + "\\shared")) { - ADD_SOURCES( configure_module_dirname + "\\shared", shared_src_class, "sqlsrv" ); + if (PHP_PDO_SQLSRV == "no" || PHP_SQLSRV_SHARED) { + ADD_SOURCES( configure_module_dirname + "\\shared", shared_src_class, "sqlsrv" ); + } CHECK_HEADER_ADD_INCLUDE("sql.h", "CFLAGS_SQLSRV_ODBC"); CHECK_HEADER_ADD_INCLUDE("sqlext.h", "CFLAGS_SQLSRV_ODBC"); ADD_FLAG( "LDFLAGS_SQLSRV", "/NXCOMPAT /DYNAMICBASE /debug /guard:cf" ); diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 1de0c6b6..532934f7 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use connection handles // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -17,7 +17,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_sqlsrv.h" +extern "C" { + #include "php_sqlsrv.h" +} + +#include "php_sqlsrv_int.h" #include #include diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index 9079ac75..80014524 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -2,7 +2,7 @@ // File: init.cpp // Contents: initialization routines for the extension // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -16,12 +16,18 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_sqlsrv.h" +extern "C" { + #include "php_sqlsrv.h" +} +#include "php_sqlsrv_int.h" + +#ifdef COMPILE_DL_SQLSRV #ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE(); #endif ZEND_GET_MODULE(g_sqlsrv) +#endif extern "C" { @@ -685,3 +691,23 @@ PHP_MINFO_FUNCTION(sqlsrv) php_info_print_table_end(); DISPLAY_INI_ENTRIES(); } + +// DllMain for the extension. +#ifdef _WIN32 +// Only needed if extension is built shared +#ifdef COMPILE_DL_SQLSRV +BOOL WINAPI DllMain( _In_ HINSTANCE hinstDLL, _In_ DWORD fdwReason, LPVOID ) +{ + switch( fdwReason ) { + case DLL_PROCESS_ATTACH: + // store the module handle for use by client_info and server_info + g_sqlsrv_hmodule = hinstDLL; + break; + default: + break; + } + + return TRUE; +} +#endif +#endif diff --git a/source/sqlsrv/php_sqlsrv.h b/source/sqlsrv/php_sqlsrv.h index f60c6c84..4de7f5c9 100644 --- a/source/sqlsrv/php_sqlsrv.h +++ b/source/sqlsrv/php_sqlsrv.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -22,248 +22,12 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "core_sqlsrv.h" -#include "version.h" - -#ifdef HAVE_CONFIG_H -#include "config.h" -#endif - -#ifdef PHP_WIN32 -#define PHP_SQLSRV_API __declspec(dllexport) -#else -#define PHP_SQLSRV_API -#endif - -// OACR is an internal Microsoft static code analysis tool -#if defined(OACR) -#include -OACR_WARNING_PUSH -OACR_WARNING_DISABLE( ALLOC_SIZE_OVERFLOW, "Third party code." ) -OACR_WARNING_DISABLE( INDEX_NEGATIVE, "Third party code." ) -OACR_WARNING_DISABLE( UNANNOTATED_BUFFER, "Third party code." ) -OACR_WARNING_DISABLE( INDEX_UNDERFLOW, "Third party code." ) -OACR_WARNING_DISABLE( REALLOCLEAK, "Third party code." ) -#endif - -extern "C" { - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning( disable: 4005 4100 4127 4142 4244 4505 4530 ) -#endif - -#ifdef ZTS -#include "TSRM.h" -#endif - -#if _MSC_VER >= 1400 -// typedef and macro to prevent a conflict between php.h and ws2tcpip.h. -// php.h defines this constant as unsigned int which causes a compile error -// in ws2tcpip.h. Fortunately php.h allows an override by defining -// HAVE_SOCKLEN_T. Since ws2tcpip.h isn't included until later, we define -// socklen_t here and override the php.h version. -typedef int socklen_t; -#define HAVE_SOCKLEN_T -#endif - -#if defined(_MSC_VER) -#pragma warning(pop) -#endif - -#if PHP_MAJOR_VERSION < 7 -#error Trying to compile "Microsoft Drivers for PHP for SQL Server (SQLSRV Driver)" with an unsupported version of PHP -#endif - -#if ZEND_DEBUG -// debug build causes warning C4505 to pop up from the Zend header files -#pragma warning( disable: 4505 ) -#endif - -} // extern "C" - -//********************************************************************************************************************************* -// Initialization Functions -//********************************************************************************************************************************* - -// module global variables (initialized in minit and freed in mshutdown) -extern HashTable* g_ss_errors_ht; -extern HashTable* g_ss_encodings_ht; -extern HashTable* g_ss_warnings_to_ignore_ht; - -// variables set during initialization -extern zend_module_entry g_sqlsrv_module_entry; // describes the extension to PHP -extern HMODULE g_sqlsrv_hmodule; // used for getting the version information - -// henv context for creating connections -extern sqlsrv_context* g_ss_henv_cp; -extern sqlsrv_context* g_ss_henv_ncp; - -extern bool isVistaOrGreater; // used to determine if OS is Vista or Greater - -#define phpext_sqlsrv_ptr &g_sqlsrv_module_entry - -// module initialization -PHP_MINIT_FUNCTION(sqlsrv); -// module shutdown function -PHP_MSHUTDOWN_FUNCTION(sqlsrv); -// request initialization function -PHP_RINIT_FUNCTION(sqlsrv); -// request shutdown function -PHP_RSHUTDOWN_FUNCTION(sqlsrv); -// module info function (info returned by phpinfo()) -PHP_MINFO_FUNCTION(sqlsrv); - -//********************************************************************************************************************************* -// Connection -//********************************************************************************************************************************* -PHP_FUNCTION(sqlsrv_connect); -PHP_FUNCTION(sqlsrv_begin_transaction); -PHP_FUNCTION(sqlsrv_client_info); -PHP_FUNCTION(sqlsrv_close); -PHP_FUNCTION(sqlsrv_commit); -PHP_FUNCTION(sqlsrv_query); -PHP_FUNCTION(sqlsrv_prepare); -PHP_FUNCTION(sqlsrv_rollback); -PHP_FUNCTION(sqlsrv_server_info); - -struct ss_sqlsrv_conn : sqlsrv_conn -{ - HashTable* stmts; - bool date_as_string; - bool format_decimals; // flag set to turn on formatting for values of decimal / numeric types - short decimal_places; // number of decimal digits to show in a result set unless format_numbers is false - bool in_transaction; // flag set when inside a transaction and used for checking validity of tran API calls - - // static variables used in process_params - static const char* resource_name; - static int descriptor; - - // initialize with default values - ss_sqlsrv_conn( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* drv TSRMLS_DC ) : - sqlsrv_conn( h, e, drv, SQLSRV_ENCODING_SYSTEM TSRMLS_CC ), - stmts( NULL ), - date_as_string( false ), - format_decimals( false ), - decimal_places( NO_CHANGE_DECIMAL_PLACES ), - in_transaction( false ) - { - } -}; - -// resource destructor -void __cdecl sqlsrv_conn_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ); - -//********************************************************************************************************************************* -// Statement -//********************************************************************************************************************************* - -// holds the field names for reuse by sqlsrv_fetch_array/object as keys -struct sqlsrv_fetch_field_name { - char* name; - SQLLEN len; -}; - -struct stmt_option_ss_scrollable : public stmt_option_functor { - - virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); -}; - -// This object inherits and overrides the callbacks necessary -struct ss_sqlsrv_stmt : public sqlsrv_stmt { - - ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_ void* drv TSRMLS_DC ); - - virtual ~ss_sqlsrv_stmt( void ); - - void new_result_set( TSRMLS_D ); - - // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants - sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); - - bool prepared; // whether the statement has been prepared yet (used for error messages) - zend_ulong conn_index; // index into the connection hash that contains this statement structure - zval* params_z; // hold parameters passed to sqlsrv_prepare but not used until sqlsrv_execute - sqlsrv_fetch_field_name* fetch_field_names; // field names for current results used by sqlsrv_fetch_array/object as keys - int fetch_fields_count; - - // static variables used in process_params - static const char* resource_name; - static int descriptor; - -}; - -// holds the field names for reuse by sqlsrv_fetch_array/object as keys -struct sqlsrv_fetch_field { - char* name; - unsigned int len; -}; - -// holds the stream param and the encoding that it was assigned -struct sqlsrv_stream_encoding { - zval* stream_z; - unsigned int encoding; - - sqlsrv_stream_encoding( _In_ zval* str_z, _In_ unsigned int enc ) : - stream_z( str_z ), encoding( enc ) - { - } -}; - -// *** statement functions *** -PHP_FUNCTION(sqlsrv_cancel); -PHP_FUNCTION(sqlsrv_execute); -PHP_FUNCTION(sqlsrv_fetch); -PHP_FUNCTION(sqlsrv_fetch_array); -PHP_FUNCTION(sqlsrv_fetch_object); -PHP_FUNCTION(sqlsrv_field_metadata); -PHP_FUNCTION(sqlsrv_free_stmt); -PHP_FUNCTION(sqlsrv_get_field); -PHP_FUNCTION(sqlsrv_has_rows); -PHP_FUNCTION(sqlsrv_next_result); -PHP_FUNCTION(sqlsrv_num_fields); -PHP_FUNCTION(sqlsrv_num_rows); -PHP_FUNCTION(sqlsrv_rows_affected); -PHP_FUNCTION(sqlsrv_send_stream_data); - -// resource destructor -void __cdecl sqlsrv_stmt_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ); - -// "internal" statement functions shared by functions in conn.cpp and stmt.cpp -void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ); -bool sqlsrv_stmt_common_execute( sqlsrv_stmt* s, const SQLCHAR* sql_string, int sql_len, bool direct, const char* function - TSRMLS_DC ); -void free_odbc_resources( ss_sqlsrv_stmt* stmt TSRMLS_DC ); -void free_stmt_resource( _Inout_ zval* stmt_z TSRMLS_DC ); - -//********************************************************************************************************************************* -// Type Functions -//********************************************************************************************************************************* - -// type functions for SQL types. -// to expose SQL Server paramterized types, we use functions that return encoded integers that contain the size/precision etc. -// for example, SQLSRV_SQLTYPE_VARCHAR(4000) matches the usage of SQLSRV_SQLTYPE_INT with the size added. -PHP_FUNCTION(SQLSRV_SQLTYPE_BINARY); -PHP_FUNCTION(SQLSRV_SQLTYPE_CHAR); -PHP_FUNCTION(SQLSRV_SQLTYPE_DECIMAL); -PHP_FUNCTION(SQLSRV_SQLTYPE_NCHAR); -PHP_FUNCTION(SQLSRV_SQLTYPE_NUMERIC); -PHP_FUNCTION(SQLSRV_SQLTYPE_NVARCHAR); -PHP_FUNCTION(SQLSRV_SQLTYPE_VARBINARY); -PHP_FUNCTION(SQLSRV_SQLTYPE_VARCHAR); - -// PHP type functions -// strings and streams may have an encoding parameterized, so we use the functions -// the valid encodings are SQLSRV_ENC_BINARY and SQLSRV_ENC_CHAR. -PHP_FUNCTION(SQLSRV_PHPTYPE_STREAM); -PHP_FUNCTION(SQLSRV_PHPTYPE_STRING); +#include "php.h" //********************************************************************************************************************************* // Global variables //********************************************************************************************************************************* -extern "C" { - // request level variables ZEND_BEGIN_MODULE_GLOBALS(sqlsrv) @@ -282,8 +46,6 @@ ZEND_END_MODULE_GLOBALS(sqlsrv) ZEND_EXTERN_MODULE_GLOBALS(sqlsrv); -} - // macro used to access the global variables. Use it to make global variable access agnostic to threads #define SQLSRV_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(sqlsrv, v) @@ -291,335 +53,83 @@ ZEND_EXTERN_MODULE_GLOBALS(sqlsrv); ZEND_TSRMLS_CACHE_EXTERN(); #endif -// INI settings and constants -// (these are defined as macros to allow concatenation as we do below) -#define INI_WARNINGS_RETURN_AS_ERRORS "WarningsReturnAsErrors" -#define INI_LOG_SEVERITY "LogSeverity" -#define INI_LOG_SUBSYSTEMS "LogSubsystems" -#define INI_BUFFERED_QUERY_LIMIT "ClientBufferMaxKBSize" -#define INI_PREFIX "sqlsrv." - -PHP_INI_BEGIN() - STD_PHP_INI_BOOLEAN( INI_PREFIX INI_WARNINGS_RETURN_AS_ERRORS , "1", PHP_INI_ALL, OnUpdateBool, warnings_return_as_errors, - zend_sqlsrv_globals, sqlsrv_globals ) - STD_PHP_INI_ENTRY( INI_PREFIX INI_LOG_SEVERITY, "0", PHP_INI_ALL, OnUpdateLong, log_severity, zend_sqlsrv_globals, - sqlsrv_globals ) - STD_PHP_INI_ENTRY( INI_PREFIX INI_LOG_SUBSYSTEMS, "0", PHP_INI_ALL, OnUpdateLong, log_subsystems, zend_sqlsrv_globals, - sqlsrv_globals ) - STD_PHP_INI_ENTRY( INI_PREFIX INI_BUFFERED_QUERY_LIMIT, INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, buffered_query_limit, - zend_sqlsrv_globals, sqlsrv_globals ) -PHP_INI_END() //********************************************************************************************************************************* -// Configuration +// Initialization Functions //********************************************************************************************************************************* + +// variables set during initialization +extern zend_module_entry g_sqlsrv_module_entry; // describes the extension to PHP + +#define phpext_sqlsrv_ptr &g_sqlsrv_module_entry + +// module initialization +PHP_MINIT_FUNCTION(sqlsrv); +// module shutdown function +PHP_MSHUTDOWN_FUNCTION(sqlsrv); +// request initialization function +PHP_RINIT_FUNCTION(sqlsrv); +// request shutdown function +PHP_RSHUTDOWN_FUNCTION(sqlsrv); +// module info function (info returned by phpinfo()) +PHP_MINFO_FUNCTION(sqlsrv); + + +//********************************************************************************************************************************* +// Functions +//********************************************************************************************************************************* + +PHP_FUNCTION(sqlsrv_connect); +PHP_FUNCTION(sqlsrv_begin_transaction); +PHP_FUNCTION(sqlsrv_client_info); +PHP_FUNCTION(sqlsrv_close); +PHP_FUNCTION(sqlsrv_commit); +PHP_FUNCTION(sqlsrv_query); +PHP_FUNCTION(sqlsrv_prepare); +PHP_FUNCTION(sqlsrv_rollback); +PHP_FUNCTION(sqlsrv_server_info); + +PHP_FUNCTION(sqlsrv_cancel); +PHP_FUNCTION(sqlsrv_execute); +PHP_FUNCTION(sqlsrv_fetch); +PHP_FUNCTION(sqlsrv_fetch_array); +PHP_FUNCTION(sqlsrv_fetch_object); +PHP_FUNCTION(sqlsrv_field_metadata); +PHP_FUNCTION(sqlsrv_free_stmt); +PHP_FUNCTION(sqlsrv_get_field); +PHP_FUNCTION(sqlsrv_has_rows); +PHP_FUNCTION(sqlsrv_next_result); +PHP_FUNCTION(sqlsrv_num_fields); +PHP_FUNCTION(sqlsrv_num_rows); +PHP_FUNCTION(sqlsrv_rows_affected); +PHP_FUNCTION(sqlsrv_send_stream_data); + +// type functions for SQL types. +// to expose SQL Server paramterized types, we use functions that return encoded integers that contain the size/precision etc. +// for example, SQLSRV_SQLTYPE_VARCHAR(4000) matches the usage of SQLSRV_SQLTYPE_INT with the size added. +PHP_FUNCTION(SQLSRV_SQLTYPE_BINARY); +PHP_FUNCTION(SQLSRV_SQLTYPE_CHAR); +PHP_FUNCTION(SQLSRV_SQLTYPE_DECIMAL); +PHP_FUNCTION(SQLSRV_SQLTYPE_NCHAR); +PHP_FUNCTION(SQLSRV_SQLTYPE_NUMERIC); +PHP_FUNCTION(SQLSRV_SQLTYPE_NVARCHAR); +PHP_FUNCTION(SQLSRV_SQLTYPE_VARBINARY); +PHP_FUNCTION(SQLSRV_SQLTYPE_VARCHAR); + +// PHP type functions +// strings and streams may have an encoding parameterized, so we use the functions +// the valid encodings are SQLSRV_ENC_BINARY and SQLSRV_ENC_CHAR. +PHP_FUNCTION(SQLSRV_PHPTYPE_STREAM); +PHP_FUNCTION(SQLSRV_PHPTYPE_STRING); + // These functions set and retrieve configuration settings. Configuration settings defined are: // WarningsReturnAsErrors - treat all ODBC warnings as errors and return false from sqlsrv APIs. // LogSeverity - combination of severity of messages to log (see Logging) // LogSubsystems - subsystems within sqlsrv to log messages (see Logging) - PHP_FUNCTION(sqlsrv_configure); PHP_FUNCTION(sqlsrv_get_config); -//********************************************************************************************************************************* -// Errors -//********************************************************************************************************************************* - -// represents the mapping between an error_code and the corresponding error message. -struct ss_error { - - unsigned int error_code; - sqlsrv_error_const sqlsrv_error; -}; - -// List of all driver specific error codes. -enum SS_ERROR_CODES { - - SS_SQLSRV_ERROR_ALREADY_IN_TXN = SQLSRV_ERROR_DRIVER_SPECIFIC, - SS_SQLSRV_ERROR_NOT_IN_TXN, - SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, - SS_SQLSRV_ERROR_REGISTER_RESOURCE, - SS_SQLSRV_ERROR_INVALID_CONNECTION_KEY, - SS_SQLSRV_ERROR_STATEMENT_NOT_PREPARED, - SS_SQLSRV_ERROR_INVALID_FETCH_STYLE, - SS_SQLSRV_ERROR_INVALID_FETCH_TYPE, - SS_SQLSRV_WARNING_FIELD_NAME_EMPTY, - SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, - SS_SQLSRV_ERROR_ZEND_BAD_CLASS, - SS_SQLSRV_ERROR_STATEMENT_SCROLLABLE, - SS_SQLSRV_ERROR_STATEMENT_NOT_SCROLLABLE, - SS_SQLSRV_ERROR_INVALID_OPTION, - SS_SQLSRV_ERROR_PARAM_INVALID_INDEX, - SS_SQLSRV_ERROR_INVALID_PARAMETER_PRECISION, - SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, - SS_SQLSRV_ERROR_VAR_REQUIRED, - SS_SQLSRV_ERROR_CONNECT_ILLEGAL_ENCODING, - SS_SQLSRV_ERROR_CONNECT_BRACES_NOT_ESCAPED, - SS_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE, - SS_SQLSRV_ERROR_PARAM_VAR_NOT_REF, - SS_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED -}; - -extern ss_error SS_ERRORS[]; - -bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_error_code, _In_ bool warning TSRMLS_DC, _In_opt_ va_list* print_args ); - // *** extension error functions *** PHP_FUNCTION(sqlsrv_errors); -// convert from the default encoding specified by the "CharacterSet" -// connection option to UTF-16. mbcs_len and utf16_len are sizes in -// bytes. The return is the number of UTF-16 characters in the string -// returned in utf16_out_string. -unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, - _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer(mbcs_in_string) wchar_t* utf16_out_string, - _In_ unsigned int utf16_len ); -// create a wide char string from the passed in mbcs string. NULL is returned if the string -// could not be created. No error is posted by this function. utf16_len is the number of -// wchar_t characters, not the number of bytes. -SQLWCHAR* utf16_string_from_mbcs_string( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, - _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); - -// *** internal error macros and functions *** -bool handle_error( sqlsrv_context const* ctx, int log_subsystem, const char* function, - sqlsrv_error const* ssphp TSRMLS_DC, ... ); -void handle_warning( sqlsrv_context const* ctx, int log_subsystem, const char* function, - sqlsrv_error const* ssphp TSRMLS_DC, ... ); -void __cdecl sqlsrv_error_dtor( zend_resource *rsrc TSRMLS_DC ); - -// release current error lists and set to NULL -inline void reset_errors( TSRMLS_D ) -{ - if( Z_TYPE( SQLSRV_G( errors )) != IS_ARRAY && Z_TYPE( SQLSRV_G( errors )) != IS_NULL ) { - DIE( "sqlsrv_errors contains an invalid type" ); - } - if( Z_TYPE( SQLSRV_G( warnings )) != IS_ARRAY && Z_TYPE( SQLSRV_G( warnings )) != IS_NULL ) { - DIE( "sqlsrv_warnings contains an invalid type" ); - } - - if( Z_TYPE( SQLSRV_G( errors )) == IS_ARRAY ) { - zend_hash_destroy( Z_ARRVAL( SQLSRV_G( errors ))); - FREE_HASHTABLE( Z_ARRVAL( SQLSRV_G( errors ))); - } - if( Z_TYPE( SQLSRV_G( warnings )) == IS_ARRAY ) { - zend_hash_destroy( Z_ARRVAL( SQLSRV_G( warnings ))); - FREE_HASHTABLE( Z_ARRVAL( SQLSRV_G( warnings ))); - } - - ZVAL_NULL( &SQLSRV_G( errors )); - ZVAL_NULL( &SQLSRV_G( warnings )); -} - -#define THROW_SS_ERROR( ctx, error_code, ... ) \ - (void)call_error_handler( ctx, error_code TSRMLS_CC, false /*warning*/, ## __VA_ARGS__ ); \ - throw ss::SSException(); - - -class sqlsrv_context_auto_ptr : public sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr > { - -public: - - sqlsrv_context_auto_ptr( void ) : - sqlsrv_auto_ptr( NULL ) - { - } - - sqlsrv_context_auto_ptr( _Inout_opt_ const sqlsrv_context_auto_ptr& src ) : - sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr >( src ) - { - } - - // free the original pointer and assign a new pointer. Use NULL to simply free the pointer. - void reset( _In_opt_ sqlsrv_context* ptr = NULL ) - { - if( _ptr ) { - _ptr->~sqlsrv_context(); - sqlsrv_free( (void*) _ptr ); - } - _ptr = ptr; - } - - sqlsrv_context* operator=( _In_opt_ sqlsrv_context* ptr ) - { - return sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr >::operator=( ptr ); - } - - void operator=( _Inout_opt_ sqlsrv_context_auto_ptr& src ) - { - sqlsrv_context* p = src.get(); - src.transferred(); - this->_ptr = p; - } -}; - - - -//********************************************************************************************************************************* -// Logging -//********************************************************************************************************************************* -#define LOG_FUNCTION( function_name ) \ - const char* _FN_ = function_name; \ - SQLSRV_G( current_subsystem ) = current_log_subsystem; \ - LOG( SEV_NOTICE, "%1!s!: entering", _FN_ ); - -#define SET_FUNCTION_NAME( context ) \ -{ \ - (context).set_func( _FN_ ); \ -} - -// logger for ss_sqlsrv called by the core layer when it wants to log something with the LOG macro -void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); - -// subsystems that may report log messages. These may be used to filter which systems write to the log to prevent noise. -enum logging_subsystems { - LOG_INIT = 0x01, - LOG_CONN = 0x02, - LOG_STMT = 0x04, - LOG_UTIL = 0x08, - LOG_ALL = -1, -}; - -//********************************************************************************************************************************* -// Common function wrappers -// have to place this namespace before the utility functions -// otherwise can't compile in Linux because 'ss' not defined -//********************************************************************************************************************************* -namespace ss { - - // an error which occurred in our SQLSRV driver - struct SSException : public core::CoreException { - - SSException() - { - } - }; - - inline void zend_register_resource( _Inout_ zval& rsrc_result, _Inout_ void* rsrc_pointer, _In_ int rsrc_type, _In_opt_ const char* rsrc_name TSRMLS_DC) - { - int zr = (NULL != (Z_RES(rsrc_result) = ::zend_register_resource(rsrc_pointer, rsrc_type)) ? SUCCESS : FAILURE); - CHECK_CUSTOM_ERROR(( zr == FAILURE ), reinterpret_cast( rsrc_pointer ), SS_SQLSRV_ERROR_REGISTER_RESOURCE, - rsrc_name ) { - throw ss::SSException(); - } - Z_TYPE_INFO(rsrc_result) = IS_RESOURCE_EX; - } -} // namespace ss - -//********************************************************************************************************************************* -// Utility Functions -//********************************************************************************************************************************* - -// generic function used to validate parameters to a PHP function. -// Register an invalid parameter error and returns NULL when parameters don't match the spec given. -template -inline H* process_params( INTERNAL_FUNCTION_PARAMETERS, _In_ char const* param_spec, _In_ const char* calling_func, _In_ size_t param_count, ... ) -{ - SQLSRV_UNUSED( return_value ); - - zval* rsrc; - H* h; - - // reset the errors from the previous API call - reset_errors( TSRMLS_C ); - - if( ZEND_NUM_ARGS() > param_count + 1 ) { - DIE( "Param count and argument count don't match." ); - return NULL; // for static analysis tools - } - - try { - - if( param_count > 6 ) { - DIE( "Param count cannot exceed 6" ); - return NULL; // for static analysis tools - } - - void* arr[6]; - va_list vaList; - va_start(vaList, param_count); //set the pointer to first argument - - for(size_t i = 0; i < param_count; ++i) { - - arr[i] = va_arg(vaList, void*); - } - - va_end(vaList); - - int result = SUCCESS; - - // dummy context to pass to the error handler - sqlsrv_context error_ctx( 0, ss_error_handler, NULL ); - error_ctx.set_func( calling_func ); - - switch( param_count ) { - - case 0: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc ); - break; - - case 1: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0] ); - break; - - case 2: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], - arr[1] ); - break; - - case 3: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], - arr[1], arr[2] ); - break; - - case 4: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], - arr[1], arr[2], arr[3] ); - break; - - case 5: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], - arr[1], arr[2], arr[3], arr[4] ); - break; - - case 6: - result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], - arr[1], arr[2], arr[3], arr[4], arr[5] ); - break; - - default: - { - THROW_CORE_ERROR( error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ); - break; - } - } - - CHECK_CUSTOM_ERROR(( result == FAILURE ), &error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ) { - - throw ss::SSException(); - } - - // get the resource registered - h = static_cast( zend_fetch_resource(Z_RES_P(rsrc) TSRMLS_CC, H::resource_name, H::descriptor )); - - CHECK_CUSTOM_ERROR(( h == NULL ), &error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ) { - - throw ss::SSException(); - } - - h->set_func( calling_func ); - } - - catch( core::CoreException& ) { - - return NULL; - } - catch ( ... ) { - - DIE( "%1!s!: Unknown exception caught in process_params.", calling_func ); - } - - return h; -} - #endif /* PHP_SQLSRV_H */ diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h new file mode 100644 index 00000000..c294f465 --- /dev/null +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -0,0 +1,468 @@ +#ifndef PHP_SQLSRV_INT_H +#define PHP_SQLSRV_INT_H + +//--------------------------------------------------------------------------------------------------------------------------------- +// File: php_sqlsrv_int.h +// +// Contents: Internal declarations for the extension +// +// Comments: Also contains "internal" declarations shared across source files. +// +// Microsoft Drivers 5.6 for PHP for SQL Server +// Copyright(c) Microsoft Corporation +// All rights reserved. +// MIT License +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +//--------------------------------------------------------------------------------------------------------------------------------- + +#include "core_sqlsrv.h" +#include "version.h" + +//********************************************************************************************************************************* +// Global variables +//********************************************************************************************************************************* + +// INI settings and constants +// (these are defined as macros to allow concatenation as we do below) +#define INI_WARNINGS_RETURN_AS_ERRORS "WarningsReturnAsErrors" +#define INI_LOG_SEVERITY "LogSeverity" +#define INI_LOG_SUBSYSTEMS "LogSubsystems" +#define INI_BUFFERED_QUERY_LIMIT "ClientBufferMaxKBSize" +#define INI_PREFIX "sqlsrv." + +PHP_INI_BEGIN() + STD_PHP_INI_BOOLEAN( INI_PREFIX INI_WARNINGS_RETURN_AS_ERRORS , "1", PHP_INI_ALL, OnUpdateBool, warnings_return_as_errors, + zend_sqlsrv_globals, sqlsrv_globals ) + STD_PHP_INI_ENTRY( INI_PREFIX INI_LOG_SEVERITY, "0", PHP_INI_ALL, OnUpdateLong, log_severity, zend_sqlsrv_globals, + sqlsrv_globals ) + STD_PHP_INI_ENTRY( INI_PREFIX INI_LOG_SUBSYSTEMS, "0", PHP_INI_ALL, OnUpdateLong, log_subsystems, zend_sqlsrv_globals, + sqlsrv_globals ) + STD_PHP_INI_ENTRY( INI_PREFIX INI_BUFFERED_QUERY_LIMIT, INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, buffered_query_limit, + zend_sqlsrv_globals, sqlsrv_globals ) +PHP_INI_END() + + +//********************************************************************************************************************************* +// Initialization Functions +//********************************************************************************************************************************* + +// module global variables (initialized in minit and freed in mshutdown) +extern HashTable* g_ss_errors_ht; +extern HashTable* g_ss_encodings_ht; +extern HashTable* g_ss_warnings_to_ignore_ht; + +extern HMODULE g_sqlsrv_hmodule; // used for getting the version information + +// henv context for creating connections +extern sqlsrv_context* g_ss_henv_cp; +extern sqlsrv_context* g_ss_henv_ncp; + + +//********************************************************************************************************************************* +// Connection +//********************************************************************************************************************************* + +struct ss_sqlsrv_conn : sqlsrv_conn +{ + HashTable* stmts; + bool date_as_string; + bool format_decimals; // flag set to turn on formatting for values of decimal / numeric types + short decimal_places; // number of decimal digits to show in a result set unless format_numbers is false + bool in_transaction; // flag set when inside a transaction and used for checking validity of tran API calls + + // static variables used in process_params + static const char* resource_name; + static int descriptor; + + // initialize with default values + ss_sqlsrv_conn( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* drv TSRMLS_DC ) : + sqlsrv_conn( h, e, drv, SQLSRV_ENCODING_SYSTEM TSRMLS_CC ), + stmts( NULL ), + date_as_string( false ), + format_decimals( false ), + decimal_places( NO_CHANGE_DECIMAL_PLACES ), + in_transaction( false ) + { + } +}; + +// resource destructor +void __cdecl sqlsrv_conn_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ); + + +//********************************************************************************************************************************* +// Statement +//********************************************************************************************************************************* + +// holds the field names for reuse by sqlsrv_fetch_array/object as keys +struct sqlsrv_fetch_field_name { + char* name; + SQLLEN len; +}; + +struct stmt_option_ss_scrollable : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + +// This object inherits and overrides the callbacks necessary +struct ss_sqlsrv_stmt : public sqlsrv_stmt { + + ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_ void* drv TSRMLS_DC ); + + virtual ~ss_sqlsrv_stmt( void ); + + void new_result_set( TSRMLS_D ); + + // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants + sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); + + bool prepared; // whether the statement has been prepared yet (used for error messages) + zend_ulong conn_index; // index into the connection hash that contains this statement structure + zval* params_z; // hold parameters passed to sqlsrv_prepare but not used until sqlsrv_execute + sqlsrv_fetch_field_name* fetch_field_names; // field names for current results used by sqlsrv_fetch_array/object as keys + int fetch_fields_count; + + // static variables used in process_params + static const char* resource_name; + static int descriptor; + +}; + +// holds the field names for reuse by sqlsrv_fetch_array/object as keys +struct sqlsrv_fetch_field { + char* name; + unsigned int len; +}; + +// holds the stream param and the encoding that it was assigned +struct sqlsrv_stream_encoding { + zval* stream_z; + unsigned int encoding; + + sqlsrv_stream_encoding( _In_ zval* str_z, _In_ unsigned int enc ) : + stream_z( str_z ), encoding( enc ) + { + } +}; + +// resource destructor +void __cdecl sqlsrv_stmt_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ); + +// "internal" statement functions shared by functions in conn.cpp and stmt.cpp +void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ); +bool sqlsrv_stmt_common_execute( sqlsrv_stmt* s, const SQLCHAR* sql_string, int sql_len, bool direct, const char* function + TSRMLS_DC ); +void free_odbc_resources( ss_sqlsrv_stmt* stmt TSRMLS_DC ); +void free_stmt_resource( _Inout_ zval* stmt_z TSRMLS_DC ); + + +//********************************************************************************************************************************* +// Errors +//********************************************************************************************************************************* + +// represents the mapping between an error_code and the corresponding error message. +struct ss_error { + + unsigned int error_code; + sqlsrv_error_const sqlsrv_error; +}; + +// List of all driver specific error codes. +enum SS_ERROR_CODES { + + SS_SQLSRV_ERROR_ALREADY_IN_TXN = SQLSRV_ERROR_DRIVER_SPECIFIC, + SS_SQLSRV_ERROR_NOT_IN_TXN, + SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, + SS_SQLSRV_ERROR_REGISTER_RESOURCE, + SS_SQLSRV_ERROR_INVALID_CONNECTION_KEY, + SS_SQLSRV_ERROR_STATEMENT_NOT_PREPARED, + SS_SQLSRV_ERROR_INVALID_FETCH_STYLE, + SS_SQLSRV_ERROR_INVALID_FETCH_TYPE, + SS_SQLSRV_WARNING_FIELD_NAME_EMPTY, + SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, + SS_SQLSRV_ERROR_ZEND_BAD_CLASS, + SS_SQLSRV_ERROR_STATEMENT_SCROLLABLE, + SS_SQLSRV_ERROR_STATEMENT_NOT_SCROLLABLE, + SS_SQLSRV_ERROR_INVALID_OPTION, + SS_SQLSRV_ERROR_PARAM_INVALID_INDEX, + SS_SQLSRV_ERROR_INVALID_PARAMETER_PRECISION, + SS_SQLSRV_ERROR_INVALID_PARAMETER_DIRECTION, + SS_SQLSRV_ERROR_VAR_REQUIRED, + SS_SQLSRV_ERROR_CONNECT_ILLEGAL_ENCODING, + SS_SQLSRV_ERROR_CONNECT_BRACES_NOT_ESCAPED, + SS_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE, + SS_SQLSRV_ERROR_PARAM_VAR_NOT_REF, + SS_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, + SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED +}; + +extern ss_error SS_ERRORS[]; + +bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_error_code, _In_ bool warning TSRMLS_DC, _In_opt_ va_list* print_args ); + +// convert from the default encoding specified by the "CharacterSet" +// connection option to UTF-16. mbcs_len and utf16_len are sizes in +// bytes. The return is the number of UTF-16 characters in the string +// returned in utf16_out_string. +unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, + _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer(mbcs_in_string) wchar_t* utf16_out_string, + _In_ unsigned int utf16_len ); +// create a wide char string from the passed in mbcs string. NULL is returned if the string +// could not be created. No error is posted by this function. utf16_len is the number of +// wchar_t characters, not the number of bytes. +SQLWCHAR* utf16_string_from_mbcs_string( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, + _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); + +// *** internal error macros and functions *** +bool handle_error( sqlsrv_context const* ctx, int log_subsystem, const char* function, + sqlsrv_error const* ssphp TSRMLS_DC, ... ); +void handle_warning( sqlsrv_context const* ctx, int log_subsystem, const char* function, + sqlsrv_error const* ssphp TSRMLS_DC, ... ); +void __cdecl sqlsrv_error_dtor( zend_resource *rsrc TSRMLS_DC ); + +// release current error lists and set to NULL +inline void reset_errors( TSRMLS_D ) +{ + if( Z_TYPE( SQLSRV_G( errors )) != IS_ARRAY && Z_TYPE( SQLSRV_G( errors )) != IS_NULL ) { + DIE( "sqlsrv_errors contains an invalid type" ); + } + if( Z_TYPE( SQLSRV_G( warnings )) != IS_ARRAY && Z_TYPE( SQLSRV_G( warnings )) != IS_NULL ) { + DIE( "sqlsrv_warnings contains an invalid type" ); + } + + if( Z_TYPE( SQLSRV_G( errors )) == IS_ARRAY ) { + zend_hash_destroy( Z_ARRVAL( SQLSRV_G( errors ))); + FREE_HASHTABLE( Z_ARRVAL( SQLSRV_G( errors ))); + } + if( Z_TYPE( SQLSRV_G( warnings )) == IS_ARRAY ) { + zend_hash_destroy( Z_ARRVAL( SQLSRV_G( warnings ))); + FREE_HASHTABLE( Z_ARRVAL( SQLSRV_G( warnings ))); + } + + ZVAL_NULL( &SQLSRV_G( errors )); + ZVAL_NULL( &SQLSRV_G( warnings )); +} + +#define THROW_SS_ERROR( ctx, error_code, ... ) \ + (void)call_error_handler( ctx, error_code TSRMLS_CC, false /*warning*/, ## __VA_ARGS__ ); \ + throw ss::SSException(); + + +class sqlsrv_context_auto_ptr : public sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr > { + +public: + + sqlsrv_context_auto_ptr( void ) : + sqlsrv_auto_ptr( NULL ) + { + } + + sqlsrv_context_auto_ptr( _Inout_opt_ const sqlsrv_context_auto_ptr& src ) : + sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr >( src ) + { + } + + // free the original pointer and assign a new pointer. Use NULL to simply free the pointer. + void reset( _In_opt_ sqlsrv_context* ptr = NULL ) + { + if( _ptr ) { + _ptr->~sqlsrv_context(); + sqlsrv_free( (void*) _ptr ); + } + _ptr = ptr; + } + + sqlsrv_context* operator=( _In_opt_ sqlsrv_context* ptr ) + { + return sqlsrv_auto_ptr< sqlsrv_context, sqlsrv_context_auto_ptr >::operator=( ptr ); + } + + void operator=( _Inout_opt_ sqlsrv_context_auto_ptr& src ) + { + sqlsrv_context* p = src.get(); + src.transferred(); + this->_ptr = p; + } +}; + + +//********************************************************************************************************************************* +// Logging +//********************************************************************************************************************************* + +#define LOG_FUNCTION( function_name ) \ + const char* _FN_ = function_name; \ + SQLSRV_G( current_subsystem ) = current_log_subsystem; \ + LOG( SEV_NOTICE, "%1!s!: entering", _FN_ ); + +#define SET_FUNCTION_NAME( context ) \ +{ \ + (context).set_func( _FN_ ); \ +} + +// logger for ss_sqlsrv called by the core layer when it wants to log something with the LOG macro +void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); + +// subsystems that may report log messages. These may be used to filter which systems write to the log to prevent noise. +enum logging_subsystems { + LOG_INIT = 0x01, + LOG_CONN = 0x02, + LOG_STMT = 0x04, + LOG_UTIL = 0x08, + LOG_ALL = -1, +}; + + +//********************************************************************************************************************************* +// Common function wrappers +// have to place this namespace before the utility functions +// otherwise can't compile in Linux because 'ss' not defined +//********************************************************************************************************************************* + +namespace ss { + + // an error which occurred in our SQLSRV driver + struct SSException : public core::CoreException { + + SSException() + { + } + }; + + inline void zend_register_resource( _Inout_ zval& rsrc_result, _Inout_ void* rsrc_pointer, _In_ int rsrc_type, _In_opt_ const char* rsrc_name TSRMLS_DC) + { + int zr = (NULL != (Z_RES(rsrc_result) = ::zend_register_resource(rsrc_pointer, rsrc_type)) ? SUCCESS : FAILURE); + CHECK_CUSTOM_ERROR(( zr == FAILURE ), reinterpret_cast( rsrc_pointer ), SS_SQLSRV_ERROR_REGISTER_RESOURCE, + rsrc_name ) { + throw ss::SSException(); + } + Z_TYPE_INFO(rsrc_result) = IS_RESOURCE_EX; + } +} // namespace ss + + +//********************************************************************************************************************************* +// Utility Functions +//********************************************************************************************************************************* + +// generic function used to validate parameters to a PHP function. +// Register an invalid parameter error and returns NULL when parameters don't match the spec given. +template +inline H* process_params( INTERNAL_FUNCTION_PARAMETERS, _In_ char const* param_spec, _In_ const char* calling_func, _In_ size_t param_count, ... ) +{ + SQLSRV_UNUSED( return_value ); + + zval* rsrc; + H* h; + + // reset the errors from the previous API call + reset_errors( TSRMLS_C ); + + if( ZEND_NUM_ARGS() > param_count + 1 ) { + DIE( "Param count and argument count don't match." ); + return NULL; // for static analysis tools + } + + try { + + if( param_count > 6 ) { + DIE( "Param count cannot exceed 6" ); + return NULL; // for static analysis tools + } + + void* arr[6]; + va_list vaList; + va_start(vaList, param_count); //set the pointer to first argument + + for(size_t i = 0; i < param_count; ++i) { + + arr[i] = va_arg(vaList, void*); + } + + va_end(vaList); + + int result = SUCCESS; + + // dummy context to pass to the error handler + sqlsrv_context error_ctx( 0, ss_error_handler, NULL ); + error_ctx.set_func( calling_func ); + + switch( param_count ) { + + case 0: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc ); + break; + + case 1: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0] ); + break; + + case 2: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], + arr[1] ); + break; + + case 3: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], + arr[1], arr[2] ); + break; + + case 4: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], + arr[1], arr[2], arr[3] ); + break; + + case 5: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], + arr[1], arr[2], arr[3], arr[4] ); + break; + + case 6: + result = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, const_cast( param_spec ), &rsrc, arr[0], + arr[1], arr[2], arr[3], arr[4], arr[5] ); + break; + + default: + { + THROW_CORE_ERROR( error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ); + break; + } + } + + CHECK_CUSTOM_ERROR(( result == FAILURE ), &error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ) { + + throw ss::SSException(); + } + + // get the resource registered + h = static_cast( zend_fetch_resource(Z_RES_P(rsrc) TSRMLS_CC, H::resource_name, H::descriptor )); + + CHECK_CUSTOM_ERROR(( h == NULL ), &error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, calling_func ) { + + throw ss::SSException(); + } + + h->set_func( calling_func ); + } + + catch( core::CoreException& ) { + + return NULL; + } + catch ( ... ) { + + DIE( "%1!s!: Unknown exception caught in process_params.", calling_func ); + } + + return h; +} + +#endif /* PHP_SQLSRV_INT_H */ diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 1f0357ac..c366e030 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use statement handles // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -18,7 +18,12 @@ //--------------------------------------------------------------------------------------------------------------------------------- // *** header files *** -#include "php_sqlsrv.h" +extern "C" { + #include "php_sqlsrv.h" +} + +#include "php_sqlsrv_int.h" + #ifdef _WIN32 #include #endif // _WIN32 diff --git a/source/sqlsrv/template.rc b/source/sqlsrv/template.rc index 88eefb55..bbe47132 100644 --- a/source/sqlsrv/template.rc +++ b/source/sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index d4be03f5..cec66c3f 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.5 for PHP for SQL Server +// Microsoft Drivers 5.6 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -19,7 +19,11 @@ // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- -#include "php_sqlsrv.h" +extern "C" { + #include "php_sqlsrv.h" +} + +#include "php_sqlsrv_int.h" namespace { @@ -368,7 +372,7 @@ ss_error SS_ERRORS[] = { }, { SS_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, - { IMSSP, (SQLCHAR*)"Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported.", -62, false } + { IMSSP, (SQLCHAR*)"Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported.", -62, false } }, { SS_SQLSRV_ERROR_AE_QUERY_SQLTYPE_REQUIRED, @@ -432,6 +436,10 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_INVALID_DECIMAL_PLACES, { IMSSP, (SQLCHAR*) "Expected an integer to specify number of decimals to format the output values of decimal data types.", -117, false} }, + { + SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, + { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -118, false} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index cf2ff605..d327d859 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -624,6 +624,33 @@ function IsDaasMode() return ($daasMode ? true : false); } +function isAzureDW() +{ + // Check if running Azure Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + try { + $conn = connect(); + + // Check if running Azure Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + $tsql = "SELECT SERVERPROPERTY ('edition'), SERVERPROPERTY ('EngineEdition')"; + $stmt = $conn->query($tsql); + + $result = $stmt->fetch(PDO::FETCH_NUM); + $edition = $result[0]; + $engEd = intval($result[1]); + + if ($edition == "SQL Azure" && $engEd == 6) { + return true; + } else { + return false; + } + } catch (Exception $e) { + echo $e->getMessage(); + die("skip Failed to connect or could not fetch edition info."); + } +} + function FatalError($errorMsg) { if (!IsPdoMode()) { diff --git a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt index 9fc7cd95..5513290b 100644 --- a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt +++ b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt @@ -6,7 +6,7 @@ emalloc (which only allocate memory in the memory space allocated for the PHP pr --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- + --FILE-- --FILE-- @@ -21,28 +23,38 @@ try { insertRow($conn, $tableName2, array("c1_int" => 990021574, "c2_varchar" => ">vh~Ö.bÐ*äß/ÄAabýZâOüzr£ðAß+|~|OU¢a|U<ßrv.uCB.ÐÜh_î+ãå@üðöã,U+ßvuU:/ý_Öãî/ð|bB|_Zbua©r++BA¢z£.üî¢öåäözÜ¢ßb:aöCrÄ~ýZ¢uªÐö.hhßð*zÜÜß*ãüåýãÄ+åýüüaߢÃÐBî@~AZöÃOßC@äoÃuCÜ,ÐÄa:îäÄÖý:h*ouªuåvUz_ArßAªãaãvÐåAUüAB:¢Äz|öub<üZvößüå:ãÄ@r/ZAÄðÄÄvzîv~C/£|ýýbüÖ~£|Öå<Üa~/v@åAz©¢£U_ßhbaÃß,zz<ã¢|<ä©>öAuövÖ>abu,zå,+ß/ü/ª_bbB:ÃC~£ü/O©O©ªAª_,|a¢~ýý/b>ßC@/böîöh>~£ð+Bßr©ÄÐÖßã:bA@:>B:UAbããîÜ~uÜ£îCöÖ£©_ÜßzÐ+ÖýZb,A:<z.ãîÄzC@©*ä|ã._ßZOäb¢Cßovå+uv.£B~~b£ª|ÖÄîßö>©Ãbb|©©ðA£åO~âãüîuvÄÜýUzîOÖ/oOßO*>ªßzêÖÐböÄåbîðîÐa~©ßîÄßУ<î>åBã_ý*ah¢rOĪ,ßo¢¢a|BÖäzU£.B£@Ü,ßAÃ>,ðßß+ßÜ©|Ðr©bCðТüãz>AßðåÃ>bÄåÄ|Z~äÃ/Cb*£bð_/Ða@~AÜãO+ý*CîîÃzÄöÃa©+@vuz>î>©.Cv>hÃý>©Bä,ö~@~@r,AðCu@Ü,@U*ÐvöÃêuã.Öa*uZªoZ/ðÖ©ßv_<ÖvåÜÐÜOÐoðßðÃUýZÐB:+ÄÃã£")); $size = 2; - $stmt = $conn->prepare("SELECT * FROM $tableName1", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE=> $size)); - $attr = $stmt->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + $stmt = $conn->prepare("SELECT * FROM $tableName1", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE=> $size)); + $attr = $stmt->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); echo("Client Buffer Size in KB: $attr\n"); - $stmt->execute(); + $stmt->execute(); $numRows = 0; while ($result = $stmt->fetch()) { $numRows++; } echo ("Number of rows: $numRows\n"); - $size = 3; - $stmt = $conn->prepare("SELECT * FROM $tableName2", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE=> $size)); - $attr = $stmt->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + $size = 3; + $stmt = $conn->prepare("SELECT * FROM $tableName2", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE=> $size)); + $attr = $stmt->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); echo("Client Buffer Size in KB: $attr\n"); - $stmt->execute(); + $stmt->execute(); $numRows = 0; while ($result = $stmt->fetch()) { $numRows++; } - echo ("Number of rows: $numRows\n"); + $size = 1; + $stmt = $conn->prepare("SELECT * FROM $tableName2", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED,PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE=> $size)); + $attr = $stmt->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + echo("Client Buffer Size in KB: $attr\n"); + try { + $stmt->execute(); + echo "Expect this to fail!!\n"; + } catch (PDOException $e) { + var_dump($e->getMessage()); + } + dropTable($conn, $tableName1); dropTable($conn, $tableName2); unset($stmt); @@ -58,4 +70,6 @@ Client Buffer Size in KB: 2 Number of rows: 1 Client Buffer Size in KB: 3 Number of rows: 1 +Client Buffer Size in KB: 1 +string(65) "SQLSTATE[IMSSP]: Memory limit of 1 KB exceeded for buffered query" Done diff --git a/test/functional/pdo_sqlsrv/pdo_228_setConnAttribute_clientbuffermaxkbsize.phpt b/test/functional/pdo_sqlsrv/pdo_228_setConnAttribute_clientbuffermaxkbsize.phpt new file mode 100644 index 00000000..b501bd7b --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_228_setConnAttribute_clientbuffermaxkbsize.phpt @@ -0,0 +1,94 @@ +--TEST-- +GitHub issue #228 - how max client buffer size affects the fetching of data +--DESCRIPTION-- +A pdo_sqlsrv variation of the example in GitHub issue 228, using PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE the connection attribute. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, $size); + } catch (PDOException $e) { + if (strpos($e->getMessage(), $error) === false) { + echo $e->getMessage() . "\n"; + } + } +} + +try { + // Connect + $conn = connect(); + + $error = 'The PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE attribute is not a number or the number is not positive. Only positive numbers are valid for this attribute.'; + testErrors($conn, 0, $error); + testErrors($conn, 2.99, $error); + + // Create 2 tables + $tableName1 = 'pdo_228_1'; + $tableName2 = 'pdo_228_2'; + + createTable($conn,$tableName1, array("c1_int" => "int", "c2_varchar" => "varchar(1000)")); + insertRow($conn, $tableName1, array("c1_int" => 990021574, "c2_varchar" => ">vh~Ö.bÐ*äß/ÄAabýZâOüzr£ðAß+|~|OU¢a|U<ßrv.uCB.ÐÜh_î+ãå@üðöã,U+ßvuU:/ý_Öãî/ð|bB|_Zbua©r++BA¢z£.üî¢öåäözÜ¢ßb:aöCrÄ~ýZ¢uªÐö.hhßð*zÜÜß*ãüåýãÄ+åýüüaߢÃÐBî@~AZöÃOßC@äoÃuCÜ,ÐÄa:îäÄÖý:h*ouªuåvUz_ArßAªãaãvÐåAUüAB:¢Äz|öub<üZvößüå:ãÄ@r/ZAÄðÄÄvzîv~C/£|ýýbüÖ~£|Öå<Üa~/v@åAz©¢£U_ßhbaÃß,zz<ã¢|<ä©>öAuövÖ>abu,zå,+ß/ü/ª_bbB:ÃC~£ü/O©O©ªAª_,|a¢~ýý/b>ßC@/böîöh>~£ð+Bßr©ÄÐÖßã:bA@:>B:UAbããîÜ~uÜ£îCöÖ£©_ÜßzÐ+ÖýZb,A:<z.ãîÄzC@©*ä|ã._ßZOäb¢Cßovå+uv.£B~~b£ª|ÖÄîßö>©Ãbb|©©ðA£åO~âãüîuvÄÜýUzîOÖ/oOßO*>ªßzêÖÐböÄåbîðîÐa~©ßîÄßУ<î>åBã_ý*ah¢rOĪ,ßo¢¢a|BÖäzU£.B£@Ü,ßAÃ>,ðßß+ßÜ©|Ðr©bCðТüãz>AßðåÃ>bÄåÄ|Z~äÃ/Cb*£bð_/Ða@~AÜãO+ý*CîîÃzÄöÃa©+@vuz>î>©.Cv>hÃý>©Bä,ö~@~@r,AðCu@Ü,@U*ÐvöÃêuã.Öa*uZªoZ/ðÖ©ßv_<ÖvåÜÐÜOÐoðßðÃUýZÐB:+ÄÃã£")); + + createTable($conn,$tableName2, array("c1_int" => "int", "c2_varchar" => "varchar(max)")); + insertRow($conn, $tableName2, array("c1_int" => 990021574, "c2_varchar" => ">vh~Ö.bÐ*äß/ÄAabýZâOüzr£ðAß+|~|OU¢a|U<ßrv.uCB.ÐÜh_î+ãå@üðöã,U+ßvuU:/ý_Öãî/ð|bB|_Zbua©r++BA¢z£.üî¢öåäözÜ¢ßb:aöCrÄ~ýZ¢uªÐö.hhßð*zÜÜß*ãüåýãÄ+åýüüaߢÃÐBî@~AZöÃOßC@äoÃuCÜ,ÐÄa:îäÄÖý:h*ouªuåvUz_ArßAªãaãvÐåAUüAB:¢Äz|öub<üZvößüå:ãÄ@r/ZAÄðÄÄvzîv~C/£|ýýbüÖ~£|Öå<Üa~/v@åAz©¢£U_ßhbaÃß,zz<ã¢|<ä©>öAuövÖ>abu,zå,+ß/ü/ª_bbB:ÃC~£ü/O©O©ªAª_,|a¢~ýý/b>ßC@/böîöh>~£ð+Bßr©ÄÐÖßã:bA@:>B:UAbããîÜ~uÜ£îCöÖ£©_ÜßzÐ+ÖýZb,A:<z.ãîÄzC@©*ä|ã._ßZOäb¢Cßovå+uv.£B~~b£ª|ÖÄîßö>©Ãbb|©©ðA£åO~âãüîuvÄÜýUzîOÖ/oOßO*>ªßzêÖÐböÄåbîðîÐa~©ßîÄßУ<î>åBã_ý*ah¢rOĪ,ßo¢¢a|BÖäzU£.B£@Ü,ßAÃ>,ðßß+ßÜ©|Ðr©bCðТüãz>AßðåÃ>bÄåÄ|Z~äÃ/Cb*£bð_/Ða@~AÜãO+ý*CîîÃzÄöÃa©+@vuz>î>©.Cv>hÃý>©Bä,ö~@~@r,AðCu@Ü,@U*ÐvöÃêuã.Öa*uZªoZ/ðÖ©ßv_<ÖvåÜÐÜOÐoðßðÃUýZÐB:+ÄÃã£")); + + $size = 2; + $conn->setAttribute(PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, $size); + $stmt = $conn->prepare("SELECT * FROM $tableName1", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $attr = $conn->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + echo("Client Buffer Size in KB: $attr\n"); + $stmt->execute(); + $numRows = 0; + while ($result = $stmt->fetch()) { + $numRows++; + } + echo ("Number of rows: $numRows\n"); + + $size = 3; + $conn->setAttribute(PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, $size); + $stmt = $conn->prepare("SELECT * FROM $tableName2", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $attr = $conn->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + echo("Client Buffer Size in KB: $attr\n"); + $stmt->execute(); + $numRows = 0; + while ($result = $stmt->fetch()) { + $numRows++; + } + + echo ("Number of rows: $numRows\n"); + + $size = 1; + $conn->setAttribute(PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, $size); + $stmt = $conn->prepare("SELECT * FROM $tableName2", array(constant('PDO::ATTR_CURSOR') => PDO::CURSOR_SCROLL,PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $attr = $conn->getAttribute(constant('PDO::SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE')); + echo("Client Buffer Size in KB: $attr\n"); + try { + $stmt->execute(); + echo "Expect this to fail!!\n"; + } catch (PDOException $e) { + var_dump($e->getMessage()); + } + + dropTable($conn, $tableName1); + dropTable($conn, $tableName2); + unset($stmt); + unset($conn); + print "Done"; +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> + +--EXPECT-- +Client Buffer Size in KB: 2 +Number of rows: 1 +Client Buffer Size in KB: 3 +Number of rows: 1 +Client Buffer Size in KB: 1 +string(65) "SQLSTATE[IMSSP]: Memory limit of 1 KB exceeded for buffered query" +Done diff --git a/test/functional/pdo_sqlsrv/pdo_574_next_rowset.phpt b/test/functional/pdo_sqlsrv/pdo_574_next_rowset.phpt index d4c0e811..5ae6c67b 100644 --- a/test/functional/pdo_sqlsrv/pdo_574_next_rowset.phpt +++ b/test/functional/pdo_sqlsrv/pdo_574_next_rowset.phpt @@ -6,6 +6,7 @@ Verifies the functionality of PDOStatement nextRowset PHPT_EXEC=true --SKIPIF-- + --FILE-- +--FILE-- +prepare("$storedProcName @OUTPUT = :output"); + if ($inout) { + $paramType = PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT; + } else { + $paramType = PDO::PARAM_STR; + } + $stmt->bindParam('output', $output, $paramType, $size); + + $stmt->execute(); + + // The output param should be doubled in size for wide characters. + // However, it should not contain any data so after trimming it + // should be merely an empty string because it was originally set to null + $len = strlen($output); + $result = trim($output); + + if ($len != ($size * 2) || $result !== "" ) { + echo "Unexpected output param for $dataType: "; + var_dump($output); + } + + $stmt->closeCursor(); + if (!is_null($output)) { + echo "Output param should be null when finalized!"; + } + unset($stmt); + } catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; + } +} + +try { + // This helper method sets PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION + // $conn = connect(); + $conn = new PDO( "sqlsrv:server=$server; Database = $databaseName", $uid, $pwd); + $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + + $dataTypes = array("VARCHAR(256)", "VARCHAR(512)", "VARCHAR(max)", "NVARCHAR(256)", "NVARCHAR(512)", "NVARCHAR(max)"); + for ($i = 0, $p = 3; $i < count($dataTypes); $i++, $p++) { + // Create the stored procedure first + $storedProcName = "spNullOutputParam" . $i; + $procArgs = "@OUTPUT $dataTypes[$i] OUTPUT"; + $procCode = "SELECT 1, 2, 3"; + + createProc($conn, $storedProcName, $procArgs, $procCode); + getOutputParam($conn, $storedProcName, $dataTypes[$i], false); + getOutputParam($conn, $storedProcName, $dataTypes[$i], true); + + // Drop the stored procedure + dropProc($conn, $storedProcName); + } + + echo "Done\n"; + + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt index 0aea766f..105f6395 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt @@ -12,30 +12,27 @@ require_once("MsSetup.inc"); // $connectionInfo = "Database = $databaseName; Authentication = SqlPassword; TrustServerCertificate = true;"; -try -{ - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); +try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); echo "Connected successfully with Authentication=SqlPassword.\n"; -} -catch( PDOException $e ) -{ +} catch (PDOException $e) { echo "Could not connect with Authentication=SqlPassword.\n"; - print_r( $e->getMessage() ); + print_r($e->getMessage()); echo "\n"; } -$stmt = $conn->query( "SELECT count(*) FROM cd_info" ); -if ( $stmt === false ) -{ +// For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql +$conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); +$stmt = $conn->query("SELECT SERVERPROPERTY('EngineEdition')"); +if ($stmt === false) { echo "Query failed.\n"; -} -else -{ - $result = $stmt->fetch(); - var_dump( $result ); +} else { + $result = $stmt->fetch(PDO::FETCH_NUM); + $edition = $result[0]; + var_dump($edition); } -$conn = null; +unset($conn); /////////////////////////////////////////////////////////////////////////////////////////// // Test Azure AD with integrated authentication. This should fail because @@ -43,16 +40,13 @@ $conn = null; // $connectionInfo = "Authentication = ActiveDirectoryIntegrated; TrustServerCertificate = true;"; -try -{ - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo" ); +try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo"); echo "Connected successfully with Authentication=ActiveDirectoryIntegrated.\n"; - $conn = null; -} -catch( PDOException $e ) -{ + unset($conn); +} catch (PDOException $e) { echo "Could not connect with Authentication=ActiveDirectoryIntegrated.\n"; - print_r( $e->getMessage() ); + print_r($e->getMessage()); echo "\n"; } @@ -65,35 +59,24 @@ $azureDatabase = $adDatabase; $azureUsername = $adUser; $azurePassword = $adPassword; -if ($azureServer != 'TARGET_AD_SERVER') -{ +if ($azureServer != 'TARGET_AD_SERVER') { $connectionInfo = "Authentication = ActiveDirectoryPassword; TrustServerCertificate = false"; - try - { - $conn = new PDO( "sqlsrv:server = $azureServer ; $connectionInfo", $azureUsername, $azurePassword ); + try { + $conn = new PDO("sqlsrv:server = $azureServer ; $connectionInfo", $azureUsername, $azurePassword); echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; - } - catch( PDOException $e ) - { + } catch (PDOException $e) { echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r( $e->getMessage() ); + print_r($e->getMessage()); echo "\n"; } -} -else -{ +} else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } ?> --EXPECTF-- Connected successfully with Authentication=SqlPassword. -array(2) { - [""]=> - string(1) "7" - [0]=> - string(1) "7" -} +string(1) "%d" Could not connect with Authentication=ActiveDirectoryIntegrated. -SQLSTATE[IMSSP]: Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. +SQLSTATE[IMSSP]: Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. %s with Authentication=ActiveDirectoryPassword. diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt new file mode 100644 index 00000000..00a0c950 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_managed_identity.phpt @@ -0,0 +1,114 @@ +--TEST-- +Test some error conditions of Azure AD Managed Identity support +--DESCRIPTION-- +This test expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- +getMessage(), $expectedError) === false) { + echo "AzureAD Managed Identity test: expected to fail with $msg\n"; + + print_r($exception->getMessage()); + echo "\n"; + } +} + +function connectWithInvalidOptions() +{ + global $server; + + $message = 'AzureAD Managed Identity test: expected to fail with '; + $expectedError = 'When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted'; + + $uid = ''; + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $testCase = 'empty UID provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", $uid); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $pwd = ''; + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $testCase = 'empty PWD provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", null, $pwd); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $pwd = 'dummy'; + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $testCase = 'PWD provided'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo", null, $pwd); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); + + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + $connectionInfo = "Authentication = ActiveDirectoryMsi; AccessToken = '123';"; + $testCase = 'AccessToken option'; + try { + $conn = new PDO("sqlsrv:server = $server; $connectionInfo"); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + verifyErrorMessage($e, $expectedError, $testCase); + } + unset($connectionInfo); +} + +function connectInvalidServer() +{ + global $server, $driver, $uid, $pwd; + + try { + $conn = new PDO("sqlsrv:server = $server; driver=$driver;", $uid, $pwd); + + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; + $version = explode(".", $msodbcsqlVer); + + if ($version[0] < 17 || $version[1] < 3) { + //skip the rest of this test, which requires ODBC driver 17.3 or above + return; + } + unset($conn); + + // Try connecting to an invalid server, should get an exception from ODBC + $connectionInfo = "Authentication = ActiveDirectoryMsi;"; + $testCase = 'invalidServer'; + try { + $conn = new PDO("sqlsrv:server = invalidServer; $connectionInfo", null, null); + echo $message . $testCase . PHP_EOL; + } catch(PDOException $e) { + // TODO: check the exception message here + } + } catch(PDOException $e) { + print_r($e->getMessage()); + } +} + +require_once('MsSetup.inc'); + +// Test some error conditions +connectWithInvalidOptions(); + +// Make a connection to an invalid server +connectInvalidServer(); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_passwords.phpt b/test/functional/pdo_sqlsrv/pdo_passwords.phpt deleted file mode 100644 index c672fc95..00000000 --- a/test/functional/pdo_sqlsrv/pdo_passwords.phpt +++ /dev/null @@ -1,57 +0,0 @@ ---TEST-- -Test password with non alphanumeric characters ---DESCRIPTION-- -The first three cases should have no problem connecting. Only the last case fails because the -right curly brace should be escaped with another right brace. -In Azure for this test to pass do not specify any particular database when connecting ---SKIPIF-- - ---FILE-- -getMessage() . "\n"); -} -try { - // Test 2 - $conn = new PDO($dsn, "test_password2", "!}} ;4triou"); - if (!$conn) { - echo "Test 2: Should have connected."; - } - unset($conn); -} catch (PDOException $e) { - print_r($e->getMessage() . "\n"); -} -try { - // Test 3 - $conn = new PDO($dsn, "test_password3", "! ;4triou}}"); - if (!$conn) { - echo "Test 3: Should have connected."; - } - unset($conn); -} catch (PDOException $e) { - print_r($e->getMessage() . "\n"); -} -// Test invalid password. -try { - // Test 4 - $conn = new PDO($dsn, "test_password3", "! ;4triou}"); -} catch (PDOException $e) { - print_r($e->getMessage()); - exit; -} - -?> - ---EXPECTREGEX-- -SQLSTATE\[IMSSP\]: An unescaped right brace \(}\) was found in either the user name or password\. All right braces must be escaped with another right brace \(}}\)\. diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_attribute.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_attribute.phpt index d0da03e1..01774f08 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_attribute.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_attribute.phpt @@ -4,6 +4,7 @@ Test PDO::prepare() with PDO::ATTR_EMULATE_PREPARES. PHPT_EXEC=true --SKIPIF-- + --FILE-- \ No newline at end of file diff --git a/test/functional/setup/168256.sql b/test/functional/setup/168256.sql index 82dbe487..f4484c34 100644 --- a/test/functional/setup/168256.sql +++ b/test/functional/setup/168256.sql @@ -1,6 +1,3 @@ -USE $(dbname) -GO - IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[168256]') AND type in (N'U')) diff --git a/test/functional/setup/cd_info.sql b/test/functional/setup/cd_info.sql index 40d3bb04..96240acf 100644 --- a/test/functional/setup/cd_info.sql +++ b/test/functional/setup/cd_info.sql @@ -1,11 +1,8 @@ -USE $(dbname) -GO - IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[tracks]') AND type in (N'U')) BEGIN -ALTER TABLE $(dbname)..[tracks] DROP CONSTRAINT [FK__tracks__asin__7F60ED59] +ALTER TABLE [tracks] DROP CONSTRAINT [FK__tracks__asin__7F60ED59] END GO diff --git a/test/functional/setup/cleanup_dbs.py b/test/functional/setup/cleanup_dbs.py index 86406303..db78ea03 100644 --- a/test/functional/setup/cleanup_dbs.py +++ b/test/functional/setup/cleanup_dbs.py @@ -3,10 +3,8 @@ import os import sys -import subprocess import platform import argparse -from subprocess import Popen, PIPE from exec_sql_scripts import * if __name__ == '__main__': @@ -25,8 +23,9 @@ if __name__ == '__main__': sys.exit(1) conn_options = ' -S ' + server + ' -U ' + uid + ' -P ' + pwd + ' ' - - executeSQLscript( os.path.join( os.path.dirname(os.path.realpath(__file__)), 'drop_db.sql'), conn_options, args.DBNAME) + + sql_script = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'drop_db.sql'); + manageTestDB(sql_script, conn_options, args.DBNAME) # if Windows, remove self signed certificate using ps command if platform.system() == 'Windows': diff --git a/test/functional/setup/create_db.sql b/test/functional/setup/create_db.sql index d8dee7ac..2c13bc32 100644 --- a/test/functional/setup/create_db.sql +++ b/test/functional/setup/create_db.sql @@ -1,13 +1,5 @@ -USE [master] -GO - -IF EXISTS (SELECT name FROM sys.databases WHERE name = '$(dbname)' ) - -BEGIN -DROP DATABASE $(dbname) -END - -CREATE DATABASE $(dbname) - -GO - +IF EXISTS (SELECT name FROM sys.databases WHERE name = 'TEST_DB' ) DROP DATABASE TEST_DB + +CREATE DATABASE TEST_DB +GO + diff --git a/test/functional/setup/create_logins_azure.sql b/test/functional/setup/create_logins_azure.sql deleted file mode 100644 index ecc5530a..00000000 --- a/test/functional/setup/create_logins_azure.sql +++ /dev/null @@ -1,18 +0,0 @@ ---for this script to work in Azure, use sqlcmd to connect to master database -IF NOT EXISTS (SELECT name FROM sys.sql_logins WHERE name = 'test_password') -BEGIN - CREATE LOGIN test_password WITH PASSWORD='! ;4triou'; -END -GO - -IF NOT EXISTS (SELECT name FROM sys.sql_logins WHERE name = 'test_password2') -BEGIN - CREATE LOGIN test_password2 WITH PASSWORD='!} ;4triou'; -END -GO - -IF NOT EXISTS (SELECT name FROM sys.sql_logins WHERE name = 'test_password3') -BEGIN - CREATE LOGIN test_password3 WITH PASSWORD='! ;4triou}'; -END -GO diff --git a/test/functional/setup/create_users_azure.sql b/test/functional/setup/create_users_azure.sql deleted file mode 100644 index af577063..00000000 --- a/test/functional/setup/create_users_azure.sql +++ /dev/null @@ -1,21 +0,0 @@ ---for this script to work in Azure, create_logins_azure.sql must have been invoked beforehand ---assuming these logins exist, use sqlcmd to connect to a test database ---these users will be granted access to that database -IF NOT EXISTS (SELECT name FROM sysusers WHERE name = 'test_password') -BEGIN - CREATE USER test_password FROM LOGIN test_password; -END -GO - -IF NOT EXISTS (SELECT name FROM sysusers WHERE name = 'test_password2') -BEGIN - CREATE USER test_password2 FROM LOGIN test_password2; -END -GO - -IF NOT EXISTS (SELECT name FROM sysusers WHERE name = 'test_password3') -BEGIN - CREATE USER test_password3 FROM LOGIN test_password3; -END -GO - diff --git a/test/functional/setup/drop_db.sql b/test/functional/setup/drop_db.sql index d45743f5..b31a055f 100644 --- a/test/functional/setup/drop_db.sql +++ b/test/functional/setup/drop_db.sql @@ -1,8 +1 @@ -USE [master] -GO - -IF EXISTS (SELECT name FROM sys.databases WHERE name = '$(dbname)' ) - -BEGIN -DROP DATABASE $(dbname) -END +IF EXISTS (SELECT name FROM sys.databases WHERE name = 'TEST_DB' ) DROP DATABASE TEST_DB diff --git a/test/functional/setup/exec_sql_scripts.py b/test/functional/setup/exec_sql_scripts.py index 80895588..0ab26434 100644 --- a/test/functional/setup/exec_sql_scripts.py +++ b/test/functional/setup/exec_sql_scripts.py @@ -1,10 +1,7 @@ #!/usr/bin/env python3 -# contains helper methods +# contains helper methods import os -import sys import subprocess -import platform -import argparse from subprocess import Popen, PIPE def executeCommmand(inst_command): @@ -15,29 +12,17 @@ def executeCommmand(inst_command): print (oo) def executeSQLscript(sqlfile, conn_options, dbname): - if platform.system() == 'Windows': - executeSQLscriptWindows(sqlfile, conn_options, dbname) - elif platform.system() == 'Linux' or platform.system() == 'Darwin': - executeSQLscriptUnix(sqlfile, conn_options, dbname) - -def executeSQLscriptWindows(sqlfile, conn_options, dbname): - inst_command = 'sqlcmd ' + conn_options + ' -i ' + sqlfile + ' -v dbname =' + dbname + inst_command = 'sqlcmd -I ' + conn_options + ' -i ' + sqlfile + ' -d ' + dbname executeCommmand(inst_command) -def executeSQLscriptUnix(sqlfile, conn_options, dbname): - # This is a workaround because sqlcmd in Unix does not support -v option for variables. - # It inserts setvar dbname into the beginning of a temp .sql file - tmpFileName = sqlfile[0:-4] + '_tmp.sql' - redirect_string = '(echo :setvar dbname {0}) > {2}; cat {1} >> {2}; ' - sqlcmd = 'sqlcmd ' + conn_options + ' -i ' + tmpFileName +def manageTestDB(sqlfile, conn_options, dbname): + tmp_sql_file = 'test_db_tmp.sql' + if os.path.exists(tmp_sql_file): + os.remove(tmp_sql_file) + with open(sqlfile, 'r') as infile: + script = infile.read().replace('TEST_DB', dbname) + with open(tmp_sql_file, 'w') as outfile: + outfile.write(script) - # Execute a simple query via sqlcmd: without this step, the next step fails in travis CI - simple_cmd = 'sqlcmd ' + conn_options + ' -Q \"select @@Version\" ' - executeCommmand(simple_cmd) - - # inst_command = redirect_string.format(dbname, sqlfile, tmpFileName) + sqlcmd - inst_command = redirect_string.format(dbname, sqlfile, tmpFileName) - executeCommmand(inst_command) - executeCommmand(sqlcmd) - - os.remove(tmpFileName) + executeSQLscript(tmp_sql_file, conn_options, 'master') + os.remove(tmp_sql_file) diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index 32b821ef..58900526 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -1,34 +1,15 @@ #!/usr/bin/env python3 # py setup_dbs.py -dbname -azure -# OR -# py setup_dbs.py -dbname +# OR +# py setup_dbs.py -dbname import os import sys -import subprocess import platform import argparse -from subprocess import Popen, PIPE from exec_sql_scripts import * -def createLoginUsers(conn_options, dbname, azure): - if (azure.lower() == 'yes'): - # can only create logins in the master database - createLoginUsersAzure('create_logins_azure.sql', conn_options, 'master') - # create users to use those logins to access the test database (dbname) - createLoginUsersAzure('create_users_azure.sql', conn_options, dbname) - else: - executeSQLscript('test_password.sql', conn_options, dbname) - -def createLoginUsersAzure(sqlfile, conn_options, dbname): - inst_command = 'sqlcmd ' + conn_options + ' -i ' + sqlfile + ' -d ' + dbname - executeCommmand(inst_command) - def setupTestDatabase(conn_options, dbname, azure): sqlFiles = ['test_types.sql', '168256.sql', 'cd_info.sql', 'tracks.sql'] - - # for Azure, must specify the database for the sql scripts to work - if (azure.lower() == 'yes'): - conn_options += ' -d ' + dbname for sqlFile in sqlFiles: executeSQLscript(sqlFile, conn_options, dbname) @@ -41,29 +22,29 @@ def populateTables(conn_options, dbname): executeBulkCopy(conn_options, dbname, '168256', '168256') def executeBulkCopy(conn_options, dbname, tblname, datafile): - redirect_string = 'bcp {0}..[{1}] in {2}.dat -f {2}.fmt ' - inst_command = redirect_string.format(dbname, tblname, datafile) + conn_options + redirect_string = 'bcp {0}..{1} in {2}.dat -f {2}.fmt -q' + inst_command = redirect_string.format(dbname, tblname, datafile) + conn_options executeCommmand(inst_command) - + def setupAE(conn_options, dbname): if (platform.system() == 'Windows'): # import self signed certificate inst_command = "certutil -user -p '' -importPFX My PHPcert.pfx NoRoot" executeCommmand(inst_command) # create Column Master Key and Column Encryption Key - script_command = 'sqlcmd ' + conn_options + ' -i ae_keys.sql -d ' + dbname + script_command = 'sqlcmd -I ' + conn_options + ' -i ae_keys.sql -d ' + dbname executeCommmand(script_command) - + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('-dbname', '--DBNAME', required=True) parser.add_argument('-azure', '--AZURE', required=False, default='no') args = parser.parse_args() - - try: - server = os.environ['TEST_PHP_SQL_SERVER'] - uid = os.environ['TEST_PHP_SQL_UID'] - pwd = os.environ['TEST_PHP_SQL_PWD'] + + try: + server = os.environ['TEST_PHP_SQL_SERVER'] + uid = os.environ['TEST_PHP_SQL_UID'] + pwd = os.environ['TEST_PHP_SQL_PWD'] except : print("TEST_PHP_SQL_SERVER environment variable must be set to the name of the server to use") print("TEST_PHP_SQL_UID environment variable must be set to the name of the user to authenticate with") @@ -72,20 +53,17 @@ if __name__ == '__main__': current_working_dir=os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) - conn_options = ' -S ' + server + ' -U ' + uid + ' -P ' + pwd + ' ' - + conn_options = ' -S ' + server + ' -U ' + uid + ' -P ' + pwd + ' ' + # In Azure, assume an empty test database has been created using Azure portal if (args.AZURE.lower() == 'no'): - executeSQLscript('create_db.sql', conn_options, args.DBNAME) + manageTestDB('create_db.sql', conn_options, args.DBNAME) - # create login users - createLoginUsers(conn_options, args.DBNAME, args.AZURE) # create tables in the new database - setupTestDatabase(conn_options, args.DBNAME, args.AZURE) + setupTestDatabase(conn_options, args.DBNAME, args.AZURE) # populate these tables populateTables(conn_options, args.DBNAME) # setup AE (certificate, column master key and column encryption key) setupAE(conn_options, args.DBNAME) - - os.chdir(current_working_dir) - + + os.chdir(current_working_dir) \ No newline at end of file diff --git a/test/functional/setup/test_password.sql b/test/functional/setup/test_password.sql deleted file mode 100644 index 683556c8..00000000 --- a/test/functional/setup/test_password.sql +++ /dev/null @@ -1,31 +0,0 @@ ---first, create new logins (user id / password pair) if not yet created -USE master; -GO - -IF NOT EXISTS (SELECT name FROM master..syslogins WHERE name = 'test_password') -BEGIN - CREATE LOGIN test_password WITH PASSWORD='! ;4triou'; -END -GO - -IF NOT EXISTS (SELECT name FROM master..syslogins WHERE name = 'test_password2') -BEGIN - CREATE LOGIN test_password2 WITH PASSWORD='!} ;4triou'; -END -GO - -IF NOT EXISTS (SELECT name FROM master..syslogins WHERE name = 'test_password3') -BEGIN - CREATE LOGIN test_password3 WITH PASSWORD='! ;4triou}'; -END -GO - ---the following users will be granted access to the test database -USE $(dbname); -GO - -CREATE USER test_password FROM LOGIN test_password; -CREATE USER test_password2 FROM LOGIN test_password2; -CREATE USER test_password3 FROM LOGIN test_password3; -GO - diff --git a/test/functional/setup/test_types.sql b/test/functional/setup/test_types.sql index eef114de..6c5afb9b 100644 --- a/test/functional/setup/test_types.sql +++ b/test/functional/setup/test_types.sql @@ -1,28 +1,25 @@ -USE $(dbname) -GO - CREATE TABLE [test_types] ([bigint_type] BIGINT null, - [int_type] INT null, - [smallint_type] SMALLINT null, - [tinyint_type] TINYINT null, - [bit_type] BIT null, - [decimal_type] DECIMAL(38,0) null, - [money_type] MONEY null, - [smallmoney_type] SMALLMONEY null, - [float_type] FLOAT(53) null, - [real_type] REAL null, - [datetime_type] DATETIME null, - [smalldatetime_type] SMALLDATETIME null ); + [int_type] INT null, + [smallint_type] SMALLINT null, + [tinyint_type] TINYINT null, + [bit_type] BIT null, + [decimal_type] DECIMAL(38,0) null, + [money_type] MONEY null, + [smallmoney_type] SMALLMONEY null, + [float_type] FLOAT(53) null, + [real_type] REAL null, + [datetime_type] DATETIME null, + [smalldatetime_type] SMALLDATETIME null ); GO -- maximum test -INSERT INTO $(dbname)..[test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) +INSERT INTO [test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) VALUES (9223372036854775807, 2147483647, 32767, 255, 1, 9999999999999999999999999999999999999, '12/12/1968 16:20', 922337203685477.5807, 214748.3647, 1.79E+308, 1.18E-38 ) -- minimum test -INSERT INTO $(dbname)..[test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) +INSERT INTO [test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) VALUES (-9223372036854775808, -2147483648, -32768, 0, 0, -10000000000000000000000000000000000001,'12/12/1968 16:20', -922337203685477.5808, -214748.3648, -1.79E+308, -1.18E-38 ) -- zero test -INSERT INTO $(dbname)..[test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) +INSERT INTO [test_types] (bigint_type, int_type, smallint_type, tinyint_type, bit_type, decimal_type, datetime_type, money_type, smallmoney_type, float_type, real_type) VALUES (0, 0, 0, 0, 0, 0, '12/12/1968 16:20', 0, 0, 0, 0) GO diff --git a/test/functional/setup/tracks.sql b/test/functional/setup/tracks.sql index 2ba3b793..57bd914f 100644 --- a/test/functional/setup/tracks.sql +++ b/test/functional/setup/tracks.sql @@ -1,6 +1,3 @@ -USE $(dbname) -GO - IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[tracks]') AND type in (N'U')) diff --git a/test/functional/sqlsrv/0013.phpt b/test/functional/sqlsrv/0013.phpt index cb2236df..6ccb4642 100644 --- a/test/functional/sqlsrv/0013.phpt +++ b/test/functional/sqlsrv/0013.phpt @@ -1,7 +1,7 @@ --TEST-- A test for a simple query --SKIPIF-- - + --FILE-- + --FILE-- + --FILE-- + + --FILE-- + --FILE-- + --FILE-- + --FILE-- + --FILE-- \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_574_next_result.phpt b/test/functional/sqlsrv/sqlsrv_574_next_result.phpt index c21f7178..82cb4b4e 100644 --- a/test/functional/sqlsrv/sqlsrv_574_next_result.phpt +++ b/test/functional/sqlsrv/sqlsrv_574_next_result.phpt @@ -6,6 +6,7 @@ Verifies the functionality of sqlsrv_next_result PHPT_EXEC=true --SKIPIF-- + --FILE-- +--FILE-- + "UTF-8")); +if (!$conn) { + fatalError("Could not connect.\n"); +} + +$dataTypes = array("VARCHAR(256)", "VARCHAR(512)", "VARCHAR(max)", "NVARCHAR(256)", "NVARCHAR(512)", "NVARCHAR(max)"); +for ($i = 0, $p = 3; $i < count($dataTypes); $i++, $p++) { + // Create the stored procedure first + $storedProcName = "spNullOutputParam" . $i; + $procArgs = "@OUTPUT $dataTypes[$i] OUTPUT"; + $procCode = "SELECT 1, 2, 3"; + + createProc($conn, $storedProcName, $procArgs, $procCode); + getOutputParam($conn, $storedProcName, false, ($i < 3)); + getOutputParam($conn, $storedProcName, true, ($i < 3)); + + // Drop the stored procedure + dropProc($conn, $storedProcName); +} + +echo "Done\n"; + +sqlsrv_close($conn); + +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt index eb1da333..51f61b27 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt @@ -13,31 +13,26 @@ require_once("MsSetup.inc"); $connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd, "Authentication"=>'SqlPassword', "TrustServerCertificate"=>true); -$conn = sqlsrv_connect( $server, $connectionInfo ); +$conn = sqlsrv_connect($server, $connectionInfo); -if( $conn === false ) -{ +if ($conn === false) { echo "Could not connect with Authentication=SqlPassword.\n"; - var_dump( sqlsrv_errors() ); -} -else -{ + var_dump(sqlsrv_errors()); +} else { echo "Connected successfully with Authentication=SqlPassword.\n"; } -$stmt = sqlsrv_query( $conn, "SELECT count(*) FROM cd_info" ); -if ( $stmt === false ) -{ +// For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql +$stmt = sqlsrv_query($conn, "SELECT SERVERPROPERTY('EngineEdition')"); +if (sqlsrv_fetch($stmt)) { + $edition = sqlsrv_get_field($stmt, 0); + var_dump($edition); +} else { echo "Query failed.\n"; } -else -{ - $result = sqlsrv_fetch_array( $stmt ); - var_dump( $result ); -} -sqlsrv_free_stmt( $stmt ); -sqlsrv_close( $conn ); +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); /////////////////////////////////////////////////////////////////////////////////////////// // Test Azure AD with integrated authentication. This should fail because @@ -45,17 +40,14 @@ sqlsrv_close( $conn ); // $connectionInfo = array( "Authentication"=>"ActiveDirectoryIntegrated", "TrustServerCertificate"=>true ); -$conn = sqlsrv_connect( $server, $connectionInfo ); -if( $conn === false ) -{ +$conn = sqlsrv_connect($server, $connectionInfo); +if ($conn === false) { echo "Could not connect with Authentication=ActiveDirectoryIntegrated.\n"; $errors = sqlsrv_errors(); print_r($errors[0]); -} -else -{ +} else { echo "Connected successfully with Authentication=ActiveDirectoryIntegrated.\n"; - sqlsrv_close( $conn ); + sqlsrv_close($conn); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -67,36 +59,25 @@ $azureDatabase = $adDatabase; $azureUsername = $adUser; $azurePassword = $adPassword; -if ($azureServer != 'TARGET_AD_SERVER') -{ - $connectionInfo = array( "UID"=>$azureUsername, "PWD"=>$azurePassword, +if ($azureServer != 'TARGET_AD_SERVER') { + $connectionInfo = array( "UID"=>$azureUsername, "PWD"=>$azurePassword, "Authentication"=>'ActiveDirectoryPassword', "TrustServerCertificate"=>false ); - $conn = sqlsrv_connect( $azureServer, $connectionInfo ); - if( $conn === false ) - { + $conn = sqlsrv_connect($azureServer, $connectionInfo); + if ($conn === false) { echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r( sqlsrv_errors() ); - } - else - { + print_r(sqlsrv_errors()); + } else { echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; - sqlsrv_close( $conn ); + sqlsrv_close($conn); } -} -else -{ +} else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } ?> --EXPECTF-- Connected successfully with Authentication=SqlPassword. -array(2) { - [0]=> - int(7) - [""]=> - int(7) -} +string(1) "%d" Could not connect with Authentication=ActiveDirectoryIntegrated. Array ( @@ -104,7 +85,7 @@ Array [SQLSTATE] => IMSSP [1] => -62 [code] => -62 - [2] => Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. - [message] => Invalid option for the Authentication keyword. Only SqlPassword or ActiveDirectoryPassword is supported. + [2] => Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. + [message] => Invalid option for the Authentication keyword. Only SqlPassword, ActiveDirectoryPassword, or ActiveDirectoryMsi is supported. ) %s with Authentication=ActiveDirectoryPassword. diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt new file mode 100644 index 00000000..644731eb --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_managed_identity.phpt @@ -0,0 +1,88 @@ +--TEST-- +Test some error conditions of Azure AD Managed Identity support +--DESCRIPTION-- +This test expects certain exceptions to be thrown under some conditions. +--SKIPIF-- + +--FILE-- +"", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty UID provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'empty PWD provided'); + unset($connectionInfo); + + $connectionInfo = array("PWD"=>"pwd", "Authentication" => "ActiveDirectoryMsi"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'PWD provided'); + unset($connectionInfo); + + $expectedError = 'When using Azure AD Access Token, the connection string must not contain UID, PWD, or Authentication keywords.'; + $connectionInfo = array("Authentication"=>"ActiveDirectoryMsi", "AccessToken" => "123"); + $conn = sqlsrv_connect($server, $connectionInfo); + verifyErrorMessage($conn, $expectedError, 'AccessToken option'); + unset($connectionInfo); +} + +function connectInvalidServer() +{ + global $server, $driver, $userName, $userPassword; + + $connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword, "Driver" => $driver); + $conn = sqlsrv_connect($server, $connectionInfo); + if ($conn === false) { + fatalError("Failed to connect in connectInvalidServer."); + } + + $msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + + if ($version[0] < 17 || $version[1] < 3) { + //skip the rest of this test, which requires ODBC driver 17.3 or above + return; + } + sqlsrv_close($conn); + + // Try connecting to an invalid server, should get an exception from ODBC + $connectionInfo = array("Authentication"=>"ActiveDirectoryMsi"); + $conn = sqlsrv_connect('invalidServer', $connectionInfo); + if ($conn) { + fatalError("AzureAD Managed Identity test: expected to fail with invalidServer\n"); + } else { + // TODO: check the exception message here, using verifyErrorMessage() + } +} + +// Test some error conditions +connectWithInvalidOptions($server); + +// Make a connection to an invalid server +connectInvalidServer(); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt index 5f456108..08f42b26 100644 --- a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt +++ b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt @@ -1,7 +1,7 @@ --TEST-- large types to strings of 1MB size. --SKIPIF-- - + --FILE-- + --FILE-- + --FILE-- + --FILE-- SQLSRV_CURSOR_CLIENT_BUFFERED)); if ($stmt === false) { printErrors(); diff --git a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize.phpt b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize.phpt index 6b4d1100..2c0e1b42 100644 --- a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize.phpt +++ b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize.phpt @@ -1,5 +1,7 @@ --TEST-- -sqlsrv_has_rows() using a forward and scrollable cursor +GitHub issue #228 - how ClientBufferMaxKBSize affects sqlsrv_has_rows and sqlsrv_fetch_array +--DESCRIPTION-- +Based on the example in GitHub issue 228, configuring ClientBufferMaxKBSize with sqlsrv_configure. --SKIPIF-- --FILE-- @@ -7,6 +9,20 @@ sqlsrv_has_rows() using a forward and scrollable cursor require_once('MsCommon.inc'); +function testErrors($conn) +{ + // set client buffer size to 0KB returns false + $ret = sqlsrv_configure('ClientBufferMaxKBSize', 0); + if (!$ret) { + echo sqlsrv_errors()[0]['message'] . "\n"; + } + + $ret = sqlsrv_configure('ClientBufferMaxKBSize', -1.9); + if (!$ret) { + echo sqlsrv_errors()[0]['message'] . "\n"; + } +} + function fetchData($conn, $table, $size) { $ret = sqlsrv_configure('ClientBufferMaxKBSize', $size); @@ -16,10 +32,13 @@ function fetchData($conn, $table, $size) echo("ClientBufferMaxKBSize is $attr\n"); sqlsrv_execute($stmt); + if ($size < 2) { + echo sqlsrv_errors()[0]['message'] . "\n"; + } + $rows = sqlsrv_has_rows($stmt); var_dump($rows); - sqlsrv_execute($stmt); $numRowsFetched = 0; while ($row = sqlsrv_fetch_array($stmt)) { $numRowsFetched++; @@ -40,18 +59,17 @@ $stmt = AE\createTable($conn, $tableName1, $columns); unset($columns); $columns = array(new AE\ColumnMeta('int', 'c1_int'), - new AE\ColumnMeta('varchar(1036)', 'c2_varchar_1036')); + new AE\ColumnMeta('varchar(1400)', 'c2_varchar_1400')); $stmt = AE\createTable($conn, $tableName2, $columns); -// insert > 1KB into c2_varchar_max & c2_varchar_1036 (1036 characters). -$longString = 'This is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a testThis is a test'; +// insert > 1KB into c2_varchar_max & c2_varchar_1400 (1400 characters). +$longString = str_repeat('This is a test', 100); $stmt = AE\insertRow($conn, $tableName1, array('c1_int' => 1, 'c2_varchar_max' => $longString)); -$stmt = AE\insertRow($conn, $tableName2, array('c1_int' => 1, 'c2_varchar_1036' => $longString)); +$stmt = AE\insertRow($conn, $tableName2, array('c1_int' => 1, 'c2_varchar_1400' => $longString)); +sqlsrv_free_stmt($stmt); -// set client buffer size to 0KB returns false -$ret = sqlsrv_configure('ClientBufferMaxKBSize', 0); -var_dump($ret); +testErrors($conn); // set client buffer size to 1KB $size = 1; @@ -62,19 +80,24 @@ $size = 2; fetchData($conn, $tableName1, $size); // this should return 1 row. fetchData($conn, $tableName2, $size); // this should return 1 row. -sqlsrv_free_stmt($stmt); +dropTable($conn, $tableName1); +dropTable($conn, $tableName2); + sqlsrv_close($conn); print "Done" ?> --EXPECT-- -bool(false) +Setting for ClientBufferMaxKBSize was non-int or non-positive. +Setting for ClientBufferMaxKBSize was non-int or non-positive. bool(true) ClientBufferMaxKBSize is 1 +Memory limit of 1 KB exceeded for buffered query bool(false) Number of rows fetched: 0 bool(true) ClientBufferMaxKBSize is 1 +Memory limit of 1 KB exceeded for buffered query bool(false) Number of rows fetched: 0 bool(true) diff --git a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt new file mode 100644 index 00000000..459169d9 --- /dev/null +++ b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt @@ -0,0 +1,109 @@ +--TEST-- +GitHub issue #228 - how ClientBufferMaxKBSize affects sqlsrv_has_rows and sqlsrv_fetch_array +--DESCRIPTION-- +A variation of the example in GitHub issue 228, using ClientBufferMaxKBSize the statement option. +--SKIPIF-- + +--FILE-- +"buffered", "ClientBufferMaxKBSize" => 0)); + if ($stmt !== false) { + echo("Setting client buffer size to 0KB should have failed\n"); + } else { + if (strpos(sqlsrv_errors()[0]['message'], $error) === false) { + print_r(sqlsrv_errors()); + } + } + + // set client buffer size to 0.99KB + $stmt = sqlsrv_prepare($conn, $query, array(), array("Scrollable"=>"buffered", "ClientBufferMaxKBSize" => 0.99)); + if ($stmt !== false) { + echo("Setting client buffer size to 0.99KB should have failed\n"); + } else { + if (strpos(sqlsrv_errors()[0]['message'], $error) === false) { + print_r(sqlsrv_errors()); + } + } +} + +function fetchData($conn, $table, $size) +{ + $stmt = sqlsrv_prepare($conn, "SELECT * FROM $table", array(), array("Scrollable"=>"buffered", "ClientBufferMaxKBSize" => $size)); + + $numRowsExpected = ($size > 1) ? 1 : 0; + $res = sqlsrv_execute($stmt); + if ($res && $size < 2) { + echo "Expect this to fail\n"; + } else { + $error = 'Memory limit of 1 KB exceeded for buffered query'; + if (strpos(sqlsrv_errors()[0]['message'], $error) === false) { + print_r(sqlsrv_errors()); + } + } + + $rows = sqlsrv_has_rows($stmt); + if ($numRowsExpected && !$rows) { + fatalError("sqlsrv_has_rows failed\n"); + } + + $numRowsFetched = 0; + while ($row = sqlsrv_fetch_array($stmt)) { + $numRowsFetched++; + } + if ($numRowsExpected != $numRowsFetched) { + echo("Expected $numRowsExpected but number of rows fetched is $numRowsFetched\n"); + } +} + +// connect +$conn = AE\connect(); + +$tableName1 = 'php_test_table_1'; +$tableName2 = 'php_test_table_2'; + +// Create tables +$columns = array(new AE\ColumnMeta('int', 'c1_int'), + new AE\ColumnMeta('varchar(max)', 'c2_varchar_max')); +$stmt = AE\createTable($conn, $tableName1, $columns); + +unset($columns); +$columns = array(new AE\ColumnMeta('int', 'c1_int'), + new AE\ColumnMeta('varchar(1050)', 'c2_varchar_1050')); +$stmt = AE\createTable($conn, $tableName2, $columns); + +// insert > 1KB into c2_varchar_max & c2_varchar_1050 (1050 characters). +$longString = str_repeat('This is a test', 75); + +$stmt = AE\insertRow($conn, $tableName1, array('c1_int' => 1, 'c2_varchar_max' => $longString)); +$stmt = AE\insertRow($conn, $tableName2, array('c1_int' => 1, 'c2_varchar_1050' => $longString)); +sqlsrv_free_stmt($stmt); + +$error = 'Setting for ClientBufferMaxKBSize was non-int or non-positive'; +testErrors($conn, $tableName1, $error); + +// set client buffer size to 1KB +$size = 1; +fetchData($conn, $tableName1, $size); // this should return 0 rows. +fetchData($conn, $tableName2, $size); // this should return 0 rows. +// set client buffer size to 2KB +$size = 2; +fetchData($conn, $tableName1, $size); // this should return 1 row. +fetchData($conn, $tableName2, $size); // this should return 1 row. + +dropTable($conn, $tableName1); +dropTable($conn, $tableName2); + +sqlsrv_close($conn); +print "Done" +?> + +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/test_closeConnection.phpt b/test/functional/sqlsrv/test_closeConnection.phpt index 24c96e0d..f796bf80 100644 --- a/test/functional/sqlsrv/test_closeConnection.phpt +++ b/test/functional/sqlsrv/test_closeConnection.phpt @@ -1,7 +1,7 @@ --TEST-- using an already closed connection. --SKIPIF-- - + --FILE-- + --FILE-- + --FILE-- + --FILE-- >c%`KE&v->1Rwwa diff --git a/test/functional/sqlsrv/test_largeData.phpt b/test/functional/sqlsrv/test_largeData.phpt index e282df2a..4fa2d830 100644 --- a/test/functional/sqlsrv/test_largeData.phpt +++ b/test/functional/sqlsrv/test_largeData.phpt @@ -1,7 +1,7 @@ --TEST-- send a large amount (10MB) using encryption. --SKIPIF-- - + --FILE-- + --FILE-- ---FILE-- - "test_password", "pwd" => "! ;4triou" )); -if (!$conn) -{ - $errors = sqlsrv_errors(); - echo( $errors[0]["message"]); -} -sqlsrv_close( $conn ); - -$conn = toConnect(array( "UID" => "test_password2", "pwd" => "!}} ;4triou" )); -if (!$conn) -{ - $errors = sqlsrv_errors(); - echo( $errors[0]["message"]); -} -sqlsrv_close( $conn ); - -$conn = toConnect(array( "UID" => "test_password3", "pwd" => "! ;4triou}}" )); -if (!$conn) -{ - $errors = sqlsrv_errors(); - echo( $errors[0]["message"]); -} -sqlsrv_close( $conn ); - -$conn = toConnect(array( "UID" => "test_password3", "pwd" => "! ;4triou}" )); -if ($conn) -{ - echo( "Shouldn't have connected" ); -} -$errors = sqlsrv_errors(); -echo $errors[0]["message"]; -sqlsrv_close( $conn ); - -print "Test successful"; -?> ---EXPECTREGEX-- -An unescaped right brace \(}\) was found in either the user name or password. All right braces must be escaped with another right brace \(}}\)\. -Warning: sqlsrv_close\(\) expects parameter 1 to be resource, bool(ean){0,1} given in .+(\/|\\)test_non_alpha_password\.php on line 45 -Test successful diff --git a/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt b/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt index 5a1504200b659e81931689353de7ca16881c351b..cdf1453510012a233933bed64095435bb02bffb6 100644 GIT binary patch delta 20 bcmZn*><^sa%^9CqRa%r9pHjZj+gbwvQ^5zj delta 10 RcmeAVYzv&=y)ndE0{|Em1N#5~ diff --git a/test/functional/sqlsrv/test_stream.phpt b/test/functional/sqlsrv/test_stream.phpt index 3836341d..dee4e72e 100644 --- a/test/functional/sqlsrv/test_stream.phpt +++ b/test/functional/sqlsrv/test_stream.phpt @@ -14,7 +14,7 @@ Test for stream zombifying. fatalError("Failed to connect."); } - $stmt = sqlsrv_query($conn, "SELECT * FROM [test_streamable_types]"); + $stmt = sqlsrv_query($conn, "SELECT * FROM sys.objects"); $metadata = sqlsrv_field_metadata($stmt); $count = count($metadata); sqlsrv_fetch($stmt); diff --git a/test/functional/sqlsrv/test_warning_errors2.phpt b/test/functional/sqlsrv/test_warning_errors2.phpt index 4145565a..15353ac6 100644 --- a/test/functional/sqlsrv/test_warning_errors2.phpt +++ b/test/functional/sqlsrv/test_warning_errors2.phpt @@ -15,7 +15,7 @@ if( $conn === false ) { die( print_r( sqlsrv_errors(), true )); } -$stmt = sqlsrv_prepare( $conn, "SELECT * FROM [cd_info]"); +$stmt = sqlsrv_prepare( $conn, "SELECT * FROM sys.objects"); $result = sqlsrv_field_metadata( $stmt ); if( $result === false ) { From 3ba26932d8c835080de305c35c79035f3efa593f Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Tue, 19 Mar 2019 16:38:58 -0700 Subject: [PATCH 2/7] 5.6.1 hotfix (#959) --- CHANGELOG.md | 25 ++++ source/pdo_sqlsrv/pdo_stmt.cpp | 24 ++-- source/shared/version.h | 2 +- source/sqlsrv/config.w32 | 2 +- source/sqlsrv/stmt.cpp | 2 +- .../pdo_sqlsrv/pdo_937_metadata.phpt | 122 +++++++++++++++++ .../pdostatement_getColumnMeta.phpt | 3 +- ...tement_getColumnMeta_unicode_col_name.phpt | 3 +- test/functional/sqlsrv/srv_937_metadata.phpt | 125 ++++++++++++++++++ 9 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdo_937_metadata.phpt create mode 100644 test/functional/sqlsrv/srv_937_metadata.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 946ce626..b7916f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.6.1 - 2019-03-19 +Updated PECL release packages. Here is the list of updates: + +### Fixed +- Issue [#937](https://github.com/Microsoft/msphpsql/issues/937) - fixed assumptions made when calculating field or column metadata which may have resulted in application termination +- Issue [#955](https://github.com/Microsoft/msphpsql/issues/955) - modified sqlsrv config file such that it can be compiled independently of pdo_sqlsrv +- Pull Request [#946](https://github.com/Microsoft/msphpsql/pull/946) - fixed PDOStatement::getColumnMeta() to return false when something goes wrong + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) +- With ColumnEncryption enabled, calling stored procedures with XML parameters does not work (Issue [#674](https://github.com/Microsoft/msphpsql/issues/674)) +- In SUSE 15, Azure Active Directory connections may fail if PHP is installed from packages (Issue [#934](https://github.com/Microsoft/msphpsql/issues/934)) + ## 5.6.0 - 2019-02-15 Updated PECL release packages. Here is the list of updates: diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index cd8c697f..773ad953 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -1020,7 +1020,7 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In // colno - The index of the field for which to return the metadata. // return_value - zval* consisting of the metadata. // Return: -// 0 for failure, 1 for success. +// FAILURE for failure, SUCCESS for success. int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno, _Inout_ zval *return_value TSRMLS_DC) { PDO_RESET_STMT_ERROR; @@ -1029,20 +1029,28 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno try { SQLSRV_ASSERT( stmt != NULL, "pdo_sqlsrv_stmt_get_col_meta: pdo_stmt object was null" ); - SQLSRV_ASSERT( stmt->columns != NULL, "pdo_sqlsrv_stmt_get_col_meta: columns are not available." ); SQLSRV_ASSERT( Z_TYPE_P( return_value ) == IS_NULL, "Metadata already has value. Must be NULL." ); - sqlsrv_malloc_auto_ptr core_meta_data; - sqlsrv_stmt* driver_stmt = static_cast( stmt->driver_data ); SQLSRV_ASSERT( driver_stmt != NULL, "pdo_sqlsrv_stmt_get_col_meta: stmt->driver_data was null"); - SQLSRV_ASSERT( colno >= 0 && colno < stmt->column_count, "pdo_sqlsrv_stmt_get_col_meta: invalid column number." ); + // Based on PDOStatement::getColumnMeta API, this should return FALSE + // if the requested column does not exist in the result set, or if + // no result set exists. Thus, do not use SQLSRV_ASSERT, which causes + // the script to fail right away. Instead, log this warning if logging + // is enabled + if (colno < 0 || colno >= stmt->column_count || stmt->columns == NULL) { + LOG( SEV_WARNING, "Invalid column number %1!d!", colno ); + return FAILURE; + } - core_meta_data = core_sqlsrv_field_metadata( driver_stmt, (SQLSMALLINT) colno TSRMLS_CC ); // initialize the array to nothing, as PDO requires us to create it core::sqlsrv_array_init( *driver_stmt, return_value TSRMLS_CC ); + sqlsrv_malloc_auto_ptr core_meta_data; + + core_meta_data = core_sqlsrv_field_metadata( driver_stmt, (SQLSMALLINT) colno TSRMLS_CC ); + // add the following fields: flags, native_type, driver:decl_type, table add_assoc_long( return_value, "flags", 0 ); @@ -1088,14 +1096,14 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno } catch( core::CoreException& ) { - return 0; + return FAILURE; } catch(...) { DIE( "pdo_sqlsrv_stmt_get_col_meta: Unknown exception occurred while retrieving metadata." ); } - return 1; + return SUCCESS; } diff --git a/source/shared/version.h b/source/shared/version.h index c7c55481..7424d900 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -27,7 +27,7 @@ // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 #define SQLVERSION_MINOR 6 -#define SQLVERSION_PATCH 0 +#define SQLVERSION_PATCH 1 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index 5a2477e9..887d1e77 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -27,7 +27,7 @@ if( PHP_SQLSRV != "no" ) { if (CHECK_LIB("odbc32.lib", "sqlsrv") && CHECK_LIB("odbccp32.lib", "sqlsrv") && CHECK_LIB("version.lib", "sqlsrv") && CHECK_LIB("psapi.lib", "sqlsrv")&& CHECK_HEADER_ADD_INCLUDE( "core_sqlsrv.h", "CFLAGS_SQLSRV", configure_module_dirname + "\\shared")) { - if (PHP_PDO_SQLSRV == "no" || PHP_SQLSRV_SHARED) { + if (PHP_SQLSRV_SHARED || PHP_PDO_SQLSRV == "no") { ADD_SOURCES( configure_module_dirname + "\\shared", shared_src_class, "sqlsrv" ); } CHECK_HEADER_ADD_INCLUDE("sql.h", "CFLAGS_SQLSRV_ODBC"); diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index c366e030..9d60de25 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -1807,7 +1807,7 @@ SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt * stmt) throw; } - SQLSRV_ASSERT(num_cols > 0 && stmt->current_meta_data.size() == num_cols, "Meta data vector out of sync" ); + SQLSRV_ASSERT(stmt->current_meta_data.size() == num_cols, "Meta data vector out of sync" ); return num_cols; } diff --git a/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt b/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt new file mode 100644 index 00000000..d0cad7e7 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_937_metadata.phpt @@ -0,0 +1,122 @@ +--TEST-- +GitHub issue 937 - getting metadata will not fail after an UPDATE / DELETE statement +--DESCRIPTION-- +Verifies that getColumnMeta will not fail after processing an UPDATE / DELETE query that returns no fields. Instead, it should simply return FALSE because no result set exists. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +getColumnMeta(0); + if ($metadata !== FALSE) { + echo "Expects FALSE because no result set exists!\n"; + } +} + +try { + $conn = connect(); + + dropTable($conn, $tableName); + dropProc($conn, $procName); + + $tsql = "CREATE TABLE $tableName([id] [int] NOT NULL, [name] [varchar](10) NOT NULL)"; + $conn->query($tsql); + + $id = 3; + $tsql = "INSERT INTO $tableName VALUES ($id, 'abcde')"; + $conn->query($tsql); + + $tsql = "UPDATE $tableName SET name = 'updated' WHERE id = $id"; + $stmt = $conn->prepare($tsql); + $stmt->execute(); + $numCol = $metadata = $stmt->columnCount(); + echo "Number of columns after UPDATE: $numCol\n"; + checkMetaData($stmt); + + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql); + $numCol = $metadata = $stmt->columnCount(); + for ($i = 0; $i < $numCol; $i++) { + $metadata = $stmt->getColumnMeta($i); + var_dump($metadata); + } + + createProc($conn, $procName, "@id int, @val varchar(10) OUTPUT", "SELECT @val = name FROM $tableName WHERE id = @id"); + + $value = ''; + $tsql = "{CALL [$procName] (?, ?)}"; + $stmt = $conn->prepare($tsql); + $stmt->bindParam(1, $id, PDO::PARAM_INT); + $stmt->bindParam(2, $value, PDO::PARAM_STR, 10); + $stmt->execute(); + $numCol = $metadata = $stmt->columnCount(); + echo "Number of columns after PROCEDURE: $numCol\n"; + echo "Value returned: $value\n"; + checkMetaData($stmt); + + $query = "DELETE FROM $tableName WHERE name = 'updated'"; + $stmt = $conn->query($query); + $numCol = $metadata = $stmt->columnCount(); + echo "Number of columns after DELETE: $numCol\n"; + checkMetaData($stmt); +} catch (PDOException $e) { + echo $e->getMessage() . PHP_EOL; +} + +dropTable($conn, $tableName); +dropProc($conn, $procName); + +unset($stmt); +unset($conn); + +?> +--EXPECT-- +Number of columns after UPDATE: 0 +array(8) { + ["flags"]=> + int(0) + ["sqlsrv:decl_type"]=> + string(3) "int" + ["native_type"]=> + string(6) "string" + ["table"]=> + string(0) "" + ["pdo_type"]=> + int(2) + ["name"]=> + string(2) "id" + ["len"]=> + int(10) + ["precision"]=> + int(0) +} +array(8) { + ["flags"]=> + int(0) + ["sqlsrv:decl_type"]=> + string(7) "varchar" + ["native_type"]=> + string(6) "string" + ["table"]=> + string(0) "" + ["pdo_type"]=> + int(2) + ["name"]=> + string(4) "name" + ["len"]=> + int(10) + ["precision"]=> + int(0) +} +Number of columns after PROCEDURE: 0 +Value returned: updated +Number of columns after DELETE: 0 diff --git a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt index f42d1d3d..40805dc2 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta.phpt @@ -220,5 +220,4 @@ array(7) { Warning: PDOStatement::getColumnMeta(): SQLSTATE[42P10]: Invalid column reference: column number must be non-negative in %s on line %x bool(false) - -Fatal error: pdo_sqlsrv_stmt_get_col_meta: invalid column number. in %s on line %x +bool(false) diff --git a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt index bb01be10..e7075ee4 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_getColumnMeta_unicode_col_name.phpt @@ -259,5 +259,4 @@ array(7) { Warning: PDOStatement::getColumnMeta(): SQLSTATE[42P10]: Invalid column reference: column number must be non-negative in %s on line %x bool(false) - -Fatal error: pdo_sqlsrv_stmt_get_col_meta: invalid column number. in %s on line %x +bool(false) \ No newline at end of file diff --git a/test/functional/sqlsrv/srv_937_metadata.phpt b/test/functional/sqlsrv/srv_937_metadata.phpt new file mode 100644 index 00000000..47c9452f --- /dev/null +++ b/test/functional/sqlsrv/srv_937_metadata.phpt @@ -0,0 +1,125 @@ +--TEST-- +GitHub issue #937 - getting metadata will not fail after an UPDATE / DELETE statement +--DESCRIPTION-- +Verifies that sqlsrv_field_metadata will return an empty array after processing an +UPDATE / DELETE query that returns no fields. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + "buffered"); +$tsql = "DELETE FROM $tableName WHERE dummyColumn = 'updated'"; +$stmt = sqlsrv_query($conn, $tsql, array(), $options); +$fieldmeta = sqlsrv_field_metadata($stmt); +var_dump($fieldmeta); + +dropTable($conn, $tableName); +dropProc($conn, $procName); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +array(2) { + [0]=> + array(6) { + ["Name"]=> + string(2) "id" + ["Type"]=> + int(4) + ["Size"]=> + NULL + ["Precision"]=> + int(10) + ["Scale"]=> + NULL + ["Nullable"]=> + int(0) + } + [1]=> + array(6) { + ["Name"]=> + string(11) "dummyColumn" + ["Type"]=> + int(12) + ["Size"]=> + int(10) + ["Precision"]=> + NULL + ["Scale"]=> + NULL + ["Nullable"]=> + int(0) + } +} +array(0) { +} +The value returned: updated +array(0) { +} +array(0) { +} \ No newline at end of file From 5dabd47e41bf4f863f8c56197c213a4e76d3ea25 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Thu, 9 May 2019 14:54:24 -0700 Subject: [PATCH 3/7] Updated links and versions (#987) --- Linux-mac-install.md | 12 ++++++------ README.md | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Linux-mac-install.md b/Linux-mac-install.md index 2a7357f4..e34f07b0 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -9,7 +9,7 @@ These instructions install PHP 7.3 by default. Note that some supported Linux di - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) - [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) - [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) -- [Installing the drivers on macOS El Capitan, Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-el-capitan-sierra-high-sierra-and-mojave) +- [Installing the drivers on macOS Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-sierra-high-sierra-and-mojave) ## Installing the drivers on Ubuntu 16.04, 18.04, and 18.10 @@ -38,7 +38,7 @@ exit ### Step 4. Install Apache and configure driver loading ``` sudo su -apt-get install libapache2-mod-php7.2 apache2 +apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork a2enmod php7.3 @@ -91,8 +91,8 @@ exit An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): ``` pecl download sqlsrv -tar xvzf sqlsrv-5.6.0.tgz -cd sqlsrv-5.6.0/ +tar xvzf sqlsrv-5.6.1.tgz +cd sqlsrv-5.6.1/ phpize ./configure --with-php-config=/usr/bin/php-config make @@ -214,7 +214,7 @@ sudo systemctl restart apache2 ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on macOS El Capitan, Sierra, High Sierra, and Mojave +## Installing the drivers on macOS Sierra, High Sierra, and Mojave If you do not already have it, install brew as follows: ``` @@ -259,7 +259,7 @@ apachectl -V | grep SERVER_CONFIG_FILE ``` and substitute the path for `httpd.conf` in the following commands: ``` -echo "LoadModule php7_module /usr/local/opt/php@7.2/lib/httpd/modules/libphp7.so" >> /usr/local/etc/httpd/httpd.conf +echo "LoadModule php7_module /usr/local/opt/php@7.3/lib/httpd/modules/libphp7.so" >> /usr/local/etc/httpd/httpd.conf (echo ""; echo "SetHandler application/x-httpd-php"; echo "";) >> /usr/local/etc/httpd/httpd.conf ``` ### Step 5. Restart Apache and test the sample script diff --git a/README.md b/README.md index 84952fe3..67183a90 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ Thank you for taking the time to participate in our last survey. You can continu [av-image]: https://ci.appveyor.com/api/projects/status/vo4rfei6lxlamrnc?svg=true [av-site]: https://ci.appveyor.com/project/msphpsql/msphpsql/branch/dev -[tv-image]: https://travis-ci.org/Microsoft/msphpsql.svg?branch=dev -[tv-site]: https://travis-ci.org/Microsoft/msphpsql/ -[Coverage Coveralls]: https://coveralls.io/repos/github/Microsoft/msphpsql/badge.svg?branch=dev -[coveralls-site]: https://coveralls.io/github/Microsoft/msphpsql?branch=dev +[tv-image]: https://travis-ci.org/microsoft/msphpsql.svg?branch=dev +[tv-site]: https://travis-ci.org/microsoft/msphpsql/ +[Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev +[coveralls-site]: https://coveralls.io/github/microsoft/msphpsql?branch=dev [Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/dev/graph/badge.svg [codecov-site]: https://codecov.io/gh/microsoft/msphpsql From bd06cf3e3f991298d209e84f727888fe685d94d4 Mon Sep 17 00:00:00 2001 From: David Puglielli Date: Thu, 5 Sep 2019 15:51:52 -0700 Subject: [PATCH 4/7] 5.7.0-preview merge (#1031) --- .travis.yml | 6 +- CHANGELOG.md | 44 ++ Dockerfile-msphpsql | 48 +- Linux-mac-install.md | 32 +- README.md | 8 +- appveyor.yml | 6 +- azure-pipelines.yml | 312 ++++++++++++ buildscripts/buildtools.py | 75 ++- media/os_development.PNG | Bin 0 -> 21753 bytes media/os_production.PNG | Bin 0 -> 22158 bytes media/php_versions.PNG | Bin 0 -> 11432 bytes media/sql_server.PNG | Bin 0 -> 17235 bytes source/packagize.sh | 14 +- source/pdo_sqlsrv/config.m4 | 2 +- source/pdo_sqlsrv/config.w32 | 2 +- source/pdo_sqlsrv/pdo_dbh.cpp | 124 +++-- source/pdo_sqlsrv/pdo_init.cpp | 3 +- source/pdo_sqlsrv/pdo_parser.cpp | 2 +- source/pdo_sqlsrv/pdo_stmt.cpp | 101 ++-- source/pdo_sqlsrv/pdo_util.cpp | 14 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 2 +- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 5 +- source/pdo_sqlsrv/template.rc | 2 +- source/shared/FormattedPrint.cpp | 2 +- source/shared/FormattedPrint.h | 2 +- source/shared/StringFunctions.cpp | 2 +- source/shared/StringFunctions.h | 2 +- source/shared/core_conn.cpp | 40 +- source/shared/core_init.cpp | 2 +- source/shared/core_results.cpp | 4 +- source/shared/core_sqlsrv.h | 121 ++++- source/shared/core_stmt.cpp | 465 ++++++++++++------ source/shared/core_stream.cpp | 28 +- source/shared/core_util.cpp | 251 +++++++++- source/shared/globalization.h | 2 +- source/shared/interlockedatomic.h | 2 +- source/shared/interlockedatomic_gcc.h | 2 +- source/shared/interlockedslist.h | 2 +- source/shared/localization.hpp | 2 +- source/shared/localizationimpl.cpp | 23 +- source/shared/msodbcsql.h | 8 +- source/shared/sal_def.h | 2 +- source/shared/typedefs_for_linux.h | 2 +- source/shared/version.h | 8 +- source/shared/xplat.h | 2 +- source/shared/xplat_intsafe.h | 2 +- source/shared/xplat_winerror.h | 2 +- source/shared/xplat_winnls.h | 2 +- source/sqlsrv/config.m4 | 2 +- source/sqlsrv/config.w32 | 2 +- source/sqlsrv/conn.cpp | 19 +- source/sqlsrv/init.cpp | 4 +- source/sqlsrv/php_sqlsrv.h | 2 +- source/sqlsrv/php_sqlsrv_int.h | 6 +- source/sqlsrv/stmt.cpp | 31 +- source/sqlsrv/template.rc | 2 +- source/sqlsrv/util.cpp | 18 +- test/functional/output.py | 37 +- .../pdo_sqlsrv/MsCommon_mid-refactor.inc | 23 + .../pdo_sqlsrv/PDO29_ConnInterface.phpt | 8 + .../pdo_sqlsrv/PDO32_StmtInterface.phpt | 8 + .../pdo_sqlsrv/PDO81_MemoryCheck.phpt | 60 ++- ...y_encoding_error_bound_by_name_errors.phpt | 164 ++++++ .../pdo_sqlsrv/pdo_569_query_varcharmax.phpt | 106 ++++ .../pdo_sqlsrv/pdo_929_language_option.phpt | 54 ++ .../pdo_sqlsrv/pdo_azure_ad_access_token.phpt | 42 +- .../pdo_azure_ad_authentication.phpt | 36 +- .../pdo_sqlsrv/pdo_batch_query.phpt | 203 ++++++++ .../pdo_sqlsrv/pdo_construct_attr_errors.phpt | 140 ++++++ .../pdo_sqlsrv/pdo_data_classification.phpt | 322 ++++++++++++ .../pdo_sqlsrv/pdo_empty_result_error.phpt | 3 + .../pdo_sqlsrv/pdo_escape_braces.phpt | 71 +++ .../pdo_sqlsrv/pdo_fetch_column_twice.phpt | 151 ++++++ .../pdo_fetch_datetime_time_as_objects.phpt | 54 +- .../pdo_fetch_datetime_time_nulls.phpt | 1 - .../pdo_fetch_variants_diff_styles.phpt | 2 +- .../pdo_insert_fetch_invalid_utf16.phpt | 86 ++++ .../pdo_insert_fetch_utf8stream.phpt | 106 ++++ .../pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt | 233 +++++++++ .../pdo_sqlsrv/pdo_output_decimal.phpt | 4 +- .../pdo_sqlsrv/pdo_output_decimal_errors.phpt | 128 +++++ .../pdo_sqlsrv/pdo_test_non_LOB_types.phpt | 78 +++ test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt | 2 +- .../pdo_sqlsrv/pdo_warning_errors.phpt | 107 ++++ .../pdo_sqlsrv/pdostatement_fetchAll.phpt | 2 +- .../pdostatement_fetch_orientation.phpt | 16 +- .../pdo_sqlsrv/pdostatement_fetch_style.phpt | 2 +- .../pdostatement_format_money_types.phpt | 14 +- .../pdo_sqlsrv/pdostatement_get_set_attr.phpt | 108 ++-- test/functional/setup/build_ksp.py | 122 ----- test/functional/setup/ksp_app.c | 305 ------------ test/functional/setup/myKSP.c | 132 ----- test/functional/setup/run_ksp.py | 57 --- test/functional/sqlsrv/0075.phpt | 34 +- test/functional/sqlsrv/MsCommon.inc | 20 + test/functional/sqlsrv/TC81_MemoryCheck.phpt | 8 +- .../sqlsrv/sqlsrv_378_out_param_error.phpt | 77 ++- .../sqlsrv/sqlsrv_ae_insert_datetime.phpt | 68 +-- .../sqlsrv/sqlsrv_ae_insert_retrieve.phpt | 16 +- .../sqlsrv_ae_insert_retrieve_fixed_size.phpt | 25 +- ...lsrv_ae_output_param_sqltype_datetime.phpt | 3 +- ...qlsrv_ae_output_param_sqltype_numeric.phpt | 3 +- ...sqlsrv_ae_output_param_sqltype_string.phpt | 3 +- .../sqlsrv/sqlsrv_azure_ad_access_token.phpt | 42 +- .../sqlsrv_azure_ad_authentication.phpt | 45 +- .../functional/sqlsrv/sqlsrv_batch_query.phpt | 199 ++++++++ test/functional/sqlsrv/sqlsrv_connStr.phpt | 111 +---- .../sqlsrv/sqlsrv_data_classification.phpt | 312 ++++++++++++ .../functional/sqlsrv/sqlsrv_data_to_str.phpt | 14 +- .../sqlsrv/sqlsrv_empty_result_error.phpt | 2 +- .../sqlsrv/sqlsrv_escape_braces.phpt | 70 +++ .../sqlsrv_statement_format_money_types.phpt | 16 +- .../sqlsrv/srv_007_login_timeout.phpt | 41 +- ...8_sqlsrv_clientbuffermaxkbsize_option.phpt | 3 +- .../sqlsrv/srv_569_query_varcharmax.phpt | 123 +++++ .../sqlsrv/srv_570_fetch_varbinary.phpt | 143 ++++++ .../sqlsrv/test_error_encoding.phpt | 52 +- ...t_error_encoding_with_language_option.phpt | 55 +++ test/functional/sqlsrv/test_largeData.phpt | 157 +++--- test/functional/sqlsrv/test_scrollable.phpt | 22 +- .../sqlsrv/test_sqlsrv_phptype_stream.phpt | Bin 10383 -> 10421 bytes 121 files changed, 5087 insertions(+), 1432 deletions(-) create mode 100644 azure-pipelines.yml create mode 100644 media/os_development.PNG create mode 100644 media/os_production.PNG create mode 100644 media/php_versions.PNG create mode 100644 media/sql_server.PNG create mode 100644 test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_929_language_option.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_batch_query.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_data_classification.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_escape_braces.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_warning_errors.phpt delete mode 100644 test/functional/setup/build_ksp.py delete mode 100644 test/functional/setup/ksp_app.c delete mode 100644 test/functional/setup/myKSP.c delete mode 100644 test/functional/setup/run_ksp.py create mode 100644 test/functional/sqlsrv/sqlsrv_batch_query.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_data_classification.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_escape_braces.phpt create mode 100644 test/functional/sqlsrv/srv_569_query_varcharmax.phpt create mode 100644 test/functional/sqlsrv/srv_570_fetch_varbinary.phpt create mode 100644 test/functional/sqlsrv/test_error_encoding_with_language_option.phpt diff --git a/.travis.yml b/.travis.yml index 6d5141ed..0618bebf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,10 @@ env: - TEST_PHP_SQL_PWD=Password123 before_install: - - docker pull microsoft/mssql-server-linux:2017-latest + - docker pull mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu install: - - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d microsoft/mssql-server-linux:2017-latest + - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu - docker build --build-arg PHPSQLDIR=$PHPSQLDIR -t msphpsql-dev -f Dockerfile-msphpsql . before_script: @@ -43,7 +43,7 @@ script: - docker exec client bash -c 'for f in ./test/functional/pdo_sqlsrv/*.out; do ls $f 2>/dev/null; cat $f 2>/dev/null; done || true' - docker exec client python ./test/functional/setup/cleanup_dbs.py -dbname $SQLSRV_DBNAME - docker exec client python ./test/functional/setup/cleanup_dbs.py -dbname $PDOSQLSRV_DBNAME - - docker exec client coveralls -e ./source/shared/ --gcov-options '\-lp' + - docker exec client coveralls -i ./source/ -e ./source/shared/ -e ./test/ --gcov-options '\-lp' - docker stop client - docker ps -a diff --git a/CHANGELOG.md b/CHANGELOG.md index b7916f19..38271771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.7.0-preview - 2019-09-05 +Updated PECL release packages. Here is the list of updates: + +### Added +- Support for PHP 7.4 RC 1 +- Support for Linux Ubuntu 19.04 and Debian 10 +- Feature Request [#929](https://github.com/microsoft/msphpsql/issues/929) - new [Language option](https://github.com/microsoft/msphpsql/wiki/Features#language) - Pull Request [#930](https://github.com/microsoft/msphpsql/pull/930) +- [Data Classification Sensitivity Metadata Retrieval](https://github.com/microsoft/msphpsql/wiki/Features#data-classification-sensitivity-metadata), which requires [MS ODBC Driver 17.2+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) and [SQL Server 2019 release candidate](https://docs.microsoft.com/sql/sql-server/sql-server-ver15-release-notes?view=sqlallproducts-allversions#-release-candidate-rc) + +### Removed +- Dropped support for Ubuntu 18.10 + +### Fixed +- Issue [#570](https://github.com/microsoft/msphpsql/issues/570) - Fixed fetching varbinary data using client buffer with sqlsrv +- Pull Request [#972](https://github.com/microsoft/msphpsql/pull/972) - Removed redundant calls to retrieve the number of columns or rows in the current query result set +- Pull Request [#978](https://github.com/microsoft/msphpsql/pull/978) - PDO_SQLSRV implementation of PDO::getColumnMeta now references cached metadata rather than making an ODBC call every time +- Pull Request [#979](https://github.com/microsoft/msphpsql/pull/979) - Added support for data classification Sensitivity metadata retrieval +- Pull Request [#985](https://github.com/microsoft/msphpsql/pull/985) - Fixed memory issues with data classification data structures +- Issue [#432](https://github.com/microsoft/msphpsql/issues/432) - Having any invalid UTF-8 name in the connection string will no longer invoke misleading error messages +- Issue [#909](https://github.com/microsoft/msphpsql/issues/909) - Fixed potential exception with locale issues in macOS +- Pull Request [#992](https://github.com/microsoft/msphpsql/pull/992) - Produced the correct error when requesting Data Classification metadata with ODBC drivers prior to 17 +- Pull Request [#1001](https://github.com/microsoft/msphpsql/pull/1001) - Fixed compilation issue with PHP 7.4 alpha +- Pull Request [#1004](https://github.com/microsoft/msphpsql/pull/1004) - Fixed another compilation issue with PHP 7.4 alpha +- Pull Request [#1008](https://github.com/microsoft/msphpsql/pull/1008) - Improved data caching when fetching datetime objects +- Pull Request [#1011](https://github.com/microsoft/msphpsql/pull/1011) - Fixed a potential buffer overflow when parsing for escaped braces in the connection string +- Pull Request [#1015](https://github.com/microsoft/msphpsql/pull/1015) - Fixed compilation issues and addressed various memory leaks detected by PHP 7.4 beta 1 + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Data Classification metadata retrieval is not compatible with ODBC Driver 17.4.1 +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) +- With ColumnEncryption enabled, calling stored procedures with XML parameters does not work (Issue [#674](https://github.com/Microsoft/msphpsql/issues/674)) + ## 5.6.1 - 2019-03-19 Updated PECL release packages. Here is the list of updates: diff --git a/Dockerfile-msphpsql b/Dockerfile-msphpsql index ab289479..4960b584 100644 --- a/Dockerfile-msphpsql +++ b/Dockerfile-msphpsql @@ -1,15 +1,17 @@ -#Download base image ubuntu 16.04 +# Download base image ubuntu 18.04 -FROM ubuntu:16.04 +FROM ubuntu:18.04 # Update Ubuntu Software repository RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository ppa:ondrej/php -y && \ apt-get -y install \ - apt-transport-https \ + apt-transport-https \ apt-utils \ autoconf \ curl \ - libcurl3 \ + libcurl4 \ g++ \ gcc \ git \ @@ -17,18 +19,26 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && \ libxml2-dev \ locales \ make \ - php7.0 \ - php7.0-dev \ + php7.3 \ + php7.3-dev \ python-pip \ re2c \ unixodbc-dev \ - unzip && apt-get clean - + unzip && apt-get clean && \ + curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \ + curl https://packages.microsoft.com/config/ubuntu/18.04/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ + apt-get -y update && \ + export ACCEPT_EULA=Y && apt-get -y install msodbcsql17 mssql-tools && \ + update-alternatives --set php /usr/bin/php7.3 + ARG PHPSQLDIR=/REPO/msphpsql-dev ENV TEST_PHP_SQL_SERVER sql ENV TEST_PHP_SQL_UID sa ENV TEST_PHP_SQL_PWD Password123 +# update PATH after ODBC driver and tools are installed +ENV PATH="/opt/mssql-tools/bin:${PATH}" + # add locale iso-8859-1 RUN sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen RUN locale-gen en_US @@ -37,19 +47,12 @@ RUN locale-gen en_US RUN locale-gen en_US.UTF-8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' -#install ODBC driver -RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - -RUN curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > /etc/apt/sources.list.d/mssql-release.list - -RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools -ENV PATH="/opt/mssql-tools/bin:${PATH}" - -#install coveralls (upgrade both pip and requests first) +# install coveralls (upgrade both pip and requests first) RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade requests RUN python -m pip install cpp-coveralls -#Either Install git / download zip (One can see other strategies : https://ryanfb.github.io/etc/2015/07/29/git_strategies_for_docker.html ) +# Either Install git / download zip (One can see other strategies : https://ryanfb.github.io/etc/2015/07/29/git_strategies_for_docker.html ) #One option is to get source from zip file of repository. #another option is to copy source to build directory on image RUN mkdir -p $PHPSQLDIR @@ -59,14 +62,17 @@ WORKDIR $PHPSQLDIR/source/ RUN chmod +x ./packagize.sh RUN /bin/bash -c "./packagize.sh" -RUN echo "extension = pdo_sqlsrv.so" >> /etc/php/7.0/cli/conf.d/20-pdo_sqlsrv.ini -RUN echo "extension = sqlsrv.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` +RUN echo "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +RUN echo "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini WORKDIR $PHPSQLDIR/source/sqlsrv -RUN phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install +RUN /usr/bin/phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install WORKDIR $PHPSQLDIR/source/pdo_sqlsrv -RUN phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install +RUN /usr/bin/phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install + +RUN phpenmod sqlsrv pdo_sqlsrv +RUN php --ri sqlsrv && php --ri pdo_sqlsrv # set name of sql server host to use WORKDIR $PHPSQLDIR/test/functional/pdo_sqlsrv diff --git a/Linux-mac-install.md b/Linux-mac-install.md index e34f07b0..2a96c89c 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -5,13 +5,13 @@ These instructions install PHP 7.3 by default. Note that some supported Linux di ## Contents of this page: -- [Installing the drivers on Ubuntu 16.04, 18.04, and 18.10](#installing-the-drivers-on-ubuntu-1604-1804-and-1810) +- [Installing the drivers on Ubuntu 16.04, 18.04, and 19.04](#installing-the-drivers-on-ubuntu-1604-1804-and-1904) - [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) -- [Installing the drivers on Debian 8 and 9](#installing-the-drivers-on-debian-8-and-9) +- [Installing the drivers on Debian 8, 9 and 10](#installing-the-drivers-on-debian-8-9-and-10) - [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) - [Installing the drivers on macOS Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-sierra-high-sierra-and-mojave) -## Installing the drivers on Ubuntu 16.04, 18.04, and 18.10 +## Installing the drivers on Ubuntu 16.04, 18.04, and 19.04 > [!NOTE] > To install PHP 7.1 or 7.2, replace 7.3 with 7.1 or 7.2 in the following commands. @@ -31,10 +31,13 @@ Install the ODBC driver for Ubuntu by following the instructions on the [Linux a sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv sudo su -echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini -echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini exit +sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv ``` +If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + ### Step 4. Install Apache and configure driver loading ``` sudo su @@ -42,8 +45,6 @@ apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork a2enmod php7.3 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini exit ``` ### Step 5. Restart Apache and test the sample script @@ -91,8 +92,8 @@ exit An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): ``` pecl download sqlsrv -tar xvzf sqlsrv-5.6.1.tgz -cd sqlsrv-5.6.1/ +tar xvzf sqlsrv-5.7.0.tgz +cd sqlsrv-5.7.0/ phpize ./configure --with-php-config=/usr/bin/php-config make @@ -116,7 +117,7 @@ sudo apachectl restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Debian 8 and 9 +## Installing the drivers on Debian 8, 9 and 10 > [!NOTE] > To install PHP 7.1 or 7.2, replace 7.3 in the following commands with 7.1 or 7.2. @@ -145,10 +146,13 @@ locale-gen sudo pecl install sqlsrv sudo pecl install pdo_sqlsrv sudo su -echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini -echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini exit +sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv ``` +If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + ### Step 4. Install Apache and configure driver loading ``` sudo su @@ -156,8 +160,6 @@ apt-get install libapache2-mod-php7.3 apache2 a2dismod mpm_event a2enmod mpm_prefork a2enmod php7.3 -echo "extension=pdo_sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/30-pdo_sqlsrv.ini -echo "extension=sqlsrv.so" >> /etc/php/7.3/apache2/conf.d/20-sqlsrv.ini ``` ### Step 5. Restart Apache and test the sample script ``` @@ -168,7 +170,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Suse 12 and 15 > [!NOTE] -> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1, and similarly for other versions. Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. +> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. > [!NOTE] > Packages for PHP 7.3 are not available for Suse 12. diff --git a/README.md b/README.md index 67183a90..842a99b1 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,16 @@ Thank you for taking the time to participate in our last survey. You can continu ### Status of Most Recent Builds -| AppVeyor (Windows) | Travis CI (Linux) | Coverage (Windows) | Coverage (Linux) | -|--------------------------|--------------------------|---------------------------------------|-------------------------------------------| -| [![av-image][]][av-site] | [![tv-image][]][tv-site] | [![Coverage Codecov][]][codecov-site] | [![Coverage Coveralls][]][coveralls-site] | +Azure Pipelines | AppVeyor (Windows) | Travis CI (Linux) | Coverage (Windows) | Coverage (Linux) | +|---------------------|--------------------------|--------------------------|---------------------------------------|-------------------------------------------| +| [![az-image][]][az-site] | [![av-image][]][av-site] | [![tv-image][]][tv-site] | [![Coverage Codecov][]][codecov-site] | [![Coverage Coveralls][]][coveralls-site] | [av-image]: https://ci.appveyor.com/api/projects/status/vo4rfei6lxlamrnc?svg=true [av-site]: https://ci.appveyor.com/project/msphpsql/msphpsql/branch/dev [tv-image]: https://travis-ci.org/microsoft/msphpsql.svg?branch=dev [tv-site]: https://travis-ci.org/microsoft/msphpsql/ +[az-site]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_build/latest?definitionId=6&branchName=dev +[az-image]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_apis/build/status/Microsoft.msphpsql?branchName=dev [Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev [coveralls-site]: https://coveralls.io/github/microsoft/msphpsql?branch=dev [Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/dev/graph/badge.svg diff --git a/appveyor.yml b/appveyor.yml index ee9f2eb3..ae779335 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -81,10 +81,10 @@ install: } Else { $env:PHP_VERSION=$env:PHP_MAJOR_VER + '.' + $env:PHP_MINOR_VER; } - - echo Downloading MSODBCSQL 17.2 + - echo Downloading MSODBCSQL 17 # AppVeyor build works are x64 VMs and 32-bit ODBC driver cannot be installed on it - - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.2.0.1_x64.msi', 'c:\projects\msodbcsql_17.2.0.1_x64.msi') - - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.2.0.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.4.1.1_x64.msi', 'c:\projects\msodbcsql_17.4.1.1_x64.msi') + - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.4.1.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL - echo Checking the version of MSODBCSQL - reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" - dir %WINDIR%\System32\msodbcsql*.dll diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..3f048788 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,312 @@ +# Add steps that build, run tests, deploy, and more: +# https://aka.ms/yaml + +variables: + phpVersion: 7.3 + server: 'localhost,1433' + host: 'sql1' + sqlsrv_db: 'sqlsrv_testdb' + pdo_sqlsrv_db: 'pdo_sqlsrv_testdb' + uid: 'sa' + pwd: 'Password456!' + +trigger: +- dev + +jobs: +- job: macOS + pool: + vmImage: 'macOS-10.13' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + brew tap + brew tap homebrew/core + brew install autoconf automake libtool + brew install php@$(phpVersion) + php -v + displayName: 'Install PHP' + + - script: | + echo ready to build extensions + cd $(Build.SourcesDirectory)/source + chmod a+x packagize.sh + ./packagize.sh + + cd $(Build.SourcesDirectory)/source/sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/sqlsrv + + cd $(Build.SourcesDirectory)/source/pdo_sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + + echo extension=pdo_sqlsrv.so >> `php --ini | grep "Loaded Configuration File" | sed -e "s|.*:\s*||"` + echo extension=sqlsrv.so >> `php --ini | grep "Loaded Configuration File" | sed -e "s|.*:\s*||"` + + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Build and install drivers' + +- job: Linux + pool: + vmImage: 'ubuntu-16.04' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + sudo update-alternatives --set php /usr/bin/php$(phpVersion) + sudo update-alternatives --set phar /usr/bin/phar$(phpVersion) + sudo update-alternatives --set phpdbg /usr/bin/phpdbg$(phpVersion) + sudo update-alternatives --set php-cgi /usr/bin/php-cgi$(phpVersion) + sudo update-alternatives --set phar.phar /usr/bin/phar.phar$(phpVersion) + php -version + displayName: 'Use PHP version $(phpVersion)' + + - script: | + echo install ODBC and dependencies + sudo apt-get purge unixodbc + sudo apt autoremove + sudo curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - + curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > mssql-release.list + sudo mv mssql-release.list /etc/apt/sources.list.d/ + sudo apt-get update + sudo ACCEPT_EULA=Y apt-get install msodbcsql17 mssql-tools + echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bash_profile + echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc + source ~/.bashrc + sudo apt-get install unixodbc-dev + odbcinst --j + odbcinst -q -d -n "ODBC Driver 17 for SQL Server" + displayName: 'Install prerequisites' + + - script: | + docker pull mcr.microsoft.com/mssql/server:2017-latest + docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=$(pwd)' -p 1433:1433 -h $(host) --name=$(host) -d mcr.microsoft.com/mssql/server:2017-latest + docker ps -a + sleep 5 + docker exec -t $(host) /opt/mssql-tools/bin/sqlcmd -S $(server) -U $(uid) -P $(pwd) -Q 'select @@Version' + displayName: 'Run SQL Server for Linux' + + - script: | + sudo sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen + sudo locale-gen en_US + sudo locale-gen en_US.UTF-8 + export LANG='en_US.UTF-8' + export LANGUAGE='en_US:en' + export LC_ALL='en_US.UTF-8' + displayName: 'Generate locales for testing' + + - script: | + echo setting env variables + export TEST_PHP_SQL_SERVER='$(server)' + export TEST_PHP_SQL_UID='$(uid)' + export TEST_PHP_SQL_PWD='$(pwd)' + + cd $(Build.SourcesDirectory)/test/functional/setup + python ./setup_dbs.py -dbname $(sqlsrv_db) + python ./setup_dbs.py -dbname $(pdo_sqlsrv_db) + displayName: 'Set up test databases' + + - script: | + echo ready to build extensions + cd $(Build.SourcesDirectory)/source + chmod a+x packagize.sh + ./packagize.sh + + dest=`php --ini | grep "Scan for additional .ini files" | sudo sed -e "s|.*:\s*||"`/ + + cd $(Build.SourcesDirectory)/source/sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/sqlsrv + echo extension=sqlsrv.so >> 20-sqlsrv.ini + + echo copying sqlsrv to $dest + sudo cp 20-sqlsrv.ini $dest + + cd $(Build.SourcesDirectory)/source/pdo_sqlsrv + ls -al + phpize && ./configure && make && sudo make install + cp run-tests.php $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + echo extension=pdo_sqlsrv.so >> 30-pdo_sqlsrv.ini + + echo copying pdo_sqlsrv to $dest + sudo cp 30-pdo_sqlsrv.ini $dest + + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Build and install drivers' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/sqlsrv + sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc + sed -i -e 's/TARGET_DATABASE/'"$(sqlsrv_db)"'/g' MsSetup.inc + sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + + php run-tests.php -P ./*.phpt 2>&1 | tee ../sqlsrv.log + displayName: 'Run sqlsrv functional tests' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/pdo_sqlsrv + sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc + sed -i -e 's/TARGET_DATABASE/'"$(pdo_sqlsrv_db)"'/g' MsSetup.inc + sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + + php run-tests.php -P ./*.phpt 2>&1 | tee ../pdo_sqlsrv.log + displayName: 'Run pdo_sqlsrv functional tests' + + - script: | + cd $(Build.SourcesDirectory)/test/functional/ + for f in sqlsrv/*.diff; do ls $f 2>/dev/null; cat $f 2>/dev/null; echo ''; done || true + for f in pdo_sqlsrv/*.diff; do ls $f 2>/dev/null; cat $f 2>/dev/null; echo ''; done || true + python output.py + ls -l *.xml + displayName: 'Processing test results' + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '*.xml' + failTaskOnFailedTests: true + searchFolder: '$(Build.SourcesDirectory)/test/functional/' + + - script: | + docker stop $(host) + docker rm $(host) + displayName: 'Stop SQL Server for Linux' + condition: always() + +- job: Windows + pool: + vmImage: 'vs2017-win2016' + steps: + - checkout: self + clean: true + fetchDepth: 1 + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + architecture: 'x64' + + - script: | + dir C:\tools\php\php* + dir C:\tools\php\ext\ + echo extension_dir=C:\tools\php\ext >> C:\tools\php\php.ini + php --ini + php -v + displayName: 'Check PHP' + + - powershell: | + cd $(Build.SourcesDirectory)\test\functional\sqlsrv + (Get-Content .\MsSetup.inc) | ForEach-Object { $_ -replace "TARGET_SERVER", "$(host)" -replace "TARGET_DATABASE", "$(sqlsrv_db)" -replace "TARGET_USERNAME", "$(uid)" -replace "TARGET_PASSWORD", "$(pwd)" } | Set-Content .\MsSetup.inc + Select-String $(host) .\MsSetup.inc + Select-String $(sqlsrv_db) .\MsSetup.inc + cd $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + (Get-Content .\MsSetup.inc) | ForEach-Object { $_ -replace "TARGET_SERVER", "$(host)" -replace "TARGET_DATABASE", "$(pdo_sqlsrv_db)" -replace "TARGET_USERNAME", "$(uid)" -replace "TARGET_PASSWORD", "$(pwd)" } | Set-Content .\MsSetup.inc + Select-String $(host) .\MsSetup.inc + Select-String $(pdo_sqlsrv_db) .\MsSetup.inc + displayName: 'Update connection credentials' + condition: false + + - powershell: | + $client = New-Object Net.WebClient + $client.DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.3.1.1_x64.msi', 'msodbcsql_17.3.1.1_x64.msi') + $client.DownloadFile('https://download.microsoft.com/download/D/5/E/D5EEF288-A277-45C8-855B-8E2CB7E25B96/x64/msodbcsql.msi', 'msodbcsql_13.1.msi') + $client.DownloadFile('https://download.microsoft.com/download/4/C/C/4CC1A229-3C56-4A7F-A3BA-F903C73E5895/EN/x64/MsSqlCmdLnUtils.msi', 'MsSqlCmdLnUtils.msi') + dir *.msi + displayName: 'Download ODBC msi and sql tools msi' + condition: false + + - script: | + msiexec /i "msodbcsql_17.3.1.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" + dir %WINDIR%\System32\msodbcsql*.dll + displayName: 'Install ODBC driver' + condition: false + + # TOFIX: Install ODBC 13.1 because of SQLCMD 15 installation bug -- this step should be removed later + - script: msiexec /i "msodbcsql_13.1.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + condition: false + + # FOR SOME REASON the set up did not set the PATH + - script: | + msiexec /i "MsSqlCmdLnUtils.msi" /qn IACCEPTMSSQLCMDLNUTILSLICENSETERMS=YES + displayName: 'Install SQL command line utilities version 15' + condition: false + + - powershell: | + $client = New-Object Net.WebClient + $client.Headers.Add("user-agent", "azure pipeline build") + $client.DownloadFile("https://windows.php.net/downloads/releases/sha256sum.txt", "sha256sum.txt") + $env:VERSION=type sha256sum.txt | where { $_ -match "php-($(phpVersion)\.\d+)-src" } | foreach { $matches[1] } + Write-Host "Latest PHP $(phpVersion) is ${env:VERSION}" + cd $(Build.SourcesDirectory)/buildscripts/ + python builddrivers.py --PHPVER=${env:VERSION} --DRIVER=sqlsrv --ARCH=x64 --THREAD=nts --SOURCE=$(Build.SourcesDirectory)/source --TESTING --NO_RENAME + dir *sqlsrv*.dll + python builddrivers.py --PHPVER=${env:VERSION} --DRIVER=pdo_sqlsrv --ARCH=x64 --THREAD=nts --SOURCE=$(Build.SourcesDirectory)/source --TESTING --NO_RENAME + cp php-sdk\phpdev\vc15\x64\php-${env:VERSION}-src\run-tests.php $(Build.SourcesDirectory)\test\functional\sqlsrv + cp php-sdk\phpdev\vc15\x64\php-${env:VERSION}-src\run-tests.php $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + dir *sqlsrv*.dll + cp *sqlsrv*.dll C:\tools\php\ext\ + displayName: 'Build drivers (separately) for the latest version of PHP $(phpVersion)' + + - script: | + echo extension=php_sqlsrv.dll >> C:\tools\php\php.ini + echo extension=php_pdo_sqlsrv.dll >> C:\tools\php\php.ini + php --ri sqlsrv + php --ri pdo_sqlsrv + displayName: 'Load drivers' + + - script: | + docker pull microsoft/mssql-server-windows-developer + docker run -d --name sqlcontainer -h $(host) -p 1433:1433 -e sa_password=$(pwd) -e ACCEPT_EULA=Y microsoft/mssql-server-windows-developer + docker ps -a + displayName: 'Run SQL Server for Windows Server' + condition: false + + - script: | + set path=C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;%path% + sqlcmd -S $(host) -U $(uid) -P $(pwd) -Q "SELECT @@Version" + set TEST_PHP_SQL_SERVER=$(host) + set TEST_PHP_SQL_UID=$(uid) + set TEST_PHP_SQL_PWD=$(pwd) + cd $(Build.SourcesDirectory)\test\functional\setup + python setup_dbs.py -dbname $(sqlsrv_db) + python setup_dbs.py -dbname $(pdo_sqlsrv_db) + displayName: 'Set up test databases' + condition: false + + - script: | + cd $(Build.SourcesDirectory)\test\functional\sqlsrv + php run-tests.php -P sqlsrv_client_info.phpt + cd $(Build.SourcesDirectory)\test\functional\pdo_sqlsrv + php run-tests.php -P pdo_getAttribute_clientInfo.phpt + displayName: 'Smoke testing' + condition: false + + - script: | + docker stop sqlcontainer + docker rm sqlcontainer + displayName: 'Stop SQL Server for Windows Server' + condition: false \ No newline at end of file diff --git a/buildscripts/buildtools.py b/buildscripts/buildtools.py index 59f8b8f5..17713f24 100644 --- a/buildscripts/buildtools.py +++ b/buildscripts/buildtools.py @@ -42,7 +42,8 @@ class BuildUtil(object): self.thread = thread.lower() self.no_rename = no_rename self.debug_enabled = debug_enabled - + self.vc = '' + def major_version(self): """Return the major version number based on the PHP version.""" return self.phpver[0:3] @@ -66,17 +67,56 @@ class BuildUtil(object): version = self.version_label() return 'php_' + driver + '_' + version + '_' + self.thread + suffix - def compiler_version(self): - """Return the appropriate compiler version based on PHP version.""" - VC = 'vc14' - version = self.version_label() - if version >= '72': # Compiler version for PHP 7.2 or above - VC = 'vc15' - return VC + def determine_compiler(self, sdk_dir, vs_ver): + """Return the compiler version using vswhere.exe.""" + vswhere = os.path.join(sdk_dir, 'php-sdk', 'bin', 'vswhere.exe') + if not os.path.exists(vswhere): + print('Could not find ' + vswhere) + exit(1) - def phpsrc_root(self, sdk_dir): + # If both VS 2017 and VS 2019 are installed, if we check only version 15, + # both versions are returned. + # For example, temp.txt would have the following values (in this order): + # 16.1.29009.5 + # 15.9.28307.344 + # But if only VS 2017 is present, temp.txt will only have one value like this: + # 15.9.28307.344 + # Likewise, if only VS 2019 is present, temp.txt contains only the one for 16. + # We can achieve the above by checking for version [15,16), in which case + # even if both compilers are present, it only returns one. If only VS 2019 + # exists, temp.txt is empty + command = '{0} -version [{1},{2}) -property installationVersion '.format(vswhere, vs_ver, vs_ver + 1) + os.system(command + ' > temp.txt') + + # Read the first line from temp.txt + with open('temp.txt', 'r') as f: + ver = f.readline() + print('Version: ' + ver) + vc = ver[:2] + if vc == '15': + return 'vc15' + else: # For VS2019, it's 'vs' instead of 'vc' + return 'vs16' + + def compiler_version(self, sdk_dir): + """Return the appropriate compiler version based on PHP version.""" + if self.vc is '': + VC = 'vc14' + version = self.version_label() + if version >= '72': # Compiler version for PHP 7.2 or above + VC = 'vc15' + if version == '74': + # Compiler version for PHP 7.4 or above + # Can be compiled using VS 2017 or VS 2019 + print('Checking compiler versions...') + VC = self.determine_compiler(sdk_dir, 15) + self.vc = VC + print('Compiler: ' + self.vc) + return self.vc + + def phpsrc_root(self, sdk_dir): """Return the path to the PHP source folder based on *sdk_dir*.""" - vc = self.compiler_version() + vc = self.compiler_version(sdk_dir) return os.path.join(sdk_dir, 'php-sdk', 'phpdev', vc, self.arch, 'php-'+self.phpver+'-src') def build_abs_path(self, sdk_dir): @@ -97,6 +137,10 @@ class BuildUtil(object): def remove_old_builds(self, sdk_dir): """Remove the extensions, e.g. the driver subfolders in php-7.*-src\ext.""" + if not os.path.exists(os.path.join(sdk_dir, 'php-sdk')): + print('No old builds to be removed...') + return + print('Removing old builds...') phpsrc = self.phpsrc_root(sdk_dir) @@ -117,6 +161,10 @@ class BuildUtil(object): """Remove all binaries and source code in the Release* or Debug* folders according to the current configuration """ + if not os.path.exists(os.path.join(sdk_dir, 'php-sdk')): + print('No old builds to be removed...') + return + print('Removing previous build...') build_dir = self.build_abs_path(sdk_dir) if not os.path.exists(build_dir): @@ -282,7 +330,7 @@ class BuildUtil(object): else: # pdo_sqlsrv cmd_line = ' --enable-pdo --with-pdo-sqlsrv=shared ' + cmd_line - cmd_line = 'cscript configure.js --disable-all --enable-cli --enable-cgi --enable-embed' + cmd_line + cmd_line = 'cscript configure.js --disable-all --enable-cli --enable-cgi --enable-json --enable-embed' + cmd_line if self.thread == 'nts': cmd_line = cmd_line + ' --disable-zts' return cmd_line @@ -370,13 +418,16 @@ class BuildUtil(object): os.system('git clone https://github.com/OSTC/php-sdk-binary-tools.git --branch master --single-branch --depth 1 ' + phpSDK) os.chdir(phpSDK) os.system('git pull ') + print('Done cloning the latest php SDK...') # Move the generated batch file to phpSDK for the php starter script + print('Moving the sdk bath file over...') sdk_batch_file = os.path.join(phpSDK, batch_file) if os.path.exists(sdk_batch_file): os.remove(sdk_batch_file) shutil.move(os.path.join(work_dir, batch_file), phpSDK) + print('Checking if source exists...') sdk_source = os.path.join(phpSDK, 'Source') # Sometimes, for various reasons, the Source folder from previous build # might exist in phpSDK. If so, remove it first @@ -386,7 +437,7 @@ class BuildUtil(object): shutil.move(source_dir, phpSDK) # Invoke phpsdk--.bat - vc = self.compiler_version() + vc = self.compiler_version(sdk_dir) starter_script = 'phpsdk-' + vc + '-' + self.arch + '.bat' print('Running starter script: ', starter_script) os.system(starter_script + ' -t ' + batch_file) diff --git a/media/os_development.PNG b/media/os_development.PNG new file mode 100644 index 0000000000000000000000000000000000000000..5bd527901fe02cc98ea93ced0f231a351c8f0e57 GIT binary patch literal 21753 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 Aw*UYD literal 0 HcmV?d00001 diff --git a/media/php_versions.PNG b/media/php_versions.PNG new file mode 100644 index 0000000000000000000000000000000000000000..98416fe81b028903d03f43422c9c075c7b95481d GIT binary patch literal 11432 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 Wpb1Zi;;$iNUocmuIH=4u% literal 0 HcmV?d00001 diff --git a/source/packagize.sh b/source/packagize.sh index dc9051b3..637aefc1 100644 --- a/source/packagize.sh +++ b/source/packagize.sh @@ -1,11 +1,11 @@ #!/bin/bash -if [ "$1" == "" ]; then - cp -rf $PWD/shared $PWD/sqlsrv - cp -rf $PWD/shared $PWD/pdo_sqlsrv +if [[ -z $1 ]]; then + cp -rf "$PWD"/shared "$PWD"/sqlsrv + cp -rf "$PWD"/shared "$PWD"/pdo_sqlsrv else [[ -d $1 ]] || { echo "No such path!"; exit 1; } - cp -rf $PWD/sqlsrv $1 - cp -rf $PWD/pdo_sqlsrv $1 - cp -rf $PWD/shared $1/sqlsrv - cp -rf $PWD/shared $1/pdo_sqlsrv + cp -rf "$PWD"/sqlsrv "$1" + cp -rf "$PWD"/pdo_sqlsrv "$1" + cp -rf "$PWD"/shared "$1"/sqlsrv + cp -rf "$PWD"/shared "$1"/pdo_sqlsrv fi diff --git a/source/pdo_sqlsrv/config.m4 b/source/pdo_sqlsrv/config.m4 index 68dbea0e..75961ad7 100644 --- a/source/pdo_sqlsrv/config.m4 +++ b/source/pdo_sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.6 for PHP for SQL Server +dnl Microsoft Drivers 5.7 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index 95bff567..1d37593b 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 669bc62b..2a0ea479 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDO object for PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -48,6 +48,7 @@ const char AttachDBFileName[] = "AttachDbFileName"; const char Authentication[] = "Authentication"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; const char Database[] = "Database"; @@ -86,7 +87,8 @@ enum PDO_STMT_OPTIONS { PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, PDO_STMT_OPTION_FORMAT_DECIMALS, - PDO_STMT_OPTION_DECIMAL_PLACES + PDO_STMT_OPTION_DECIMAL_PLACES, + PDO_STMT_OPTION_DATA_CLASSIFICATION }; // List of all the statement options supported by this driver. @@ -103,6 +105,7 @@ const stmt_option PDO_STMT_OPTS[] = { { NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr( new stmt_option_fetch_datetime ) }, { NULL, 0, PDO_STMT_OPTION_FORMAT_DECIMALS, std::unique_ptr( new stmt_option_format_decimals ) }, { NULL, 0, PDO_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { NULL, 0, PDO_STMT_OPTION_DATA_CLASSIFICATION, std::unique_ptr( new stmt_option_data_classification ) }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -241,6 +244,15 @@ const connection_option PDO_CONN_OPTS[] = { CONN_ATTR_BOOL, conn_null_func::func }, + { + PDOConnOptionNames::Language, + sizeof( PDOConnOptionNames::Language ), + SQLSRV_CONN_OPTION_LANGUAGE, + ODBCConnOptions::Language, + sizeof( ODBCConnOptions::Language ), + CONN_ATTR_STRING, + conn_str_append_func::func + }, { PDOConnOptionNames::Driver, sizeof(PDOConnOptionNames::Driver), @@ -1126,6 +1138,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1183,7 +1196,8 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout // Statement level only case PDO_ATTR_EMULATE_PREPARES: case PDO_ATTR_CURSOR: - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + case SQLSRV_ATTR_DATA_CLASSIFICATION: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_STMT_LEVEL_ATTR ); } @@ -1584,70 +1598,75 @@ namespace { // Maps the PDO driver specific statement option/attribute constants to the core layer // statement option/attribute constants. -void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, - _Inout_ zval* data TSRMLS_DC ) +void add_stmt_option_key(_Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ HashTable* options_ht, + _Inout_ zval* data TSRMLS_DC) { - zend_ulong option_key = -1; - switch( key ) { - - case PDO_ATTR_CURSOR: - option_key = SQLSRV_STMT_OPTION_SCROLLABLE; - break; - - case SQLSRV_ATTR_ENCODING: - option_key = PDO_STMT_OPTION_ENCODING; - break; + zend_ulong option_key = -1; + switch (key) { - case SQLSRV_ATTR_QUERY_TIMEOUT: - option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; - break; + case PDO_ATTR_CURSOR: + option_key = SQLSRV_STMT_OPTION_SCROLLABLE; + break; - case PDO_ATTR_STATEMENT_CLASS: - break; + case SQLSRV_ATTR_ENCODING: + option_key = PDO_STMT_OPTION_ENCODING; + break; - case SQLSRV_ATTR_DIRECT_QUERY: - option_key = PDO_STMT_OPTION_DIRECT_QUERY; - break; + case SQLSRV_ATTR_QUERY_TIMEOUT: + option_key = SQLSRV_STMT_OPTION_QUERY_TIMEOUT; + break; - case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: - option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; - break; + case PDO_ATTR_STATEMENT_CLASS: + break; - case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: - option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; - break; + case SQLSRV_ATTR_DIRECT_QUERY: + option_key = PDO_STMT_OPTION_DIRECT_QUERY; + break; - case PDO_ATTR_EMULATE_PREPARES: - option_key = PDO_STMT_OPTION_EMULATE_PREPARES; - break; + case SQLSRV_ATTR_CURSOR_SCROLL_TYPE: + option_key = PDO_STMT_OPTION_CURSOR_SCROLL_TYPE; + break; - case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; - break; + case SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE: + option_key = PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE; + break; - case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: - option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; - break; + case PDO_ATTR_EMULATE_PREPARES: + option_key = PDO_STMT_OPTION_EMULATE_PREPARES; + break; - case SQLSRV_ATTR_FORMAT_DECIMALS: - option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; - break; + case SQLSRV_ATTR_FETCHES_NUMERIC_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; + break; - case SQLSRV_ATTR_DECIMAL_PLACES: - option_key = PDO_STMT_OPTION_DECIMAL_PLACES; - break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; + break; - default: - CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { - throw core::CoreException(); - } - break; + case SQLSRV_ATTR_FORMAT_DECIMALS: + option_key = PDO_STMT_OPTION_FORMAT_DECIMALS; + break; + + case SQLSRV_ATTR_DECIMAL_PLACES: + option_key = PDO_STMT_OPTION_DECIMAL_PLACES; + break; + + case SQLSRV_ATTR_DATA_CLASSIFICATION: + option_key = PDO_STMT_OPTION_DATA_CLASSIFICATION; + break; + + default: + CHECK_CUSTOM_ERROR(true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION) + { + throw core::CoreException(); + } + break; } // if a PDO handled option makes it through (such as PDO_ATTR_STATEMENT_CLASS, just skip it - if( option_key != -1 ) { - zval_add_ref( data ); - core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC ); + if (option_key != -1) { + zval_add_ref(data); + core::sqlsrv_zend_hash_index_update(ctx, options_ht, option_key, data TSRMLS_CC); } } @@ -1673,7 +1692,6 @@ void validate_stmt_options( _Inout_ sqlsrv_context& ctx, _Inout_ zval* stmt_opti ZEND_HASH_FOREACH_KEY_VAL( options_ht, int_key, key, data ) { int type = HASH_KEY_NON_EXISTENT; - int result = 0; type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; CHECK_CUSTOM_ERROR(( type != HASH_KEY_IS_LONG ), ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { throw core::CoreException(); diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index bee4cfc5..8f374093 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -3,7 +3,7 @@ // // Contents: initialization routines for PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -294,6 +294,7 @@ namespace { { "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE }, { "SQLSRV_ATTR_FORMAT_DECIMALS" , SQLSRV_ATTR_FORMAT_DECIMALS }, { "SQLSRV_ATTR_DECIMAL_PLACES" , SQLSRV_ATTR_DECIMAL_PLACES }, + { "SQLSRV_ATTR_DATA_CLASSIFICATION" , SQLSRV_ATTR_DATA_CLASSIFICATION }, // used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int, // PDO::PARAM_STR uses the size of the string in the variable diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index edd333e4..6a96ace2 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -5,7 +5,7 @@ // // Copyright Microsoft Corporation // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 773ad953..71c42690 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDOStatement object for the PDO_SQLSRV // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -220,7 +220,7 @@ void meta_data_free( _Inout_ field_meta_data* meta ) sqlsrv_free( meta ); } -zval convert_to_zval( _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) +zval convert_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) { zval out_zval; ZVAL_UNDEF(&out_zval); @@ -264,15 +264,8 @@ zval convert_to_zval( _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val break; } case SQLSRV_PHPTYPE_DATETIME: - if (*in_val == NULL) { - - ZVAL_NULL(&out_zval); - } - else { - - out_zval = *(reinterpret_cast(*in_val)); - sqlsrv_free(*in_val); - } + convert_datetime_string_to_zval(stmt, static_cast(*in_val), field_len, out_zval); + sqlsrv_free(*in_val); break; case SQLSRV_PHPTYPE_NULL: ZVAL_NULL(&out_zval); @@ -478,7 +471,6 @@ int pdo_sqlsrv_stmt_describe_col( _Inout_ pdo_stmt_t *stmt, _In_ int colno TSRML // Set the name column_data->name = zend_string_init( (const char*)core_meta_data->field_name.get(), core_meta_data->field_name_len, 0 ); - core_meta_data->field_name.reset(); // Set the maxlen column_data->maxlen = ( core_meta_data->field_precision > 0 ) ? core_meta_data->field_precision : core_meta_data->field_size; @@ -593,12 +585,26 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt TSRMLS_DC ) if ( execReturn == SQL_NO_DATA ) { stmt->column_count = 0; stmt->row_count = 0; + driver_stmt->column_count = 0; + driver_stmt->row_count = 0; } else { - stmt->column_count = core::SQLNumResultCols( driver_stmt TSRMLS_CC ); + if (driver_stmt->column_count == ACTIVE_NUM_COLS_INVALID) { + stmt->column_count = core::SQLNumResultCols( driver_stmt TSRMLS_CC ); + driver_stmt->column_count = stmt->column_count; + } + else { + stmt->column_count = driver_stmt->column_count; + } - // return the row count regardless if there are any rows or not - stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + if (driver_stmt->row_count == ACTIVE_NUM_ROWS_INVALID) { + // return the row count regardless if there are any rows or not + stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + driver_stmt->row_count = stmt->row_count; + } + else { + stmt->row_count = driver_stmt->row_count; + } } // workaround for a bug in the PDO driver manager. It is fairly simple to crash the PDO driver manager with @@ -692,11 +698,16 @@ int pdo_sqlsrv_stmt_fetch( _Inout_ pdo_stmt_t *stmt, _In_ enum pdo_fetch_orienta SQLSMALLINT odbc_fetch_ori = pdo_fetch_ori_to_odbc_fetch_ori( ori ); bool data = core_sqlsrv_fetch( driver_stmt, odbc_fetch_ori, offset TSRMLS_CC ); - // support for the PDO rowCount method. Since rowCount doesn't call a method, PDO relies on us to fill the - // pdo_stmt_t::row_count member - if( driver_stmt->past_fetch_end || driver_stmt->cursor_type != SQL_CURSOR_FORWARD_ONLY ) { + // support for the PDO rowCount method. Since rowCount doesn't call a + // method, PDO relies on us to fill the pdo_stmt_t::row_count member + // The if condition was changed from + // `driver_stmt->past_fetch_end || driver_stmt->cursor_type != SQL_CURSOR_FORWARD_ONLY` + // because it caused SQLRowCount to be called at each fetch if using a non-forward cursor + // which is unnecessary and a performance hit + if( driver_stmt->past_fetch_end || driver_stmt->cursor_type == SQL_CURSOR_DYNAMIC) { stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + driver_stmt->row_count = stmt->row_count; // a row_count of -1 means no rows, but we change it to 0 if( stmt->row_count == -1 ) { @@ -815,11 +826,11 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, core_sqlsrv_get_field( driver_stmt, colno, sqlsrv_php_type, false, *(reinterpret_cast(ptr)), reinterpret_cast( len ), true, &sqlsrv_phptype_out TSRMLS_CC ); - if ( ptr ) { - zval* zval_ptr = reinterpret_cast( sqlsrv_malloc( sizeof( zval ))); - *zval_ptr = convert_to_zval( sqlsrv_phptype_out, reinterpret_cast( ptr ), *len ); - *ptr = reinterpret_cast( zval_ptr ); - *len = sizeof( zval ); + if (ptr) { + zval* zval_ptr = reinterpret_cast(sqlsrv_malloc(sizeof(zval))); + *zval_ptr = convert_to_zval(driver_stmt, sqlsrv_phptype_out, reinterpret_cast(ptr), *len); + *ptr = reinterpret_cast(zval_ptr); + *len = sizeof(zval); } return 1; @@ -894,6 +905,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In core_sqlsrv_set_decimal_places(driver_stmt, val TSRMLS_CC); break; + case SQLSRV_ATTR_DATA_CLASSIFICATION: + driver_stmt->data_classification = (zend_is_true(val)) ? true : false; + break; + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -993,6 +1008,12 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; } + case SQLSRV_ATTR_DATA_CLASSIFICATION: + { + ZVAL_BOOL(return_value, driver_stmt->data_classification); + break; + } + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -1047,12 +1068,28 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno // initialize the array to nothing, as PDO requires us to create it core::sqlsrv_array_init( *driver_stmt, return_value TSRMLS_CC ); - sqlsrv_malloc_auto_ptr core_meta_data; - - core_meta_data = core_sqlsrv_field_metadata( driver_stmt, (SQLSMALLINT) colno TSRMLS_CC ); + field_meta_data* core_meta_data; + // metadata should have been saved earlier + SQLSRV_ASSERT(colno < driver_stmt->current_meta_data.size(), "pdo_sqlsrv_stmt_get_col_meta: Metadata vector out of sync with column numbers"); + core_meta_data = driver_stmt->current_meta_data[colno]; + // add the following fields: flags, native_type, driver:decl_type, table - add_assoc_long( return_value, "flags", 0 ); + if (driver_stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(driver_stmt); + + // initialize the column data classification array + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*driver_stmt, &data_classification TSRMLS_CC ); + + data_classification::fill_column_sensitivity_array(driver_stmt, (SQLSMALLINT)colno, &data_classification); + + add_assoc_zval(return_value, "flags", &data_classification); + } + else { + add_assoc_long(return_value, "flags", 0); + } // get the name of the data type char field_type_name[SQL_SERVER_IDENT_SIZE_MAX] = {'\0'}; @@ -1090,16 +1127,13 @@ int pdo_sqlsrv_stmt_get_col_meta( _Inout_ pdo_stmt_t *stmt, _In_ zend_long colno if( stmt->columns && stmt->columns[colno].param_type == PDO_PARAM_ZVAL ) { add_assoc_long( return_value, "pdo_type", pdo_type ); } - - // this will ensure that the field_name field, which is an auto pointer gets freed. - (*core_meta_data).~field_meta_data(); } catch( core::CoreException& ) { - + zval_ptr_dtor(return_value); return FAILURE; } catch(...) { - + zval_ptr_dtor(return_value); DIE( "pdo_sqlsrv_stmt_get_col_meta: Unknown exception occurred while retrieving metadata." ); } @@ -1146,6 +1180,9 @@ int pdo_sqlsrv_stmt_next_rowset( _Inout_ pdo_stmt_t *stmt TSRMLS_DC ) // return the row count regardless if there are any rows or not stmt->row_count = core::SQLRowCount( driver_stmt TSRMLS_CC ); + + driver_stmt->column_count = stmt->column_count; + driver_stmt->row_count = stmt->row_count; } catch( core::CoreException& ) { diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index e399fda1..6cfb43ac 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -3,7 +3,7 @@ // // Contents: Utility functions used by both connection or statement functions // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -449,6 +449,18 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -93, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -94, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -95, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -96, true} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 3e013f95..a6403219 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Declarations for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 4b50bbc7..6a616da0 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -6,7 +6,7 @@ // // Contents: Internal declarations for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -79,7 +79,8 @@ enum PDO_SQLSRV_ATTR { SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, SQLSRV_ATTR_FETCHES_DATETIME_TYPE, SQLSRV_ATTR_FORMAT_DECIMALS, - SQLSRV_ATTR_DECIMAL_PLACES + SQLSRV_ATTR_DECIMAL_PLACES, + SQLSRV_ATTR_DATA_CLASSIFICATION }; // valid set of values for TransactionIsolation connection option diff --git a/source/pdo_sqlsrv/template.rc b/source/pdo_sqlsrv/template.rc index fdbeaa57..2db6a2e9 100644 --- a/source/pdo_sqlsrv/template.rc +++ b/source/pdo_sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.cpp b/source/shared/FormattedPrint.cpp index b664159c..185489d7 100644 --- a/source/shared/FormattedPrint.cpp +++ b/source/shared/FormattedPrint.cpp @@ -6,7 +6,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.h b/source/shared/FormattedPrint.h index 907b1647..32f65ebb 100644 --- a/source/shared/FormattedPrint.h +++ b/source/shared/FormattedPrint.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.cpp b/source/shared/StringFunctions.cpp index 6aac5a05..ac0a8239 100644 --- a/source/shared/StringFunctions.cpp +++ b/source/shared/StringFunctions.cpp @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.h b/source/shared/StringFunctions.h index a2bfceaa..0eb011c3 100644 --- a/source/shared/StringFunctions.h +++ b/source/shared/StringFunctions.h @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 3d195fe9..cea5a3dd 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use connection handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -386,7 +386,7 @@ SQLRETURN core_odbc_connect( _Inout_ sqlsrv_conn* conn, _Inout_ std::string& con // We only support UTF-8 encoding for connection string. // Convert our UTF-8 connection string to UTF-16 before connecting with SQLDriverConnnectW - wconn_string = utf16_string_from_mbcs_string( SQLSRV_ENCODING_UTF8, conn_str.c_str(), static_cast( conn_str.length() ), &wconn_len ); + wconn_string = utf16_string_from_mbcs_string( SQLSRV_ENCODING_UTF8, conn_str.c_str(), static_cast( conn_str.length() ), &wconn_len, true ); CHECK_CUSTOM_ERROR( wconn_string == 0, conn, SQLSRV_ERROR_CONNECT_STRING_ENCODING_TRANSLATE, get_last_error_message()) { @@ -702,22 +702,32 @@ void core_sqlsrv_get_client_info( _Inout_ sqlsrv_conn* conn, _Out_ zval *client_ bool core_is_conn_opt_value_escaped( _Inout_ const char* value, _Inout_ size_t value_len ) { - // if the value is already quoted, then only analyse the part inside the quotes and return it as - // unquoted since we quote it when adding it to the connection string. - if( value_len > 0 && value[0] == '{' && value[value_len - 1] == '}' ) { - ++value; + if (value_len == 0) { + return true; + } + + if (value_len == 1) { + return (value[0] != '}'); + } + + const char *pstr = value; + if (value_len > 0 && value[0] == '{' && value[value_len - 1] == '}') { + pstr = ++value; value_len -= 2; } - // check to make sure that all right braces are escaped + + const char *pch = strchr(pstr, '}'); size_t i = 0; - while( ( value[i] != '}' || ( value[i] == '}' && value[i+1] == '}' )) && i < value_len ) { - // skip both braces - if( value[i] == '}' ) - ++i; - ++i; - } - if( i < value_len && value[i] == '}' ) { - return false; + + while (pch != NULL && i < value_len) { + i = pch - pstr + 1; + + if (i == value_len || (i < value_len && pstr[i] != '}')) { + return false; + } + + i++; // skip the brace + pch = strchr(pch + 2, '}'); // continue searching } return true; diff --git a/source/shared/core_init.cpp b/source/shared/core_init.cpp index 504a145b..63d08ce9 100644 --- a/source/shared/core_init.cpp +++ b/source/shared/core_init.cpp @@ -3,7 +3,7 @@ // // Contents: common initialization routines shared by PDO and sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index 93427bd7..1fbe005f 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -3,7 +3,7 @@ // // Contents: Result sets // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -243,14 +243,12 @@ std::string getUTF8StringFromString( _In_z_ const SQLWCHAR* source ) { // convert to regular character string first char c_str[4] = ""; - mbstate_t mbs; SQLLEN i = 0; std::string str; while ( source[i] ) { memset( c_str, 0, sizeof( c_str ) ); - DWORD rc; int cch = 0; errno_t err = mplat_wctomb_s( &cch, c_str, sizeof( c_str ), source[i++] ); if ( cch > 0 && err == ERROR_SUCCESS ) diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 886d5e82..a9e281c4 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Core routines and constants shared by the Microsoft Drivers for PHP for SQL Server // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -170,7 +170,6 @@ OACR_WARNING_POP // constants for maximums in SQL Server const int SS_MAXCOLNAMELEN = 128; const int SQL_SERVER_MAX_FIELD_SIZE = 8000; -const int SQL_SERVER_MAX_PRECISION = 38; const int SQL_SERVER_MAX_TYPE_SIZE = 0; const int SQL_SERVER_MAX_PARAMS = 2100; const int SQL_SERVER_MAX_MONEY_SCALE = 4; @@ -182,6 +181,11 @@ const int SQL_MAX_ERROR_MESSAGE_LENGTH = SQL_MAX_MESSAGE_LENGTH * 2; // max size of a date time string when converting from a DateTime object to a string const int MAX_DATETIME_STRING_LEN = 256; +// identifier for whether or not we have obtained the number of rows and columns +// of a result +const short ACTIVE_NUM_COLS_INVALID = -99; +const long ACTIVE_NUM_ROWS_INVALID = -99; + // precision and scale for the date time types between servers const int SQL_SERVER_2005_DEFAULT_DATETIME_PRECISION = 23; const int SQL_SERVER_2005_DEFAULT_DATETIME_SCALE = 3; @@ -993,8 +997,6 @@ class sqlsrv_context { SQLSRV_ENCODING encoding_; // encoding of the context }; -const int SQLSRV_OS_VISTA_OR_LATER = 6; // major version for Vista - // maps an IANA encoding to a code page struct sqlsrv_encoding { @@ -1115,6 +1117,7 @@ enum SQLSRV_STMT_OPTIONS { SQLSRV_STMT_OPTION_DATE_AS_STRING, SQLSRV_STMT_OPTION_FORMAT_DECIMALS, SQLSRV_STMT_OPTION_DECIMAL_PLACES, + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, // Driver specific connection options SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000, @@ -1131,6 +1134,7 @@ const char Authentication[] = "Authentication"; const char Driver[] = "Driver"; const char CharacterSet[] = "CharacterSet"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; @@ -1163,6 +1167,7 @@ enum SQLSRV_CONN_OPTIONS { SQLSRV_CONN_OPTION_ACCESS_TOKEN, SQLSRV_CONN_OPTION_CHARACTERSET, SQLSRV_CONN_OPTION_CONN_POOLING, + SQLSRV_CONN_OPTION_LANGUAGE, SQLSRV_CONN_OPTION_DATABASE, SQLSRV_CONN_OPTION_ENCRYPT, SQLSRV_CONN_OPTION_FAILOVER_PARTNER, @@ -1314,6 +1319,11 @@ struct stmt_option_decimal_places : public stmt_option_functor { virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); }; +struct stmt_option_data_classification : public stmt_option_functor { + + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC ); +}; + // used to hold the table for statment options struct stmt_option { @@ -1417,6 +1427,75 @@ struct sqlsrv_output_param { } }; +namespace data_classification { + // *** data classficiation metadata structures and helper methods -- to store and/or process the sensitivity classification data *** + struct name_id_pair; + struct sensitivity_metadata; + + void name_id_pair_free(name_id_pair * pair); + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>* pairs, _Inout_ unsigned char **pptr TSRMLS_CC); + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr); + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *column_data TSRMLS_CC); + + struct name_id_pair { + UCHAR name_len; + sqlsrv_malloc_auto_ptr name; + UCHAR id_len; + sqlsrv_malloc_auto_ptr id; + + name_id_pair() : name_len(0), id_len(0) + { + } + + ~name_id_pair() + { + } + }; + + struct label_infotype_pair { + USHORT label_idx; + USHORT infotype_idx; + + label_infotype_pair() : label_idx(0), infotype_idx(0) + { + } + }; + + struct column_sensitivity { + USHORT num_pairs; + std::vector label_info_pairs; + + column_sensitivity() : num_pairs(0) + { + } + + ~column_sensitivity() + { + label_info_pairs.clear(); + } + }; + + struct sensitivity_metadata { + USHORT num_labels; + std::vector> labels; + USHORT num_infotypes; + std::vector> infotypes; + USHORT num_columns; + std::vector columns_sensitivity; + + sensitivity_metadata() : num_labels(0), num_infotypes(0), num_columns(0) + { + } + + ~sensitivity_metadata() + { + reset(); + } + + void reset(); + }; +} // namespace data_classification + // forward decls struct sqlsrv_result_set; struct field_meta_data; @@ -1427,6 +1506,9 @@ struct sqlsrv_stmt : public sqlsrv_context { void free_param_data( TSRMLS_D ); virtual void new_result_set( TSRMLS_D ); + // free sensitivity classification metadata + void clean_up_sensitivity_metadata(); + sqlsrv_conn* conn; // Connection that created this statement bool executed; // Whether the statement has been executed yet (used for error messages) @@ -1436,13 +1518,15 @@ struct sqlsrv_stmt : public sqlsrv_context { bool has_rows; // Has_rows is set if there are actual rows in the row set bool fetch_called; // Used by core_sqlsrv_get_field to return an informative error if fetch not yet called int last_field_index; // last field retrieved by core_sqlsrv_get_field - bool past_next_result_end; // core_sqlsrv_next_result sets this to true when the statement goes beyond the - // last results + bool past_next_result_end; // core_sqlsrv_next_result sets this to true when the statement goes beyond the last results + short column_count; // Number of columns in the current result set obtained from SQLNumResultCols + long row_count; // Number of rows in the current result set obtained from SQLRowCount unsigned long query_timeout; // maximum allowed statement execution time zend_long buffered_query_limit; // maximum allowed memory for a buffered query (measured in KB) bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings bool format_decimals; // false by default but the user can set this to true to add the missing leading zeroes and/or control number of decimal digits to show short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits) + bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata // holds output pointers for SQLBindParameter // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving @@ -1465,6 +1549,9 @@ struct sqlsrv_stmt : public sqlsrv_context { // meta data for current result set std::vector> current_meta_data; + // meta data for data classification + sqlsrv_malloc_auto_ptr current_sensitivity_metadata; + sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_opt_ void* drv TSRMLS_DC ); virtual ~sqlsrv_stmt( void ); @@ -1536,6 +1623,7 @@ bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ); void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLEN limit TSRMLS_DC ); void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC); +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); //********************************************************************************************************************************* // Result Set @@ -1717,7 +1805,9 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { bool convert_string_from_utf16_inplace( _In_ SQLSRV_ENCODING encoding, _Inout_updates_z_(len) char** string, _Inout_ SQLLEN& len); bool validate_string( _In_ char* string, _In_ SQLLEN& len); bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(cchInLen) const SQLWCHAR* inString, _In_ SQLINTEGER cchInLen, _Inout_updates_bytes_(cchOutLen) char** outString, _Out_ SQLLEN& cchOutLen ); -SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); +SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len, bool use_strict_conversion = false ); + +void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* input, _In_ SQLLEN length, _Inout_ zval& out_zval); //********************************************************************************************************************************* // Error handling routines and Predefined Errors @@ -1779,6 +1869,9 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_EMPTY_ACCESS_TOKEN, SQLSRV_ERROR_INVALID_DECIMAL_PLACES, SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, @@ -2402,7 +2495,7 @@ namespace core { inline void sqlsrv_add_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zend_ulong index, _In_ zval* value TSRMLS_DC) { - int zr = ::add_index_zval( array, index, value ); + int zr = add_index_zval( array, index, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { throw CoreException(); } @@ -2410,7 +2503,7 @@ namespace core { inline void sqlsrv_add_next_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zval* value TSRMLS_DC) { - int zr = ::add_next_index_zval( array, value ); + int zr = add_next_index_zval( array, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { throw CoreException(); } @@ -2443,6 +2536,14 @@ namespace core { } } + inline void sqlsrv_add_assoc_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array_z, _In_ const char* key, _In_ zval* val TSRMLS_DC ) + { + int zr = ::add_assoc_zval(array_z, key, val); + CHECK_ZEND_ERROR (zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { + throw CoreException(); + } + } + inline void sqlsrv_array_init( _Inout_ sqlsrv_context& ctx, _Out_ zval* new_array TSRMLS_DC) { #if PHP_VERSION_ID < 70300 @@ -2520,7 +2621,7 @@ namespace core { } } - inline void sqlsrv_zend_hash_next_index_insert_mem( _Inout_ sqlsrv_context& ctx, _In_ HashTable* ht, _In_reads_bytes_(data_size) void* data, _In_ uint data_size TSRMLS_DC) + inline void sqlsrv_zend_hash_next_index_insert_mem( _Inout_ sqlsrv_context& ctx, _In_ HashTable* ht, _In_reads_bytes_(data_size) void* data, _In_ size_t data_size TSRMLS_DC) { int zr = (data = ::zend_hash_next_index_insert_mem(ht, data, data_size)) != NULL ? SUCCESS : FAILURE; CHECK_ZEND_ERROR(zr, ctx, SQLSRV_ERROR_ZEND_HASH) { diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 9cac96a5..13bb2e5e 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use statement handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -140,10 +140,13 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error fetch_called( false ), last_field_index( -1 ), past_next_result_end( false ), + column_count( ACTIVE_NUM_COLS_INVALID ), + row_count( ACTIVE_NUM_ROWS_INVALID ), query_timeout( QUERY_TIMEOUT_INVALID ), date_as_string(false), format_decimals(false), // no formatting needed decimal_places(NO_CHANGE_DECIMAL_PLACES), // the default is no formatting to resultset required + data_classification(false), buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ), param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte send_streams_at_exec( true ), @@ -189,6 +192,9 @@ sqlsrv_stmt::~sqlsrv_stmt( void ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + invalidate(); zval_ptr_dtor( ¶m_input_strings ); zval_ptr_dtor( &output_params ); @@ -225,6 +231,8 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) this->past_next_result_end = false; this->past_fetch_end = false; this->last_field_index = -1; + this->column_count = ACTIVE_NUM_COLS_INVALID; + this->row_count = ACTIVE_NUM_ROWS_INVALID; // delete any current results if( current_results ) { @@ -233,6 +241,9 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) current_results = NULL; } + // delete sensivity data + clean_up_sensitivity_metadata(); + // create a new result set if( cursor_type == SQLSRV_CURSOR_BUFFERED ) { sqlsrv_malloc_auto_ptr result; @@ -246,6 +257,15 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) } } +// free sensitivity classification metadata +void sqlsrv_stmt::clean_up_sensitivity_metadata() +{ + if (current_sensitivity_metadata) { + current_sensitivity_metadata->~sensitivity_metadata(); + current_sensitivity_metadata.reset(); + } +} + // core_sqlsrv_create_stmt // Common code to allocate a statement from either driver. Returns a valid driver statement object or // throws an exception if an error occurs. @@ -819,9 +839,17 @@ bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orient CHECK_CUSTOM_ERROR( stmt->past_fetch_end, stmt, SQLSRV_ERROR_FETCH_PAST_END ) { throw core::CoreException(); } + // First time only if ( !stmt->fetch_called ) { - SQLSMALLINT has_fields = core::SQLNumResultCols( stmt TSRMLS_CC ); + SQLSMALLINT has_fields; + if (stmt->column_count != ACTIVE_NUM_COLS_INVALID) { + has_fields = stmt->column_count; + } else { + has_fields = core::SQLNumResultCols( stmt TSRMLS_CC ); + stmt->column_count = has_fields; + } + CHECK_CUSTOM_ERROR( has_fields == 0, stmt, SQLSRV_ERROR_NO_FIELDS ) { throw core::CoreException(); } @@ -939,7 +967,7 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL } } - // Set the field name lenth + // Set the field name length meta_data->field_name_len = static_cast( field_name_len ); field_meta_data* result_field_meta_data = meta_data; @@ -947,6 +975,101 @@ field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQL return result_field_meta_data; } +void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) +{ + sqlsrv_malloc_auto_ptr dcbuf; + SQLINTEGER dclen = 0; + SQLINTEGER dclenout = 0; + SQLHANDLE ird; + SQLRETURN r; + + try { + if (!stmt->data_classification) { + return; + } + + if (stmt->current_sensitivity_metadata) { + // Already cached, so return + return; + } + + CHECK_CUSTOM_ERROR(!stmt->executed, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION) { + throw core::CoreException(); + } + + // Reference: https://docs.microsoft.com/sql/connect/odbc/data-classification + // To retrieve sensitivity classfication data, the first step is to retrieve the IRD(Implementation Row Descriptor) handle by + // calling SQLGetStmtAttr with SQL_ATTR_IMP_ROW_DESC statement attribute + r = ::SQLGetStmtAttr(stmt->handle(), SQL_ATTR_IMP_ROW_DESC, (SQLPOINTER)&ird, SQL_IS_POINTER, 0); + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in getting Implementation Row Descriptor handle." ); + throw core::CoreException(); + } + + // First call to get dclen + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, 0, &dclen); + if (r != SQL_SUCCESS || dclen == 0) { + // log the error first + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW first time." ); + + // If this fails, check if it is the "Invalid Descriptor Field error" + SQLRETURN rc; + SQLCHAR state[SQL_SQLSTATE_BUFSIZE] = {'\0'}; + SQLSMALLINT len; + rc = ::SQLGetDiagField(SQL_HANDLE_DESC, ird, 1, SQL_DIAG_SQLSTATE, state, SQL_SQLSTATE_BUFSIZE, &len TSRMLS_CC); + + CHECK_SQL_ERROR_OR_WARNING(rc, stmt) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(!strcmp("HY091", reinterpret_cast(state)), stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE) { + throw core::CoreException(); + } + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Check if ODBC driver or the server supports the Data Classification feature.") { + throw core::CoreException(); + } + } + + // Call again to read SQL_CA_SS_DATA_CLASSIFICATION data + dcbuf = static_cast(sqlsrv_malloc(dclen * sizeof(char))); + + r = ::SQLGetDescFieldW(ird, 0, SQL_CA_SS_DATA_CLASSIFICATION, dcbuf, dclen, &dclenout); + if (r != SQL_SUCCESS) { + LOG(SEV_ERROR, "core_sqlsrv_sensitivity_metadata: failed in calling SQLGetDescFieldW again." ); + + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "SQLGetDescFieldW failed unexpectedly") { + throw core::CoreException(); + } + } + + // Start parsing the data (blob) + using namespace data_classification; + unsigned char *dcptr = dcbuf; + + sqlsrv_malloc_auto_ptr sensitivity_meta; + sensitivity_meta = new (sqlsrv_malloc(sizeof(sensitivity_metadata))) sensitivity_metadata(); + + // Parse the name id pairs for labels first then info types + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_labels, &sensitivity_meta->labels, &dcptr); + parse_sensitivity_name_id_pairs(stmt, sensitivity_meta->num_infotypes, &sensitivity_meta->infotypes, &dcptr); + + // Next parse the sensitivity properties + parse_column_sensitivity_props(sensitivity_meta, &dcptr); + + unsigned char *dcend = dcbuf; + dcend += dclen; + + CHECK_CUSTOM_ERROR(dcptr != dcend, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Metadata parsing ends unexpectedly") { + throw core::CoreException(); + } + + stmt->current_sensitivity_metadata = sensitivity_meta; + sensitivity_meta.transferred(); + } catch (core::CoreException& e) { + throw e; + } +} // core_sqlsrv_get_field // Return the value of a column from ODBC @@ -1066,10 +1189,27 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) { - // Use SQLNumResultCols to determine if we have rows or not. - SQLSMALLINT num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); - // use SQLRowCount to determine if there is a rows status waiting - SQLLEN rows_affected = core::SQLRowCount( stmt TSRMLS_CC ); + SQLSMALLINT num_cols; + SQLLEN rows_affected; + + if (stmt->column_count != ACTIVE_NUM_COLS_INVALID) { + num_cols = stmt->column_count; + } + else { + // Use SQLNumResultCols to determine if we have rows or not + num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); + stmt->column_count = num_cols; + } + + if (stmt->row_count != ACTIVE_NUM_ROWS_INVALID) { + rows_affected = stmt->row_count; + } + else { + // Use SQLRowCount to determine if there is a rows status waiting + rows_affected = core::SQLRowCount( stmt TSRMLS_CC ); + stmt->row_count = rows_affected; + } + return (num_cols != 0) || (rows_affected > 0); } @@ -1475,6 +1615,16 @@ void stmt_option_decimal_places:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op core_sqlsrv_set_decimal_places(stmt, value_z TSRMLS_CC); } +void stmt_option_data_classification:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC ) +{ + if (zend_is_true(value_z)) { + stmt->data_classification = true; + } + else { + stmt->data_classification = false; + } +} + // internal function to release the active stream. Called by each main API function // that will alter the statement and cancel any retrieval of data from a stream. void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) @@ -1717,54 +1867,36 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i break; } - // get the date as a string (http://msdn2.microsoft.com/en-us/library/ms712387(VS.85).aspx) and - // convert it to a DateTime object and return the created object + // Reference: https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-timestamp + // Retrieve the datetime data as a string, which may be cached for later use. + // The string is converted to a DateTime object only when it is required to + // be returned as a zval. case SQLSRV_PHPTYPE_DATETIME: { - char field_value_temp[MAX_DATETIME_STRING_LEN] = {'\0'}; - zval params[1]; - zval field_value_temp_z; - zval function_z; + sqlsrv_malloc_auto_ptr field_value_temp; + SQLLEN field_len_temp = 0; - ZVAL_UNDEF( &field_value_temp_z ); - ZVAL_UNDEF( &function_z ); - ZVAL_UNDEF( params ); + field_value_temp = static_cast(sqlsrv_malloc(MAX_DATETIME_STRING_LEN)); + memset(field_value_temp, '\0', MAX_DATETIME_STRING_LEN); - SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_CHAR, field_value_temp, - MAX_DATETIME_STRING_LEN, field_len, true TSRMLS_CC ); + SQLRETURN r = stmt->current_results->get_data(field_index + 1, SQL_C_CHAR, field_value_temp, MAX_DATETIME_STRING_LEN, &field_len_temp, true TSRMLS_CC); - CHECK_CUSTOM_ERROR(( r == SQL_NO_DATA ), stmt, SQLSRV_ERROR_NO_DATA, field_index ) { + if (r == SQL_NO_DATA || field_len_temp == SQL_NULL_DATA) { + field_value_temp.reset(); + field_len_temp = 0; + } + + CHECK_CUSTOM_ERROR((r == SQL_NO_DATA), stmt, SQLSRV_ERROR_NO_DATA, field_index) { throw core::CoreException(); } - zval_auto_ptr return_value_z; - return_value_z = ( zval * )sqlsrv_malloc( sizeof( zval )); - ZVAL_UNDEF( return_value_z ); + field_value = field_value_temp; + field_value_temp.transferred(); + *field_len = field_len_temp; - if( *field_len == SQL_NULL_DATA ) { - ZVAL_NULL( return_value_z ); - field_value = reinterpret_cast( return_value_z.get()); - return_value_z.transferred(); - break; - } - - // Convert the string date to a DateTime object - core::sqlsrv_zval_stringl( &field_value_temp_z, field_value_temp, *field_len ); - core::sqlsrv_zval_stringl( &function_z, "date_create", sizeof("date_create") - 1 ); - params[0] = field_value_temp_z; - - if( call_user_function( EG( function_table ), NULL, &function_z, return_value_z, 1, - params TSRMLS_CC ) == FAILURE) { - THROW_CORE_ERROR(stmt, SQLSRV_ERROR_DATETIME_CONVERSION_FAILED); - } - - field_value = reinterpret_cast( return_value_z.get()); - return_value_z.transferred(); - zend_string_free( Z_STR( field_value_temp_z )); - zend_string_free( Z_STR( function_z )); break; } - + // create a stream wrapper around the field and return that object to the PHP script. calls to fread // on the stream will result in calls to SQLGetData. This is handled in stream.cpp. See that file // for how these fields are used. @@ -2288,142 +2420,151 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) { - if( Z_ISUNDEF(stmt->output_params) ) + if (Z_ISUNDEF(stmt->output_params)) return; - HashTable* params_ht = Z_ARRVAL( stmt->output_params ); + HashTable* params_ht = Z_ARRVAL(stmt->output_params); zend_ulong index = -1; zend_string* key = NULL; void* output_param_temp = NULL; - ZEND_HASH_FOREACH_KEY_PTR( params_ht, index, key, output_param_temp ) { - sqlsrv_output_param* output_param = static_cast( output_param_temp ); - zval* value_z = Z_REFVAL_P( output_param->param_z ); - switch( Z_TYPE_P( value_z )) { - case IS_STRING: + try { + ZEND_HASH_FOREACH_KEY_PTR(params_ht, index, key, output_param_temp) { - // adjust the length of the string to the value returned by SQLBindParameter in the ind_ptr parameter - char* str = Z_STRVAL_P( value_z ); - SQLLEN str_len = stmt->param_ind_ptrs[output_param->param_num]; - if( str_len == 0 ) { - core::sqlsrv_zval_stringl( value_z, "", 0 ); - continue; - } - if( str_len == SQL_NULL_DATA ) { - zend_string_release( Z_STR_P( value_z )); - ZVAL_NULL( value_z ); - continue; - } - - // if there was more to output than buffer size to hold it, then throw a truncation error - int null_size = 0; - switch( output_param->encoding ) { - case SQLSRV_ENCODING_UTF8: - null_size = sizeof( SQLWCHAR ); // string isn't yet converted to UTF-8, still UTF-16 - break; - case SQLSRV_ENCODING_SYSTEM: - null_size = 1; - break; - case SQLSRV_ENCODING_BINARY: - null_size = 0; - break; - default: - SQLSRV_ASSERT( false, "Invalid encoding in output_param structure." ); - break; - } - CHECK_CUSTOM_ERROR( str_len > ( output_param->original_buffer_len - null_size ), stmt, - SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, output_param->param_num + 1 ) { - throw core::CoreException(); - } - - // For ODBC 11+ see https://msdn.microsoft.com/en-us/library/jj219209.aspx - // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains up to - // output_param->original_buffer_len data and is NULL terminated. - // The IF statement can be true when using connection pooling with unixODBC 2.3.4. - if ( str_len == SQL_NO_TOTAL ) + sqlsrv_output_param* output_param = static_cast(output_param_temp); + zval* value_z = Z_REFVAL_P(output_param->param_z); + switch (Z_TYPE_P(value_z)) { + case IS_STRING: { - str_len = output_param->original_buffer_len - null_size; - } - - if (output_param->encoding == SQLSRV_ENCODING_BINARY) { - // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated - // so we do that here if the length of the returned data is less than the original allocation. The - // original allocation null terminates the buffer already. - if (str_len < output_param->original_buffer_len) { - str[str_len] = '\0'; + // adjust the length of the string to the value returned by SQLBindParameter in the ind_ptr parameter + char* str = Z_STRVAL_P(value_z); + SQLLEN str_len = stmt->param_ind_ptrs[output_param->param_num]; + if (str_len == 0) { + core::sqlsrv_zval_stringl(value_z, "", 0); + continue; } - core::sqlsrv_zval_stringl(value_z, str, str_len); - } - else { - param_meta_data metaData = output_param->getMetaData(); - - if (output_param->encoding != SQLSRV_ENCODING_CHAR) { - char* outString = NULL; - SQLLEN outLen = 0; - bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen ); - CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) { - throw core::CoreException(); - } - - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, outString, &outLen); - } - - core::sqlsrv_zval_stringl(value_z, outString, outLen); - sqlsrv_free(outString); + if (str_len == SQL_NULL_DATA) { + zend_string_release(Z_STR_P(value_z)); + ZVAL_NULL(value_z); + continue; } - else { - if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { - format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len); - } + // if there was more to output than buffer size to hold it, then throw a truncation error + int null_size = 0; + switch (output_param->encoding) { + case SQLSRV_ENCODING_UTF8: + null_size = sizeof(SQLWCHAR); // string isn't yet converted to UTF-8, still UTF-16 + break; + case SQLSRV_ENCODING_SYSTEM: + null_size = 1; + break; + case SQLSRV_ENCODING_BINARY: + null_size = 0; + break; + default: + SQLSRV_ASSERT(false, "Invalid encoding in output_param structure."); + break; + } + CHECK_CUSTOM_ERROR(str_len > (output_param->original_buffer_len - null_size), stmt, + SQLSRV_ERROR_OUTPUT_PARAM_TRUNCATED, output_param->param_num + 1) + { + throw core::CoreException(); + } + + // For ODBC 11+ see https://msdn.microsoft.com/en-us/library/jj219209.aspx + // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains up to + // output_param->original_buffer_len data and is NULL terminated. + // The IF statement can be true when using connection pooling with unixODBC 2.3.4. + if (str_len == SQL_NO_TOTAL) { + str_len = output_param->original_buffer_len - null_size; + } + + if (output_param->encoding == SQLSRV_ENCODING_BINARY) { + // ODBC doesn't null terminate binary encodings, but PHP complains if a string isn't null terminated + // so we do that here if the length of the returned data is less than the original allocation. The + // original allocation null terminates the buffer already. + if (str_len < output_param->original_buffer_len) { + str[str_len] = '\0'; + } core::sqlsrv_zval_stringl(value_z, str, str_len); } - } - } - break; - case IS_LONG: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if( stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA ) { - ZVAL_NULL( value_z ); - } - else if( output_param->is_bool ) { - convert_to_boolean( value_z ); - } - else { - ZVAL_LONG( value_z, static_cast( Z_LVAL_P( value_z ))); - } - break; - case IS_DOUBLE: - // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so - if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { - ZVAL_NULL(value_z); - } - else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) { - // first check if its value is out of range - double dval = Z_DVAL_P(value_z); - if (dval > INT_MAX || dval < INT_MIN) { - CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) { - throw core::CoreException(); + else { + param_meta_data metaData = output_param->getMetaData(); + + if (output_param->encoding != SQLSRV_ENCODING_CHAR) { + char* outString = NULL; + SQLLEN outLen = 0; + bool result = convert_string_from_utf16(output_param->encoding, reinterpret_cast(str), int(str_len / sizeof(SQLWCHAR)), &outString, outLen); + CHECK_CUSTOM_ERROR(!result, stmt, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, get_last_error_message()) + { + throw core::CoreException(); + } + + if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, outString, &outLen); + } + + core::sqlsrv_zval_stringl(value_z, outString, outLen); + sqlsrv_free(outString); + } + else { + if (stmt->format_decimals && (metaData.sql_type == SQL_DECIMAL || metaData.sql_type == SQL_NUMERIC)) { + format_decimal_numbers(NO_CHANGE_DECIMAL_PLACES, metaData.decimal_digits, str, &str_len); + } + + core::sqlsrv_zval_stringl(value_z, str, str_len); } } - // if the output param is a boolean, still convert to - // a long integer first to take care of rounding - convert_to_long(value_z); - if (output_param->is_bool) { - convert_to_boolean(value_z); - } } break; - default: - DIE( "Illegal or unknown output parameter type. This should have been caught in core_sqlsrv_bind_parameter." ); - break; - } - value_z = NULL; - } ZEND_HASH_FOREACH_END(); - + case IS_LONG: + // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so + if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } + else if (output_param->is_bool) { + convert_to_boolean(value_z); + } + else { + ZVAL_LONG(value_z, static_cast(Z_LVAL_P(value_z))); + } + break; + case IS_DOUBLE: + // for a long or a float, simply check if NULL was returned and set the parameter to a PHP null if so + if (stmt->param_ind_ptrs[output_param->param_num] == SQL_NULL_DATA) { + ZVAL_NULL(value_z); + } + else if (output_param->php_out_type == SQLSRV_PHPTYPE_INT) { + // first check if its value is out of range + double dval = Z_DVAL_P(value_z); + if (dval > INT_MAX || dval < INT_MIN) { + CHECK_CUSTOM_ERROR(true, stmt, SQLSRV_ERROR_DOUBLE_CONVERSION_FAILED) + { + throw core::CoreException(); + } + } + // if the output param is a boolean, still convert to + // a long integer first to take care of rounding + convert_to_long(value_z); + if (output_param->is_bool) { + convert_to_boolean(value_z); + } + } + break; + default: + DIE("Illegal or unknown output parameter type. This should have been caught in core_sqlsrv_bind_parameter."); + break; + } + value_z = NULL; + } ZEND_HASH_FOREACH_END(); + } + catch (core::CoreException&) { + // empty the hash table due to exception caught + zend_hash_clean(Z_ARRVAL(stmt->output_params)); + throw; + } // empty the hash table since it's been processed - zend_hash_clean( Z_ARRVAL( stmt->output_params )); + zend_hash_clean(Z_ARRVAL(stmt->output_params)); return; } diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index d822d4a8..9ca8e8c0 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -3,7 +3,7 @@ // // Contents: Implementation of PHP streams for reading SQL Server data // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -44,7 +44,11 @@ int sqlsrv_stream_close( _Inout_ php_stream* stream, int /*close_handle*/ TSRMLS // read from a sqlsrv stream into the buffer provided by Zend. The parameters for binary vs. char are // set when sqlsrv_get_field is called by the user specifying which field type they want. -size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC ) +#if PHP_VERSION_ID >= 70400 +ssize_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC) +#else +size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count) char* buf, _Inout_ size_t count TSRMLS_DC) +#endif { SQLLEN read = 0; SQLSMALLINT c_type = SQL_C_CHAR; @@ -89,7 +93,8 @@ size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) break; } - SQLRETURN r = SQLGetData( ss->stmt->handle(), ss->field_index + 1, c_type, get_data_buffer, count /*BufferLength*/, &read ); + // Warnings will be handled below + SQLRETURN r = ss->stmt->current_results->get_data(ss->field_index + 1, c_type, get_data_buffer, count /*BufferLength*/, &read, false /*handle_warning*/ TSRMLS_CC); CHECK_SQL_ERROR( r, ss->stmt ) { stream->eof = 1; @@ -183,15 +188,20 @@ size_t sqlsrv_stream_read( _Inout_ php_stream* stream, _Out_writes_bytes_(count) return static_cast( read ); } - - catch( core::CoreException& ) { - + catch (core::CoreException&) { +#if PHP_VERSION_ID >= 70400 + return -1; +#else return 0; +#endif } - catch( ... ) { - - LOG( SEV_ERROR, "sqlsrv_stream_read: Unknown exception caught." ); + catch (...) { + LOG(SEV_ERROR, "sqlsrv_stream_read: Unknown exception caught."); +#if PHP_VERSION_ID >= 70400 + return -1; +#else return 0; +#endif } } diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 515eb38b..9f2e0eea 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -34,7 +34,7 @@ char last_err_msg[2048] = {'\0'}; // 2k to hold the error messages unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, - _In_ unsigned int utf16_len ); + _In_ unsigned int utf16_len, bool use_strict_conversion = false ); } // SQLSTATE for all internal errors @@ -172,11 +172,11 @@ bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_( // allocation of the destination string. An empty string passed in returns // failure since it's a failure case for convert_string_from_default_encoding. SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, _In_ unsigned int mbcs_len, - _Out_ unsigned int* utf16_len ) + _Out_ unsigned int* utf16_len, bool use_strict_conversion ) { *utf16_len = (mbcs_len + 1); SQLWCHAR* utf16_string = reinterpret_cast( sqlsrv_malloc( *utf16_len * sizeof( SQLWCHAR ))); - *utf16_len = convert_string_from_default_encoding( php_encoding, mbcs_string, mbcs_len, utf16_string, *utf16_len ); + *utf16_len = convert_string_from_default_encoding( php_encoding, mbcs_string, mbcs_len, utf16_string, *utf16_len, use_strict_conversion ); if( *utf16_len == 0 ) { // we preserve the error and reset it because sqlsrv_free resets the last error @@ -189,6 +189,40 @@ SQLWCHAR* utf16_string_from_mbcs_string( _In_ SQLSRV_ENCODING php_encoding, _In_ return utf16_string; } +// Converts an input (assuming a datetime string) to a zval containing a PHP DateTime object. +// If the input is null, this simply returns a NULL zval. If anything wrong occurs during conversion, +// an exception will be thrown. +void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* input, _In_ SQLLEN length, _Inout_ zval& out_zval) +{ + if (input == NULL) { + ZVAL_NULL(&out_zval); + return; + } + + zval params[1]; + zval value_temp_z; + zval function_z; + + // Initialize all zval variables + ZVAL_UNDEF(&out_zval); + ZVAL_UNDEF(&value_temp_z); + ZVAL_UNDEF(&function_z); + ZVAL_UNDEF(params); + + // Convert the datetime string to a PHP DateTime object + core::sqlsrv_zval_stringl(&value_temp_z, input, length); + core::sqlsrv_zval_stringl(&function_z, "date_create", sizeof("date_create") - 1); + params[0] = value_temp_z; + + if (call_user_function(EG(function_table), NULL, &function_z, &out_zval, 1, + params TSRMLS_CC) == FAILURE) { + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_DATETIME_CONVERSION_FAILED); + } + + zend_string_free(Z_STR(value_temp_z)); + zend_string_free(Z_STR(function_z)); +} + // call to retrieve an error from ODBC. This uses SQLGetDiagRec, so the // errno is 1 based. It returns it as an array with 3 members: // 1/SQLSTATE) sqlstate @@ -384,7 +418,7 @@ namespace { // to convert. unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, - _In_ unsigned int utf16_len ) + _In_ unsigned int utf16_len, bool use_strict_conversion ) { unsigned int win_encoding = CP_ACP; switch( php_encoding ) { @@ -399,8 +433,14 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin win_encoding = php_encoding; break; } -#ifndef _WIN32 - unsigned int required_len = SystemLocale::ToUtf16( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); +#ifndef _WIN32 + unsigned int required_len; + if (use_strict_conversion) { + required_len = SystemLocale::ToUtf16Strict( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); + } + else { + required_len = SystemLocale::ToUtf16( win_encoding, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); + } #else unsigned int required_len = MultiByteToWideChar( win_encoding, MB_ERR_INVALID_CHARS, mbcs_in_string, mbcs_len, utf16_out_string, utf16_len ); #endif // !_Win32 @@ -414,3 +454,200 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin } } + + +namespace data_classification { + const char* DATA_CLASS = "Data Classification"; + const char* LABEL = "Label"; + const char* INFOTYPE = "Information Type"; + const char* NAME = "name"; + const char* ID = "id"; + + void convert_sensivity_field(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_ENCODING encoding, _In_ unsigned char *ptr, _In_ int len, _Inout_updates_bytes_(cchOutLen) char** field_name) + { + sqlsrv_malloc_auto_ptr temp_field_name; + int temp_field_len = len * sizeof(SQLWCHAR); + SQLLEN field_name_len = 0; + + if (len == 0) { + *field_name = reinterpret_cast(sqlsrv_malloc(1)); + *field_name[0] = '\0'; + return; + } + + temp_field_name = static_cast(sqlsrv_malloc((len + 1) * sizeof(SQLWCHAR))); + memset(temp_field_name, L'\0', len + 1); + memcpy_s(temp_field_name, temp_field_len, ptr, temp_field_len); + + bool converted = convert_string_from_utf16(encoding, temp_field_name, len, field_name, field_name_len); + + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message()) { + throw core::CoreException(); + } + } + + void name_id_pair_free(_Inout_ name_id_pair* pair) + { + if (pair->name) { + pair->name.reset(); + } + if (pair->id) { + pair->id.reset(); + } + sqlsrv_free(pair); + } + + void parse_sensitivity_name_id_pairs(_Inout_ sqlsrv_stmt* stmt, _Inout_ USHORT& numpairs, _Inout_ std::vector>* pairs, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short npairs; + numpairs = npairs = *(reinterpret_cast(ptr)); + SQLSRV_ENCODING encoding = ((stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); + + pairs->reserve(numpairs); + + ptr += sizeof(unsigned short); + while (npairs--) { + int namelen, idlen; + unsigned char *nameptr, *idptr; + + sqlsrv_malloc_auto_ptr pair; + pair = new(sqlsrv_malloc(sizeof(name_id_pair))) name_id_pair(); + + sqlsrv_malloc_auto_ptr name; + sqlsrv_malloc_auto_ptr id; + + namelen = *ptr++; + nameptr = ptr; + + pair->name_len = namelen; + convert_sensivity_field(stmt, encoding, nameptr, namelen, (char**)&name); + pair->name = name; + + ptr += namelen * 2; + idlen = *ptr++; + idptr = ptr; + ptr += idlen * 2; + + pair->id_len = idlen; + convert_sensivity_field(stmt, encoding, idptr, idlen, (char**)&id); + pair->id = id; + + pairs->push_back(pair.get()); + pair.transferred(); + } + *pptr = ptr; + } + + void parse_column_sensitivity_props(_Inout_ sensitivity_metadata* meta, _Inout_ unsigned char **pptr) + { + unsigned char *ptr = *pptr; + unsigned short ncols; + + // Get number of columns + meta->num_columns = ncols = *(reinterpret_cast(ptr)); + + // Move forward + ptr += sizeof(unsigned short); + + while (ncols--) { + unsigned short npairs = *(reinterpret_cast(ptr)); + + ptr += sizeof(unsigned short); + + column_sensitivity column; + column.num_pairs = npairs; + + while (npairs--) { + label_infotype_pair pair; + + unsigned short labelidx, typeidx; + labelidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + typeidx = *(reinterpret_cast(ptr)); + ptr += sizeof(unsigned short); + + pair.label_idx = labelidx; + pair.infotype_idx = typeidx; + + column.label_info_pairs.push_back(pair); + } + + meta->columns_sensitivity.push_back(column); + } + + *pptr = ptr; + } + + USHORT fill_column_sensitivity_array(_Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno, _Inout_ zval *return_array TSRMLS_CC) + { + sensitivity_metadata* meta = stmt->current_sensitivity_metadata; + if (meta == NULL) { + return 0; + } + + SQLSRV_ASSERT(colno >= 0 && colno < meta->num_columns, "fill_column_sensitivity_array: column number out of bounds"); + + zval data_classification; + ZVAL_UNDEF(&data_classification); + core::sqlsrv_array_init(*stmt, &data_classification TSRMLS_CC ); + + USHORT num_pairs = meta->columns_sensitivity[colno].num_pairs; + + if (num_pairs == 0) { + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return 0; + } + + zval sensitivity_properties; + ZVAL_UNDEF(&sensitivity_properties); + core::sqlsrv_array_init(*stmt, &sensitivity_properties TSRMLS_CC); + + for (USHORT j = 0; j < num_pairs; j++) { + zval label_array, infotype_array; + ZVAL_UNDEF(&label_array); + ZVAL_UNDEF(&infotype_array); + + core::sqlsrv_array_init(*stmt, &label_array TSRMLS_CC); + core::sqlsrv_array_init(*stmt, &infotype_array TSRMLS_CC); + + USHORT labelidx = meta->columns_sensitivity[colno].label_info_pairs[j].label_idx; + USHORT typeidx = meta->columns_sensitivity[colno].label_info_pairs[j].infotype_idx; + + char *label = meta->labels[labelidx]->name; + char *label_id = meta->labels[labelidx]->id; + char *infotype = meta->infotypes[typeidx]->name; + char *infotype_id = meta->infotypes[typeidx]->id; + + core::sqlsrv_add_assoc_string(*stmt, &label_array, NAME, label, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &label_array, ID, label_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, LABEL, &label_array TSRMLS_CC); + + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, NAME, infotype, 1 TSRMLS_CC); + core::sqlsrv_add_assoc_string(*stmt, &infotype_array, ID, infotype_id, 1 TSRMLS_CC); + + core::sqlsrv_add_assoc_zval(*stmt, &sensitivity_properties, INFOTYPE, &infotype_array TSRMLS_CC); + + // add the pair of sensitivity properties to data_classification + core::sqlsrv_add_next_index_zval(*stmt, &data_classification, &sensitivity_properties TSRMLS_CC ); + } + + // add data classfication as associative array + core::sqlsrv_add_assoc_zval(*stmt, return_array, DATA_CLASS, &data_classification TSRMLS_CC); + + return num_pairs; + } + + void sensitivity_metadata::reset() + { + std::for_each(labels.begin(), labels.end(), name_id_pair_free); + labels.clear(); + + std::for_each(infotypes.begin(), infotypes.end(), name_id_pair_free); + infotypes.clear(); + + columns_sensitivity.clear(); + } +} // namespace data_classification diff --git a/source/shared/globalization.h b/source/shared/globalization.h index 4ddccc52..f7e1afd3 100644 --- a/source/shared/globalization.h +++ b/source/shared/globalization.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic.h b/source/shared/interlockedatomic.h index f46e2b1d..12456143 100644 --- a/source/shared/interlockedatomic.h +++ b/source/shared/interlockedatomic.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic_gcc.h b/source/shared/interlockedatomic_gcc.h index 8a1f3732..171c1ad2 100644 --- a/source/shared/interlockedatomic_gcc.h +++ b/source/shared/interlockedatomic_gcc.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedslist.h b/source/shared/interlockedslist.h index cd71452a..4bc04c1f 100644 --- a/source/shared/interlockedslist.h +++ b/source/shared/interlockedslist.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, singly // linked list. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index a572ab02..cb624951 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -3,7 +3,7 @@ // // Contents: Contains portable classes for localization // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index e67bbf22..0e733826 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -5,7 +5,7 @@ // Must be included in one c/cpp file per binary // A build error will occur if this inclusion policy is not followed // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -280,9 +280,23 @@ bool EncodingConverter::Initialize() using namespace std; SystemLocale::SystemLocale( const char * localeName ) - : m_pLocale( new std::locale(localeName) ) - , m_uAnsiCP(CP_UTF8) + : m_uAnsiCP(CP_UTF8) + , m_pLocale(NULL) { + const char* DEFAULT_LOCALE = "en_US.UTF-8"; + + try { + m_pLocale = new std::locale(localeName); + } + catch(const std::exception& e) { + localeName = DEFAULT_LOCALE; + } + + if(!m_pLocale) { + m_pLocale = new std::locale(localeName); + } + + // Mapping from locale charset to codepage struct LocaleCP { const char* localeName; @@ -331,8 +345,7 @@ const SystemLocale & SystemLocale::Singleton() #if !defined(__GNUC__) || defined(NO_THREADSAFE_STATICS) #error "Relying on GCC's threadsafe initialization of local statics." #endif - // get locale from environment and set as default - static const SystemLocale s_Default(setlocale(LC_ALL, NULL)); + static const SystemLocale s_Default(setlocale(LC_CTYPE, NULL)); return s_Default; } diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 2392c5e3..b0496f5d 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -20,7 +20,7 @@ // pecuniary loss) arising out of the use of or inability to use // this SDK, even if Microsoft has been advised of the possibility // of such damages. -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -144,7 +144,11 @@ // force column encryption #define SQL_CA_SS_FORCE_ENCRYPT (SQL_CA_SS_BASE+36) // indicate mandatory encryption for this parameter -#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+37) +// Data Classification +#define SQL_CA_SS_DATA_CLASSIFICATION (SQL_CA_SS_BASE+37) // retrieve data classification information + +#define SQL_CA_SS_MAX_USED (SQL_CA_SS_BASE+38) + // Defines for use with SQL_COPT_SS_INTEGRATED_SECURITY - Pre-Connect Option only #define SQL_IS_OFF 0L // Integrated security isn't used #define SQL_IS_ON 1L // Integrated security is used diff --git a/source/shared/sal_def.h b/source/shared/sal_def.h index 053fb199..7d4efb17 100644 --- a/source/shared/sal_def.h +++ b/source/shared/sal_def.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/typedefs_for_linux.h b/source/shared/typedefs_for_linux.h index 390d03cc..82e33eba 100644 --- a/source/shared/typedefs_for_linux.h +++ b/source/shared/typedefs_for_linux.h @@ -1,7 +1,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- // File: typedefs_for_linux.h // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/version.h b/source/shared/version.h index 7424d900..6bc7e796 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -4,7 +4,7 @@ // File: version.h // Contents: Version number constants // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -26,12 +26,12 @@ // Increase Minor with backward compatible new functionalities and API changes. // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 -#define SQLVERSION_MINOR 6 -#define SQLVERSION_PATCH 1 +#define SQLVERSION_MINOR 7 +#define SQLVERSION_PATCH 0 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 -#define PREVIEW 0 +#define PREVIEW 1 #define SEMVER_PRERELEASE // Semantic versioning build metadata, build meta data is not counted in precedence order. diff --git a/source/shared/xplat.h b/source/shared/xplat.h index 1b195639..a32cdda0 100644 --- a/source/shared/xplat.h +++ b/source/shared/xplat.h @@ -3,7 +3,7 @@ // // Contents: include for definition of Windows types for non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_intsafe.h b/source/shared/xplat_intsafe.h index 91db2652..90d2f150 100644 --- a/source/shared/xplat_intsafe.h +++ b/source/shared/xplat_intsafe.h @@ -4,7 +4,7 @@ // Contents: This module defines helper functions to prevent // integer overflow bugs. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winerror.h b/source/shared/xplat_winerror.h index 34e7ffb5..0ebd1f2a 100644 --- a/source/shared/xplat_winerror.h +++ b/source/shared/xplat_winerror.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winnls.h b/source/shared/xplat_winnls.h index bc81cf58..22bfedc9 100644 --- a/source/shared/xplat_winnls.h +++ b/source/shared/xplat_winnls.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index 20d4d858..a8a4e89d 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.6 for PHP for SQL Server +dnl Microsoft Drivers 5.7 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index 887d1e77..33cac9bd 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 532934f7..5500da23 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use connection handles // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -219,6 +219,7 @@ namespace SSStmtOptionNames { const char DATE_AS_STRING[] = "ReturnDatesAsStrings"; const char FORMAT_DECIMALS[] = "FormatDecimals"; const char DECIMAL_PLACES[] = "DecimalPlaces"; + const char DATA_CLASSIFICATION[] = "DataClassification"; } namespace SSConnOptionNames { @@ -233,6 +234,7 @@ const char Authentication[] = "Authentication"; const char CharacterSet[] = "CharacterSet"; const char ColumnEncryption[] = "ColumnEncryption"; const char ConnectionPooling[] = "ConnectionPooling"; +const char Language[] = "Language"; const char ConnectRetryCount[] = "ConnectRetryCount"; const char ConnectRetryInterval[] = "ConnectRetryInterval"; const char Database[] = "Database"; @@ -311,6 +313,12 @@ const stmt_option SS_STMT_OPTS[] = { SQLSRV_STMT_OPTION_DECIMAL_PLACES, std::unique_ptr( new stmt_option_decimal_places ) }, + { + SSStmtOptionNames::DATA_CLASSIFICATION, + sizeof( SSStmtOptionNames::DATA_CLASSIFICATION ), + SQLSRV_STMT_OPTION_DATA_CLASSIFICATION, + std::unique_ptr( new stmt_option_data_classification ) + }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -380,6 +388,15 @@ const connection_option SS_CONN_OPTS[] = { CONN_ATTR_BOOL, conn_null_func::func }, + { + SSConnOptionNames::Language, + sizeof(SSConnOptionNames::Language), + SQLSRV_CONN_OPTION_LANGUAGE, + ODBCConnOptions::Language, + sizeof(ODBCConnOptions::Language), + CONN_ATTR_STRING, + conn_str_append_func::func + }, { SSConnOptionNames::Driver, sizeof(SSConnOptionNames::Driver), diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index 80014524..2b394862 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -2,7 +2,7 @@ // File: init.cpp // Contents: initialization routines for the extension // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -219,7 +219,7 @@ zend_function_entry sqlsrv_functions[] = { PHP_FE( sqlsrv_client_info, sqlsrv_client_info_arginfo ) PHP_FE( sqlsrv_server_info, sqlsrv_server_info_arginfo ) PHP_FE( sqlsrv_cancel, sqlsrv_cancel_arginfo ) - PHP_FE( sqlsrv_free_stmt, sqlsrv_close_arginfo ) + PHP_FE( sqlsrv_free_stmt, sqlsrv_free_stmt_arginfo ) PHP_FE( sqlsrv_field_metadata, sqlsrv_field_metadata_arginfo ) PHP_FE( sqlsrv_send_stream_data, sqlsrv_send_stream_data_arginfo ) PHP_FE( SQLSRV_SQLTYPE_BINARY, sqlsrv_sqltype_size_arginfo ) diff --git a/source/sqlsrv/php_sqlsrv.h b/source/sqlsrv/php_sqlsrv.h index 4de7f5c9..36bb9a95 100644 --- a/source/sqlsrv/php_sqlsrv.h +++ b/source/sqlsrv/php_sqlsrv.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index c294f465..3ebb179f 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -214,12 +214,12 @@ bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_err // returned in utf16_out_string. unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) char const* mbcs_in_string, _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer(mbcs_in_string) wchar_t* utf16_out_string, - _In_ unsigned int utf16_len ); + _In_ unsigned int utf16_len, bool use_strict_conversion = false ); // create a wide char string from the passed in mbcs string. NULL is returned if the string // could not be created. No error is posted by this function. utf16_len is the number of // wchar_t characters, not the number of bytes. SQLWCHAR* utf16_string_from_mbcs_string( _In_ unsigned int php_encoding, _In_reads_bytes_(mbcs_len) const char* mbcs_string, - _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len ); + _In_ unsigned int mbcs_len, _Out_ unsigned int* utf16_len, bool use_strict_conversion = false ); // *** internal error macros and functions *** bool handle_error( sqlsrv_context const* ctx, int log_subsystem, const char* function, diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 9d60de25..bbb01190 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use statement handles // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -42,7 +42,6 @@ unsigned int current_log_subsystem = LOG_STMT; // constants used as invalid types for type errors const zend_uchar PHPTYPE_INVALID = SQLSRV_PHPTYPE_INVALID; -const int SQLTYPE_INVALID = 0; const int SQLSRV_INVALID_PRECISION = -1; const SQLUINTEGER SQLSRV_INVALID_SIZE = (~1U); const int SQLSRV_INVALID_SCALE = -1; @@ -51,8 +50,6 @@ const int SQLSRV_SIZE_MAX_TYPE = -1; // constants for maximums in SQL Server const int SQL_SERVER_MAX_FIELD_SIZE = 8000; const int SQL_SERVER_MAX_PRECISION = 38; -const int SQL_SERVER_DEFAULT_PRECISION = 18; -const int SQL_SERVER_DEFAULT_SCALE = 0; // default class used when no class is specified by sqlsrv_fetch_object const char STDCLASS_NAME[] = "stdclass"; @@ -470,7 +467,6 @@ PHP_FUNCTION( sqlsrv_fetch_array ) PHP_FUNCTION( sqlsrv_field_metadata ) { sqlsrv_stmt* stmt = NULL; - SQLSMALLINT num_cols = -1; LOG_FUNCTION( "sqlsrv_field_metadata" ); @@ -481,6 +477,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // get the number of fields in the resultset and its metadata if not exists SQLSMALLINT num_cols = get_resultset_meta_data(stmt); + if (stmt->data_classification) { + core_sqlsrv_sensitivity_metadata(stmt); + } + zval result_meta_data; ZVAL_UNDEF( &result_meta_data ); core::sqlsrv_array_init( *stmt, &result_meta_data TSRMLS_CC ); @@ -533,6 +533,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) core::sqlsrv_add_assoc_long( *stmt, &field_array, FieldMetaData::NULLABLE, core_meta_data->field_is_nullable TSRMLS_CC ); + if (stmt->data_classification) { + data_classification::fill_column_sensitivity_array(stmt, f, &field_array TSRMLS_CC); + } + // add this field's meta data to the result set meta data core::sqlsrv_add_next_index_zval( *stmt, &result_meta_data, &field_array TSRMLS_CC ); } @@ -1505,11 +1509,11 @@ void convert_to_zval( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_ Z_TRY_ADDREF( out_zval ); break; } - case SQLSRV_PHPTYPE_DATETIME: - { - out_zval = *( static_cast( in_val )); - break; - } + case SQLSRV_PHPTYPE_DATETIME: + { + convert_datetime_string_to_zval(stmt, static_cast(in_val), field_len, out_zval); + break; + } case SQLSRV_PHPTYPE_NULL: ZVAL_NULL(&out_zval); @@ -1791,7 +1795,12 @@ SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt * stmt) if (num_cols == 0) { getMetaData = true; - num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + if (stmt->column_count == ACTIVE_NUM_COLS_INVALID) { + num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + stmt->column_count = num_cols; + } else { + num_cols = stmt->column_count; + } } try { diff --git a/source/sqlsrv/template.rc b/source/sqlsrv/template.rc index bbe47132..e95041ae 100644 --- a/source/sqlsrv/template.rc +++ b/source/sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index cec66c3f..9e545b6a 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.6 for PHP for SQL Server +// Microsoft Drivers 5.7 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -205,8 +205,8 @@ ss_error SS_ERRORS[] = { }, { - SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, - { IMSSP, (SQLCHAR*)"Failed to create an instance of class %1!s!.", -30, true } + SS_SQLSRV_ERROR_ZEND_OBJECT_FAILED, + { IMSSP, (SQLCHAR*)"Failed to create an instance of class %1!s!.", -30, true } }, { @@ -440,6 +440,18 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_AAD_MSI_UID_PWD_NOT_NULL, { IMSSP, (SQLCHAR*) "When using ActiveDirectoryMsi Authentication, PWD must be NULL. UID can be NULL, but if not, an empty string is not accepted.", -118, false} }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, + { IMSSP, (SQLCHAR*) "The statement must be executed to retrieve Data Classification Sensitivity Metadata.", -119, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.", -120, false} + }, + { + SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -121, true} + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/output.py b/test/functional/output.py index 94c3e5a8..99543bc3 100644 --- a/test/functional/output.py +++ b/test/functional/output.py @@ -5,13 +5,14 @@ # Requirement of python 3.4 to execute this script and required result log file(s) # are in the same location # Run with command line without options required. Example: py output.py -# This script parse output of PHP Native Test +# This script parse output of PHP Test logs # ############################################################################################# import os import stat import re +import argparse # This module appends an entry to the tests list, may include the test title. # Input: search_pattern - pattern to look for in the line of the log file @@ -46,12 +47,14 @@ def get_test_entry(search_pattern, line, index, tests_list, get_title = False): tests_list.append(entry) # Extract individual test results from the log file and -# enter it in the nativeresult.xml file. -# Input: logfile - the log file -# number - the number for this xml file -def gen_XML(logfile, number): +# enter it in the xml report file. +# Input: logfile - the test log file +# number - the number for this xml file (applicable if using the default report name) +# logfilename - use the log file name for the xml output file Instead +def gen_XML(logfile, number, logfilename): print('================================================') - print("\n" + os.path.splitext(logfile)[0] + "\n" ) + filename = os.path.splitext(logfile)[0] + print("\n" + filename + "\n" ) tests_list = [] with open(os.path.dirname(os.path.realpath(__file__)) + os.sep + logfile) as f: @@ -70,10 +73,16 @@ def gen_XML(logfile, number): print(line) print('================================================') - # Generating the nativeresult.xml file. - file = open('nativeresult' + str(number) + '.xml', 'w') + # Generating the xml report. + if logfilename is True: + file = open(filename + '.xml', 'w') + report = filename + else: + file = open('nativeresult' + str(number) + '.xml', 'w') + report = 'Native Tests' + file.write('' + os.linesep) - file.write('' + os.linesep) + file.write('' + os.linesep) index = 1 for test in tests_list: @@ -83,12 +92,18 @@ def gen_XML(logfile, number): # ----------------------- Main Function ----------------------- -# Display results on screen from result log file. +# Generate XML reports from test result log files. if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--LOGFILENAME', action='store_true', help="Generate XML files using log file names (default: False)") + + args = parser.parse_args() + logfilename = args.LOGFILENAME + num = 1 for f in os.listdir(os.path.dirname(os.path.realpath(__file__))): if f.endswith("log"): logfile = f - gen_XML(logfile, num) + gen_XML(logfile, num, logfilename) num = num + 1 diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index d327d859..969d1446 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -624,6 +624,29 @@ function IsDaasMode() return ($daasMode ? true : false); } +function isSQLAzure() +{ + // 'SQL Azure' indicates SQL Database or SQL Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + try { + $conn = connect(); + $tsql = "SELECT SERVERPROPERTY ('edition')"; + $stmt = $conn->query($tsql); + + $result = $stmt->fetch(PDO::FETCH_NUM); + $edition = $result[0]; + + if ($edition === "SQL Azure") { + return true; + } else { + return false; + } + } catch (Exception $e) { + echo $e->getMessage(); + die("Could not fetch server property."); + } +} + function isAzureDW() { // Check if running Azure Data Warehouse diff --git a/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt b/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt index 237a5e5b..49649bdc 100644 --- a/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt +++ b/test/functional/pdo_sqlsrv/PDO29_ConnInterface.phpt @@ -46,6 +46,14 @@ function CheckInterface($conn) '__sleep' => true, 'inTransaction' => true, ); + + $phpver = substr(phpversion(), 0, 3); + if ($phpver >= '7.4') { + // Reference: https://wiki.php.net/rfc/custom_object_serialization + unset($expected['__wakeup']); + unset($expected['__sleep']); + } + $classname = get_class($conn); $methods = get_class_methods($classname); foreach ($methods as $k => $method) diff --git a/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt b/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt index 06ca54d1..1bc73c2f 100644 --- a/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt +++ b/test/functional/pdo_sqlsrv/PDO32_StmtInterface.phpt @@ -49,6 +49,14 @@ function checkInterface($stmt) '__wakeup' => true, '__sleep' => true, ); + + $phpver = substr(phpversion(), 0, 3); + if ($phpver >= '7.4') { + // Reference: https://wiki.php.net/rfc/custom_object_serialization + unset($expected['__wakeup']); + unset($expected['__sleep']); + } + $classname = get_class($stmt); $methods = get_class_methods($classname); foreach ($methods as $k => $method) { diff --git a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt index 5513290b..1153d6ac 100644 --- a/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt +++ b/test/functional/pdo_sqlsrv/PDO81_MemoryCheck.phpt @@ -11,6 +11,10 @@ PHPT_EXEC=true columnCount(); + $result = $stmt->fetchAll(); + $rowCount = count($result); + unset($result); + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + + case 5: // fetchObject + $stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true)); + $fldCount = $stmt->columnCount(); + while ($obj = $stmt->fetchObject()) { + unset($obj); + $rowCount++; + } + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + + case 6: // fetchColumn + $stmt = ExecuteQueryEx($conn, $tsql, ($prepared ? false : true)); + $fldCount = $stmt->columnCount(); + // Check for "false" to terminate because fetchColumn may return NULL + while (($result = $stmt->fetchColumn()) !== false) { + unset($result); + $rowCount++; + } + $stmt->closeCursor(); + unset($stmt); + if ($rowCount != $noRows) { + die("$rowCount rows retrieved instead of $noRows\n"); + } + break; + default: break; @@ -228,7 +286,7 @@ function Repro() { try { - MemCheck(20, 10, 15, 1, 3, 0); + MemCheck(_NUM_PASSES, _NUM_ROWS1, _NUM_ROWS2, 1, 6, 0); } catch (Exception $e) { diff --git a/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt new file mode 100644 index 00000000..39d30729 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_035_binary_encoding_error_bound_by_name_errors.phpt @@ -0,0 +1,164 @@ +--TEST-- +GitHub Issue #35 binary encoding error when binding by name +--DESCRIPTION-- +Based on pdo_035_binary_encoding_error_bound_by_name.phpt but this includes error checking for various encoding errors +--SKIPIF-- + +--FILE-- +prepare($sql); + $stmt->bindParam(1, $value, PDO::PARAM_INT, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->setAttribute(constant('PDO::SQLSRV_ATTR_ENCODING'), PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $input, PDO::PARAM_LOB); + $stmt->execute(); + echo "bindTypeNoEncoding: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An encoding was specified for parameter 1. Only PDO::PARAM_LOB and PDO::PARAM_STR can take an encoding option.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in bindTypeNoEncoding\n"; + var_dump($e->getMessage()); + } + } +} + +function bindDefaultEncoding($conn, $sql, $input) +{ + try { + $value = 1; + + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $value, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->setAttribute(constant('PDO::SQLSRV_ATTR_ENCODING'), PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam(2, $input, PDO::PARAM_LOB); + $stmt->execute(); + echo "bindDefaultEncoding: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*Invalid encoding specified for parameter 1.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in bindDefaultEncoding\n"; + var_dump($e->getMessage()); + } + } +} + +function insertData($conn, $sql, $input) +{ + try { + $value = 1; + + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $value); + // Specify binary encoding for the second parameter only such that the first + // parameter is unaffected + $stmt->bindParam(2, $input, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + } catch (PDOException $e) { + echo "Error unexpected in insertData\n"; + var_dump($e->getMessage()); + } +} + +function invalidEncoding1($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn(1, $id, PDO::PARAM_INT, 0, PDO::SQLSRV_ENCODING_UTF8); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding1: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An encoding was specified for column 1. Only PDO::PARAM_LOB and PDO::PARAM_STR column types can take an encoding option.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding1\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidEncoding2($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_DEFAULT); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding2: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*Invalid encoding specified for column 1.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding2\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidEncoding3($conn, $sql) +{ + try { + $stmt = $conn->prepare($sql); + $stmt->bindColumn(1, $id, PDO::PARAM_STR, 0, "dummy"); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + echo "invalidEncoding3: expected to fail!\n"; + } catch (PDOException $e) { + $error = '*An invalid type or value was given as bound column driver data for column 1. Only encoding constants such as PDO::SQLSRV_ENCODING_UTF8 may be used as bound column driver data.'; + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in invalidEncoding3\n"; + var_dump($e->getMessage()); + } + } +} + +try { + require_once( "MsCommon_mid-refactor.inc" ); + + // Connect + $conn = connect(); + + // Create a table + $tableName = "testTableIssue35"; + createTable($conn, $tableName, array("ID" => "int", "Value" => "varbinary(max)")); + + // Insert data using bind parameters + $sql = "INSERT INTO $tableName VALUES (?, ?)"; + $message = "This is to test github issue 35."; + $value = base64_encode($message); + + // Errors expected + bindTypeNoEncoding($conn, $sql, $value); + bindDefaultEncoding($conn, $sql, $value); + + // No error expected + insertData($conn, $sql, $value); + + // Fetch data, but test several invalid encoding issues (errors expected) + $sql = "SELECT * FROM $tableName"; + invalidEncoding1($conn, $sql); + invalidEncoding2($conn, $sql); + invalidEncoding3($conn, $sql); + + // Now fetch it back + $stmt = $conn->prepare("SELECT Value FROM $tableName"); + $stmt->bindColumn('Value', $val1, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + $stmt->fetch(PDO::FETCH_BOUND); + var_dump($val1 === $value); + + // Close connection + dropTable($conn, $tableName); + unset($stmt); + unset($conn); + print "Done\n"; +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +bool(true) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt new file mode 100644 index 00000000..16947da0 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax.phpt @@ -0,0 +1,106 @@ +--TEST-- +GitHub issue #569 - direct query on varchar max fields results in function sequence error +--DESCRIPTION-- +Verifies that the problem is no longer reproducible. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoTestTable_569'; + dropTable($conn, $tableName); + + if ($qualified && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = deterministic, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = AEColumnKey))"; + } else { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max))"; + } + $conn->exec($tsql); + + $input = 'some very large string'; + $tsql = "INSERT INTO $tableName (c1) VALUES (?)"; + $stmt = $conn->prepare($tsql); + $param = array($input); + $stmt->execute($param); + + $tsql = "SELECT * FROM $tableName"; + try { + $stmt = $conn->prepare($tsql); + $stmt->execute(); + } catch (PDOException $e) { + echo ("Failed to read from $tableName\n"); + echo $e->getMessage(); + } + + $row = $stmt->fetch(PDO::FETCH_NUM); + if ($row[0] !== $input) { + echo "Expected $input but got: "; + var_dump($row[0]); + } + + $tsql2 = "DELETE FROM $tableName"; + $rows = $conn->exec($tsql2); + if ($rows !== 1) { + echo 'Expected 1 row affected but got: '; + var_dump($rows); + } + + // Fetch from the empty table + try { + $stmt = $conn->prepare($tsql); + $stmt->execute(); + } catch (PDOException $e) { + echo ("Failed to read $tableName, now empty\n"); + echo $e->getMessage(); + } + + $result = $stmt->fetch(PDO::FETCH_NUM); + if ($result !== false) { + echo 'Expected bool(false) when fetching an empty table but got: '; + var_dump($result); + } + + // Fetch the same table but using client cursor + $stmt = $conn->prepare($tsql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL)); + $stmt->execute(); + + $result = $stmt->fetch(); + if ($result !== false) { + echo 'Expected bool(false) when fetching an empty table but got: '; + var_dump($result); + } + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + echo $e->getMessage(); +} + +echo "Done\n"; + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt b/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt new file mode 100644 index 00000000..870a8457 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_929_language_option.phpt @@ -0,0 +1,54 @@ +--TEST-- +GitHub issue 929 - able to change the language when connecting +--DESCRIPTION-- +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +getCode(); + if ($code !== '42S22') { + echo "Expected SQLSTATE 42S22\n"; + var_dump($code); + } + + // The error message is different when testing against Azure DB / Data Warehouse + // Use wildcard patterns for matching + if (isSQLAzure()) { + $expected = "*Invalid column name [\"']BadColumn[\"']\."; + } else { + $expected = "*Ungültiger Spaltenname [\"']BadColumn[\"']\."; + } + + $message = $e->getMessage(); + if (!fnmatch($expected, $message)) { + echo "Expected to find $expected in the error message\n"; + var_dump($message); + } + +} + +require_once("MsSetup.inc"); + +try { + $conn = new PDO("sqlsrv:server=$server;Language = German", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tsql = "SELECT *, BadColumn FROM sys.syslanguages"; + $conn->query($tsql); + echo 'This should have failed!\n'; +} catch (PDOException $e) { + verifyErrorContents($e); +} + +unset($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt index f468ffd6..ce36df03 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_access_token.phpt @@ -129,6 +129,25 @@ function simpleTest($conn) dropTable($conn, $tableName); } +function connectAzureDB($accToken, $showException) +{ + global $adServer, $adDatabase, $maxAttempts; + + $conn = false; + try { + $connectionInfo = "Database = $adDatabase; AccessToken = $accToken;"; + $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo"); + } catch (PDOException $e) { + if ($showException) { + echo "Could not connect with Azure AD AccessToken after $maxAttempts retries.\n"; + print_r($e->getMessage()); + echo PHP_EOL; + } + } + + return $conn; +} + // First test some error conditions require_once('MsSetup.inc'); connectWithInvalidOptions($server); @@ -138,13 +157,26 @@ connectWithEmptyAccessToken($server); // Next, test with a valid access token and perform some simple tasks require_once('access_token.inc'); +$maxAttempts = 3; + try { if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { - $connectionInfo = "Database = $adDatabase; AccessToken = $accToken;"; - $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo"); - $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); - simpleTest($conn); - unset($conn); + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($accToken, ($numAttempts == ($maxAttempts - 1))); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); + + // Proceed when successfully connected + if ($conn) { + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); + simpleTest($conn); + unset($conn); + } } } catch(PDOException $e) { print_r( $e->getMessage() ); diff --git a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt index 105f6395..d5cf54b0 100644 --- a/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt +++ b/test/functional/pdo_sqlsrv/pdo_azure_ad_authentication.phpt @@ -55,21 +55,39 @@ try { // your credentials to test, or this part is skipped. // $azureServer = $adServer; -$azureDatabase = $adDatabase; -$azureUsername = $adUser; -$azurePassword = $adPassword; +$maxAttempts = 3; -if ($azureServer != 'TARGET_AD_SERVER') { +function connectAzureDB($showException) +{ + global $adServer, $adUser, $adPassword, $maxAttempts; + $connectionInfo = "Authentication = ActiveDirectoryPassword; TrustServerCertificate = false"; - + + $conn = false; try { - $conn = new PDO("sqlsrv:server = $azureServer ; $connectionInfo", $azureUsername, $azurePassword); + $conn = new PDO("sqlsrv:server = $adServer; $connectionInfo", $adUser, $adPassword); echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; } catch (PDOException $e) { - echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r($e->getMessage()); - echo "\n"; + if ($showException) { + echo "Could not connect with ActiveDirectoryPassword after $maxAttempts retries.\n"; + print_r($e->getMessage()); + echo "\n"; + } } + + return $conn; +} + +if ($azureServer != 'TARGET_AD_SERVER') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($numAttempts == ($maxAttempts - 1)); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); } else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } diff --git a/test/functional/pdo_sqlsrv/pdo_batch_query.phpt b/test/functional/pdo_sqlsrv/pdo_batch_query.phpt new file mode 100644 index 00000000..9453e26d --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_batch_query.phpt @@ -0,0 +1,203 @@ +--TEST-- +Test a batch query with different cursor types +--DESCRIPTION-- +Verifies row and column counts from batch queries. This is the +equivalent of sqlsrv_batch_query.phpt on the sqlsrv side. +TODO: Fix this test once error reporting in PDO is fixed, because batch +queries are not supposed to work with server side cursors. For now, no errors +or warnings are returned. For information on the expected behaviour of cursors +with batch queries, see +https://docs.microsoft.com/en-us/previous-versions/visualstudio/aa266531(v=vs.60) +--SKIPIF-- + +--FILE-- + PDO::CURSOR_FWDONLY), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_DYNAMIC), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_STATIC), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_KEYSET), + array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED), + ); + +// Data for testing, all integer types +$data = array(array(86, -217483648, 0, -432987563, 7, 217483647), + array(0, 31, 127, 255, 1, 10), + array(4534, -212, 32767, 0, 7, -32768), + array(-1, 546098342985600, 9223372000000000000, 5115115115115, 7, -7), + array(0, 1, 0, 0, 1, 1), + ); + +// Column names +$colName = array('c1_int', 'c2_tinyint', 'c3_smallint', 'c4_bigint', 'c5_bit'); + +// Fetch one column at a time +$expectedCols = 1; + +// Number of table rows +$expectedRows = sizeof($data[0]); + +// Expected result sets = number of columns, since the batch fetches each column sequentially +$expectedResultSets = sizeof($colName); + +function checkErrors($expectedError) +{ + // TODO: Fill this in once PDO error reporting is fixed +} + +function checkColumnsAndRows($stmt, $cursor, $before) +{ + global $expectedCols, $expectedRows; + + $cols = $stmt->columnCount(); + + if ($cols != $expectedCols) { + fatalError("Incorrect number of columns returned with $cursor cursor. Expected $expectedCols columns, got $cols columns\n"); + } + + $rows = $stmt->rowCount(); + + // Buffered cursors always return the correct number of rows. Other cursors + // return -1 rows before fetching. Static and keyset cursors return -1 even + // after fetching, while forward and dynamic cursors return the correct + // number of rows after fetching. + if ($cursor == 'buffered') { + if ($rows != $expectedRows) { + fatalError("Incorrect number of columns returned with buffered cursor. Expected $expectedRows rows, got $rows rows\n"); + } + } else { + if ($before) { + if ($rows !== -1) { + fatalError("Incorrect number of rows returned before fetching with a $cursor cursor. Expected -1 rows, got $rows rows\n"); + } + } else { + if ($cursor == 'static' or $cursor == 'keyset') { + if ($rows !== -1) { + fatalError("Incorrect number of rows returned before fetching with a $cursor cursor. Expected -1 rows, got $rows rows\n"); + } + } else { + if ($rows != $expectedRows) { + fatalError("Incorrect number of columns returned with buffered cursor. Expected $expectedRows rows, got $rows rows\n"); + } + } + } + } +} + +function printCursor($element) +{ + $cursor = 'forward'; + switch($element) { + case 0: + echo "Testing with forward cursor...\n"; + break; + case 1: + echo "Testing with dynamic cursor...\n"; + $cursor = 'dynamic'; + break; + case 2: + echo "Testing with static cursor...\n"; + $cursor = 'static'; + break; + case 3: + echo "Testing with keyset cursor...\n"; + $cursor = 'keyset'; + break; + case 4: + echo "Testing with buffered cursor...\n"; + $cursor = 'buffered'; + break; + default: + fatalError("Unknown cursor type! Exiting\n"); + } + return $cursor; +} + +$conn = connect(); +$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Create and populate a table of integer types +$tableName = 'batch_query_test'; +$columns = array(new ColumnMeta('int', $colName[0]), + new ColumnMeta('tinyint', $colName[1]), + new ColumnMeta('smallint',$colName[2]), + new ColumnMeta('bigint', $colName[3]), + new ColumnMeta('bit', $colName[4])); + +createTable($conn, $tableName, $columns); + +// Insert each row. Need an associative array to use insertRow() +for ($i = 0; $i < $expectedRows; ++$i) { + $inputs = array(); + for ($j = 0; $j < $expectedResultSets; ++$j) { + $inputs[$colName[$j]] = $data[$j][$i]; + } + + $stmt = insertRow($conn, $tableName, $inputs); + unset($inputs); + unset($stmt); +} + +$query = "SELECT c1_int FROM $tableName; + SELECT c2_tinyint FROM $tableName; + SELECT c3_smallint FROM $tableName; + SELECT c4_bigint FROM $tableName; + SELECT c5_bit FROM $tableName;"; + +// Test the batch query with different cursor types +for ($i = 0; $i < sizeof($cursors); ++$i) { + try { + $cursorType = $cursors[$i]; + $cursor = printCursor($i); + + $stmt = $conn->prepare($query, $cursorType); + $stmt->execute(); + + $numResultSets = 0; + + // Check the column and row count before and after running through + // each result set, because some cursor types may return the number + // of rows only after fetching all rows in the result set + do { + checkColumnsAndRows($stmt, $cursor, true); + + $row = 0; + while ($res = $stmt->fetch(PDO::FETCH_NUM)) { + if ($res[0] != $data[$numResultSets][$row]) { + fatalError("Wrong result, expected ".$data[$numResultSets][$row].", got $res[0]\n"); + } + ++$row; + } + + checkColumnsAndRows($stmt, $cursor, false); + ++$numResultSets; + } while ($next = $stmt->nextRowset()); + + if ($numResultSets != $expectedResultSets) { + fatalError("Unexpected number of result sets, expected $expectedResultedSets, got $numResultSets\n"); + } + } catch(PDOException $e) { + echo "Exception caught\n"; + print_r($e); + } + +unset($stmt); +} + +dropTable($conn, $tableName); +unset($conn); + +echo "Done.\n"; +?> +--EXPECT-- +Testing with forward cursor... +Testing with dynamic cursor... +Testing with static cursor... +Testing with keyset cursor... +Testing with buffered cursor... +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt b/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt new file mode 100644 index 00000000..7b0f101a --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_construct_attr_errors.phpt @@ -0,0 +1,140 @@ +--TEST-- +Test various connection errors with invalid attributes +--DESCRIPTION-- +This is similar to sqlsrv sqlsrv_connStr.phpt such that invalid connection attributes or values used when connecting. +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_EXCEPTION); + $conn = connect("", $options); + $attr = ($binary) ? PDO::SQLSRV_ENCODING_BINARY : 'gibberish'; + + $conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, $attr); + echo "Should have failed about an invalid encoding.\n"; + } catch (PDOException $e) { + $error = '*An invalid encoding was specified for SQLSRV_ATTR_ENCODING.'; + if (!fnmatch($error, $e->getMessage())) { + echo "invalidEncoding($binary)\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidServer() +{ + global $uid, $pwd; + + // Test an invalid server name in UTF-8 + try { + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $invalid = pack("H*", "ffc0"); + $conn = new PDO("sqlsrv:server = $invalid;", $uid, $pwd, $options); + echo "Should have failed to connect to invalid server.\n"; + } catch (PDOException $e) { + $error1 = '*Login timeout expired'; + $error2 = '*An error occurred translating the connection string to UTF-16: *'; + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidServer\n"; + var_dump($e->getMessage()); + } + } +} + +function utf8APP() +{ + global $server, $uid, $pwd; + try { + // Use a UTF-8 name + $app = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + $dsn = "APP = $app;"; + $conn = connect($dsn); + } catch (PDOException $e) { + echo "With APP in UTF8 it should not have failed!\n"; + var_dump($e->getMessage()); + } +} + +function invalidCredentials() +{ + global $server, $database; + + // Use valid UTF-8 + $user = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + $passwd = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $error1 = "*Login failed for user \'*\'."; + $error2 = "*Login timeout expired*"; + $error3 = "*Could not open a connection to SQL Server*"; + + try { + $conn = new PDO("sqlsrv:server = $server; database = $database;", $user, $passwd, $options); + echo "Should have failed to connect\n"; + } catch (PDOException $e) { + if (fnmatch($error1, $e->getMessage()) || + fnmatch($error2, $e->getMessage()) || + fnmatch($error3, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidCredentials()\n"; + var_dump($e->getMessage()); + } + } +} + +function invalidPassword() +{ + global $server, $database; + + // Use valid UTF-8 + $user = pack('H*', 'c59ec6a1d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + // Use invalid UTF-8 + $passwd = pack('H*', 'c59ec6c0d0bcc49720c59bc3a4e1839dd180c580e1bb8120ce86c59ac488c4a8c4b02dc5a5e284aec397c5a7'); + + $options = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + + // Possible errors + $error = "*An error occurred translating the connection string to UTF-16: *"; + $error1 = "*Login failed for user \'*\'."; + $error2 = "*Login timeout expired*"; + + try { + $conn = new PDO("sqlsrv:server = $server; database = $database;", $user, $passwd, $options); + echo "Should have failed to connect\n"; + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + // Sometimes it might fail with two other possible error messages + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; // matched at least one of the expected error messages + } else { + echo "invalidPassword()\n"; + var_dump($e->getMessage()); + } + } + } +} + +try { + invalidEncoding(false); + invalidEncoding(true); + invalidServer(); + utf8APP(); + invalidCredentials(); + invalidPassword(); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); +} +?> +--EXPECT-- +Done + diff --git a/test/functional/pdo_sqlsrv/pdo_data_classification.phpt b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt new file mode 100644 index 00000000..13add5bb --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_data_classification.phpt @@ -0,0 +1,322 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + PDO::ERRMODE_EXCEPTION, PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $conn = new PDO($dsn, $uid, $pwd, $attr); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (1) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + } catch (PDOException $e) { + if (!fnmatch($stmtErr, $e->getMessage())) { + echo "Connection attribute test (2) unexpected\n"; + var_dump($e->getMessage()); + } + } + + try { + $dsn = getDSN($server, $databaseName, $driver); + $attr = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + $conn = new PDO($dsn, $uid, $pwd, $attr); + $conn->getAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION); + } catch (PDOException $e) { + if (!fnmatch($noSupportErr, $e->getMessage())) { + echo "Connection attribute test (3) unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function testNotAvailable($conn, $tableName, $isSupported, $driverCapable) +{ + // If supported, the query should return a column with no classification + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = $conn->prepare($tsql, $options); + $stmt->execute(); + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + + $unexpectedErrorState = '*Failed to retrieve Data Classification Sensitivity Metadata: Check if ODBC driver or the server supports the Data Classification feature.'; + + $error = ($driverCapable) ? $notAvailableErr : $unexpectedErrorState; + try { + $metadata = $stmt->getColumnMeta(0); + echo "testNotAvailable: expected getColumnMeta to fail\n"; + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "testNotAvailable: exception unexpected\n"; + var_dump($e->getMessage()); + } + } +} + +function isDataClassSupported($conn, &$driverCapable) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)["DriverVer"]; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + $driverCapable = true; + if ($version[0] < 17 || $version[1] < 2) { + $driverCapable = false; + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); + if (explode('.', $serverVer)[0] < 15) + return false; + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = $conn->query($tsql); + + // Run the query with the attribute set to false + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => false); + $stmt2 = $conn->prepare($tsql, $options); + $stmt2->execute(); + + // The metadata for each column should be identical + $numCol = $stmt1->columnCount(); + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + $diff = array_diff($metadata1, $metadata2); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data (Label, Information Type) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + global $dataClassKey; + + $numCol = $stmt1->columnCount(); + $noClassInfo = array($dataClassKey => array()); + + for ($i = 0; $i < $numCol; $i++) { + $metadata1 = $stmt1->getColumnMeta($i); + $metadata2 = $stmt2->getColumnMeta($i); + + // If classification sensitivity data exists, only the + // 'flags' field should be different + foreach ($metadata2 as $key => $value) { + if ($key == 'flags') { + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($value !== $noClassInfo) { + var_dump($value); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $value[$dataClassKey])) { + var_dump($value); + } + } + } else { + // The other fields should be identical + if ($metadata1[$key] !== $value) { + var_dump($value); + } + } + } + } +} + +function runBatchQuery($conn, $tableName) +{ + global $dataClassKey; + + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $tsql = "SELECT SSN, BirthDate FROM $tableName"; + + // Run a batch query + $batchQuery = $tsql . ';' . $tsql; + $stmt = $conn->prepare($batchQuery, $options); + $stmt->execute(); + + $numCol = $stmt->columnCount(); + + // The metadata returned should be the same + $c = rand(0, $numCol - 1); + $metadata1 = $stmt->getColumnMeta($c); + $stmt->nextRowset(); + $metadata2 = $stmt->getColumnMeta($c); + + // Check the returned flags + $data1 = $metadata1['flags']; + $data2 = $metadata2['flags']; + + if (!array_key_exists($dataClassKey, $data1) || !array_key_exists($dataClassKey, $data2)) { + echo "Metadata returned with no classification data\n"; + var_dump($data1); + var_dump($data2); + } else { + $jstr1 = json_encode($data1[$dataClassKey]); + $jstr2 = json_encode($data2[$dataClassKey]); + if ($jstr1 !== $jstr2) { + echo "The JSON encoded strings should be identical\n"; + var_dump($jstr1); + var_dump($jstr2); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +try { + testConnAttrCases(); + + $conn = connect(); + $driverCapable = true; + $isSupported = isDataClassSupported($conn, $driverCapable); + + // Create a test table + $tableName = 'pdoPatients'; + $colMeta = array(new ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new ColumnMeta('CHAR(11)', 'SSN'), + new ColumnMeta('NVARCHAR(50)', 'FirstName'), + new ColumnMeta('NVARCHAR(50)', 'LastName'), + new ColumnMeta('DATE', 'BirthDate')); + createTable($conn, $tableName, $colMeta); + + // If data classification is supported, then add sensitivity classification metadata + // to columns SSN and Birthdate + $classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + + if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $conn->query($sql); + } + + // Test another error condition + testNotAvailable($conn, $tableName, $isSupported, $driverCapable); + + // Run the query without data classification metadata + $tsql = "SELECT * FROM $tableName"; + $stmt = getRegularMetadata($conn, $tsql); + + // Proceeed to retrieve sensitivity metadata, if supported + if ($isSupported) { + $options = array(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION => true); + $stmt1 = $conn->prepare($tsql, $options); + $stmt1->execute(); + + compareDataClassification($stmt, $stmt1, $classData); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = $conn->prepare($tsql); + $stmt2->execute(); + $stmt2->setAttribute(PDO::SQLSRV_ATTR_DATA_CLASSIFICATION, true); + + compareDataClassification($stmt, $stmt2, $classData); + + unset($stmt1); + unset($stmt2); + + runBatchQuery($conn, $tableName); + } + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt b/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt index 0d2b730a..32bacc69 100644 --- a/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt +++ b/test/functional/pdo_sqlsrv/pdo_empty_result_error.phpt @@ -12,6 +12,7 @@ require_once("MsCommon.inc"); // These are the error messages we expect at various points below $errorNoMoreResults = "There are no more results returned by the query."; $errorNoFields = "The active result for the query contains no fields."; +$errorNoMoreRows = "There are no more rows in the active result set. Since this result set is not scrollable, no more data may be retrieved."; // This function compares the expected error message and the error returned by errorInfo(). function CheckError($stmt, $expectedError=NULL) @@ -94,6 +95,7 @@ echo "Empty result set, call fetch first: ##################################\n"; $stmt = $conn->query("TestEmptySetProc @a='a', @b='w'"); Fetch($stmt); +Fetch($stmt, $errorNoMoreRows); NextResult($stmt); Fetch($stmt); NextResult($stmt, $errorNoMoreResults); @@ -158,6 +160,7 @@ Next result... Next result... Empty result set, call fetch first: ################################## Fetch... +Fetch... Next result... Fetch... Next result... diff --git a/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt b/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt new file mode 100644 index 00000000..c8d4f191 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_escape_braces.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test that right braces are escaped correctly and that error messages are correct when they're not +--SKIPIF-- + +--FILE-- +getMessage(), $test[1]) === false) { + print_r("Wrong error message returned for test string ".$test[0].". Expected ".$test[1].", actual output:\n"); + print_r($e->getMessage); + echo "\n"; + } + } +} + +echo "Done.\n"; +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt new file mode 100644 index 00000000..7e2ec194 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_fetch_column_twice.phpt @@ -0,0 +1,151 @@ +--TEST-- +Test fetchColumn twice in a row. Intentionally trigger various error messages. +--DESCRIPTION-- +This is similar to sqlsrv_fetch_field_twice_data_types.phpt. +--SKIPIF-- + +--FILE-- +prepare($tsql); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row !== false) { + echo "fetchBeforeExecute: fetch should have failed before execute!\n"; + } + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_NUM); + + for ($i = 0; $i < count($inputs); $i++) { + if ($row[$i] !== $inputs[$i]) { + echo "fetchBeforeExecute: expected $inputs[$i] but got $row[$i]\n"; + } + } + + unset($stmt); + } catch (PDOException $e) { + var_dump($e->getMessage()); + } +} + +function fetchColumnTwice($conn, $tableName, $col, $input) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql); + $result = $stmt->fetchColumn($col); + if ($result !== $input) { + echo "fetchColumnTwice (1): expected $input but got $result\n"; + } + $result = $stmt->fetchColumn($col); + if ($result !== false) { + echo "fetchColumnTwice (2): expected the second fetchColumn to fail\n"; + } + + // Re-run the query with fetch style + $stmt = $conn->query($tsql, PDO::FETCH_COLUMN, $col); + $result = $stmt->fetch(); + if ($result !== $input) { + echo "fetchColumnTwice (3): expected $input but got $result\n"; + } + $result = $stmt->fetch(); + if ($result !== false) { + echo "fetchColumnTwice (4): expected the second fetch to fail\n"; + } + $result = $stmt->fetchColumn($col); + echo "fetchColumnTwice (5): expected fetchColumn to throw an exception\n"; + unset($stmt); + } catch (PDOException $e) { + $error = '*There are no more rows in the active result set. Since this result set is not scrollable, no more data may be retrieved.'; + + if (!fnmatch($error, $e->getMessage())) { + echo "Error message unexpected in fetchColumnTwice\n"; + var_dump($e->getMessage()); + } + } +} + +function fetchColumnOutOfBound1($conn, $tableName, $col) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql); + $result = $stmt->fetchColumn($col); + echo "fetchColumnOutOfBound1: expected fetchColumn to throw an exception\n"; + unset($stmt); + } catch (PDOException $e) { + $error1 = '*General error: Invalid column index'; + $error2 = '*An invalid column number was specified.'; + + // Different errors may be returned depending on running with run-tests.php or not + if (fnmatch($error1, $e->getMessage()) || fnmatch($error2, $e->getMessage())) { + ; + } else { + echo "Error message unexpected in fetchColumnOutOfBound1\n"; + var_dump($e->getMessage()); + } + } +} + +function fetchColumnOutOfBound2($conn, $tableName, $col) +{ + try { + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->query($tsql, PDO::FETCH_COLUMN, $col); + $result = $stmt->fetch(); + unset($stmt); + } catch (PDOException $e) { + var_dump($e->getMessage()); + } +} + +try { + $conn = connect(); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoFetchColumnTwice'; + $colMeta = array(new ColumnMeta('int', 'c1_int'), + new ColumnMeta('varchar(20)', 'c2_varchar'), + new ColumnMeta('decimal(5, 3)', 'c3_decimal'), + new ColumnMeta('datetime', 'c4_datetime')); + createTable($conn, $tableName, $colMeta); + + $inputs = array('968580013', 'dummy value', '3.438', ('1756-04-16 23:27:09.130')); + $numCols = count($inputs); + + $tsql = "INSERT INTO $tableName(c1_int, c2_varchar, c3_decimal, c4_datetime) VALUES (?,?,?,?)"; + $stmt = $conn->prepare($tsql); + + for ($i = 0; $i < $numCols; $i++) { + $stmt->bindParam($i + 1, $inputs[$i]); + } + $stmt->execute(); + unset($stmt); + + fetchBeforeExecute($conn, $tableName, $inputs); + for ($i = 0; $i < $numCols; $i++) { + fetchColumnTwice($conn, $tableName, $i, $inputs[$i]); + } + + fetchColumnOutOfBound1($conn, $tableName, -1); + + // Change to warning mode + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + fetchColumnOutOfBound2($conn, $tableName, $numCols + 1); + + dropTable($conn, $tableName); + unset($conn); + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); +} +?> +--EXPECTREGEX-- +Warning: PDOStatement::fetch\(\): SQLSTATE\[HY000\]: General error: Invalid column index in .+(\/|\\)pdo_fetch_column_twice.php on line [0-9]+ + +Warning: PDOStatement::fetch\(\): SQLSTATE\[HY000\]: General error in .+(\/|\\)pdo_fetch_column_twice.php on line [0-9]+ +Done diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt index ab8ba5ec..8463875b 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt @@ -15,11 +15,24 @@ require_once("MsCommon_mid-refactor.inc"); function checkStringValues($obj, $columns, $values) { $size = count($values); - $objArray = (array)$obj; // turn the object into an associated array - for ($i = 0; $i < $size; $i++) { $col = $columns[$i]; - $val = $objArray[$col]; + switch ($i) { + case 0: + $val = $obj->c1; break; + case 1: + $val = $obj->c2; break; + case 2: + $val = $obj->c3; break; + case 3: + $val = $obj->c4; break; + case 4: + $val = $obj->c5; break; + case 5: + $val = $obj->c6; break; + default: + echo "Something went wrong!\n"; + } if ($val != $values[$i]) { echo "Expected $values[$i] for column $col but got: "; @@ -57,6 +70,33 @@ function checkColumnDTValue($index, $column, $values, $dtObj) } } +function randomColumns($conn, $query, $columns, $values) +{ + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query); + + // Fetch a random column to trigger caching + $lastCol = count($columns) - 1; + $col = rand(0, $lastCol); + $stmt->execute(); + $dtObj = $stmt->fetchColumn($col); + checkColumnDTValue($col, $columns[$col], $values, $dtObj); + + // Similarly, fetch another column + $col = (++$col) % count($columns); + $stmt->execute(); + $dtObj = $stmt->fetchColumn($col); + checkColumnDTValue($col, $columns[$col], $values, $dtObj); + + // Now fetch all columns in a backward order + $i = $lastCol; + do { + $stmt->execute(); + $dtObj = $stmt->fetchColumn($i); + checkColumnDTValue($i, $columns[$i], $values, $dtObj); + } while (--$i >= 0); +} + function runTest($conn, $query, $columns, $values, $useBuffer = false) { // fetch the date time values as strings or date time objects @@ -90,7 +130,7 @@ function runTest($conn, $query, $columns, $values, $useBuffer = false) $stmt->execute(); $row = $stmt->fetch(PDO::FETCH_BOTH); checkDTObjectValues($row, $columns, $values, PDO::FETCH_BOTH); - + // ATTR_STRINGIFY_FETCHES should have no effect when fetching date time objects // Setting it to true only converts numeric values to strings when fetching // See http://www.php.net/manual/en/pdo.setattribute.php for details @@ -151,7 +191,6 @@ function runTest($conn, $query, $columns, $values, $useBuffer = false) // last test: set statement attribute fetch_datetime on with no change to // prepared statement -- expected datetime objects to be returned $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); - $stmt->execute(); $i = 0; do { $stmt->execute(); @@ -221,8 +260,9 @@ try { $query = "SELECT * FROM $tableName"; - runTest($conn, $query, $columns, $values); - runTest($conn, $query, $columns, $values, true); + runtest($conn, $query, $columns, $values); + runtest($conn, $query, $columns, $values, true); + randomColumns($conn, $query, $columns, $values); dropTable($conn, $tableName); diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt index 9bffdf75..98c7c957 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt @@ -110,7 +110,6 @@ function runTest($conn, $query, $columns, $useBuffer = false) // last test: set statement attribute fetch_datetime on with no change to // prepared statement -- expected datetime objects to be returned $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); - $stmt->execute(); $i = 0; do { $stmt->execute(); diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt index 311538ab..e99aae85 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt @@ -200,7 +200,7 @@ function fetchColumns($conn, $tableName, $numRows, $numCols) $res = $stmtTmp->execute(); if (! $res) { - echo "Failed to insert data from column ". $j +1 ."\n"; + echo "Failed to insert data from column ". ($j + 1) ."\n"; } } diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt new file mode 100644 index 00000000..c90e3135 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_invalid_utf16.phpt @@ -0,0 +1,86 @@ +--TEST-- +Test fetching invalid UTF-16 from the server +--DESCRIPTION-- +This is similar to sqlsrv 0079.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_SYSTEM); + + // Create test table + $tableName = 'pdoUTF16invalid'; + $columns = array(new ColumnMeta('int', 'id', 'identity'), + new ColumnMeta('nvarchar(100)', 'c1')); + $stmt = createTable($conn, $tableName, $columns); + + // 0xdc00,0xdbff is an invalid surrogate pair + $invalidUTF16 = pack("H*", '410042004300440000DCFFDB45004600'); + + $insertSql = "INSERT INTO $tableName (c1) VALUES (?)"; + $stmt = $conn->prepare($insertSql); + $stmt->bindParam(1, $invalidUTF16, PDO::PARAM_STR, null, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + + try { + // Now fetch data with UTF-8 encoding + $tsql = "SELECT * FROM $tableName"; + $stmt = $conn->prepare($tsql); + $stmt->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8); + $stmt->execute(); + $utf8 = $stmt->fetchColumn(1); // Ignore the id column + echo "fetchColumn should have failed with an error.\n"; + } catch (PDOException $e) { + $error = '*An error occurred translating string for a field to UTF-8:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + dropProc($conn, 'Utf16InvalidOut'); + $createProc = <<query($createProc); + + try { + $invalidUTF16Out = ''; + $tsql = '{call Utf16InvalidOut(?)}'; + $stmt = $conn->prepare($tsql); + $stmt->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8); + $stmt->bindParam(1, $invalidUTF16Out, PDO::PARAM_STR, 25); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*An error occurred translating string for an output param to UTF-8:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + echo "Done\n"; + + // Done testing with the stored procedure and test table + dropProc($conn, 'Utf16InvalidOut'); + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt new file mode 100644 index 00000000..b5d2c9b2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8stream.phpt @@ -0,0 +1,106 @@ +--TEST-- +Test inserting UTF-8 stream via PHP including some checking of error conditions +--DESCRIPTION-- +This is similar to sqlsrv 0067.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- +prepare($insertSql); + $stmt->bindParam(1, $f1); + $stmt->bindParam(2, $f2); + $stmt->bindParam(3, $f3); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + + $stmt->execute(); + + // Next test UTF-8 cutoff in the middle of a valid 3 byte UTF-8 char + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Now test a 2 byte incomplete character + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "dfa0"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Then test a 4 byte incomplete character + $utf8 = str_repeat("41", 8186); + $utf8 = $utf8 . "f1a680bf"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + + // Finally, verify error conditions with invalid inputs + $error = '*An error occurred translating a PHP stream from UTF-8 to UTF-16:*'; + + // First test UTF-8 cutoff (really cutoff) + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e383"; + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + try { + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + echo "Should have failed with a cutoff UTF-8 string\n"; + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + // Then test UTF-8 invalid/corrupt stream + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395e38395"; + $utf8 = substr_replace($utf8, "fe", 1000, 2); + $utf8 = pack("H*", $utf8); + $f4 = fopen("data://text/plain," . $utf8, "r"); + try { + $stmt->bindParam(4, $f4, PDO::PARAM_LOB); + $stmt->execute(); + echo "Should have failed with an invalid UTF-8 string\n"; + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } + + echo "Done\n"; + + // Done testing with stored procedures and table + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt new file mode 100644 index 00000000..acbeaf23 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_insert_fetch_utf8text.phpt @@ -0,0 +1,233 @@ +--TEST-- +Test inserting and retrieving UTF-8 text +--DESCRIPTION-- +This is similar to sqlsrv 0065.phpt with checking for error conditions concerning encoding issues. +--SKIPIF-- + +--FILE-- + 0) { + if ($results[$i] !== $utf8) { + echo $columns[$i]->colName . ' does not match the inserted UTF-8 text'; + var_dump($results[$i]); + } + } else { + // The first column, a varchar(100) column, should have question marks, + // like this one: + $expected = "So?e sä???? ?SCII-te×t"; + // With AE, the fetched result may be different in Windows and other + // platforms -- the point is to check if there are some '?' + if (!isAEConnected() && $results[$i] !== $expected) { + echo $columns[$i]->colName . " does not match $expected"; + var_dump($results[$i]); + } else { + $arr = explode('?', $results[$i]); + if (count($arr) == 1) { + // this means there is no question mark in $t + echo $columns[$i]->colName . " value is unexpected"; + var_dump($results[$i]); + } + } + } + } +} + +function dropProcedures($conn) +{ + // Drop all procedures + dropProc($conn, "pdoIntDoubleProc"); + dropProc($conn, "pdoUTF8OutProc"); + dropProc($conn, "pdoUTF8OutWithResultsetProc"); + dropProc($conn, "pdoUTF8InOutProc"); +} + +function createProcedures($conn, $tableName) +{ + // Drop all procedures first + dropProcedures($conn); + + $createProc = <<query($createProc); + + $createProc = "CREATE PROCEDURE pdoUTF8OutWithResultsetProc @param NVARCHAR(25) OUTPUT AS BEGIN SELECT c1, c2, c3 FROM $tableName SET @param = CONVERT(NVARCHAR(25), 0x5E01A1013C04170120005B01E400DD1040044001C11E200086035A010801280130012D0065012E21D7006701); END"; + $stmt = $conn->query($createProc); + + $createProc = "CREATE PROCEDURE pdoUTF8InOutProc @param NVARCHAR(25) OUTPUT AS BEGIN SET @param = CONVERT(NVARCHAR(25), 0x6001E11EDD10130120006101E200DD1040043A01BB1E2000C5005A01C700CF0007042D006501BF1E45046301); END"; + $stmt = $conn->query($createProc); + + $createProc = "CREATE PROCEDURE pdoIntDoubleProc @param INT OUTPUT AS BEGIN SET @param = @param + @param; END;"; + $stmt = $conn->query($createProc); +} + +function runBaselineProc($conn) +{ + $sql = "{call pdoIntDoubleProc(?)}"; + $val = 1; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_INT | PDO::PARAM_INPUT_OUTPUT, 100); + $stmt->execute(); + + if ($val !== 2) { + echo "Incorrect value $val for pdoIntDoubleProc\n"; + } +} + +function runImmediateConversion($conn, $utf8) +{ + $sql = "{call pdoUTF8OutProc(?)}"; + $val = ''; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR, 50); + $stmt->execute(); + + if ($val !== $utf8) { + echo "Incorrect value $val for pdoUTF8OutProc\n"; + } +} + +function runProcWithResultset($conn, $utf8) +{ + $sql = "{call pdoUTF8OutWithResultsetProc(?)}"; + $val = ''; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR, 50); + $stmt->execute(); + + // Moves the cursor to the next result set + $stmt->nextRowset(); + + if ($val !== $utf8) { + echo "Incorrect value $val for pdoUTF8OutWithResultsetProc\n"; + } +} + +function runInOutProcWithErrors($conn, $utf8_2) +{ + // The input string is smaller than the output size for testing + $val = 'This is a test.'; + + // The following should work + $sql = "{call pdoUTF8InOutProc(?)}"; + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 25); + $stmt->execute(); + + if ($val !== $utf8_2) { + echo "Incorrect value $val for pdoUTF8InOutProc Part 1\n"; + } + + // Use a much longer input string + $val = 'This is a longer test that exceeds the returned values buffer size so that we can test an input buffer size larger than the output buffer size.'; + try { + $stmt->bindParam(1, $val, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 25); + $stmt->execute(); + echo "Should have failed since the string is too long!\n"; + } catch (PDOException $e) { + $error = '*String data, right truncation'; + if ($e->getCode() !== "22001" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function runIntDoubleProcWithErrors($conn) +{ + $sql = "{call pdoIntDoubleProc(?)}"; + $val = pack('H*', 'ffffffff'); + + try { + $stmt = $conn->prepare($sql); + $stmt->bindParam(1, $val, PDO::PARAM_STR); + $stmt->execute(); + echo "Should have failed because of an invalid utf-8 string!\n"; + } catch (PDOException $e) { + $error = '*An error occurred translating string for input param 1 to UCS-2:*'; + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + // Create test table + $tableName = 'pdoUTF8test'; + $columns = array(new ColumnMeta('varchar(100)', 'c1'), + new ColumnMeta('nvarchar(100)', 'c2'), + new ColumnMeta('nvarchar(max)', 'c3')); + $stmt = createTable($conn, $tableName, $columns); + + $utf8 = "Şơмė śäოрŀề ΆŚĈĨİ-ť℮×ŧ"; + + $insertSql = "INSERT INTO $tableName (c1, c2, c3) VALUES (?, ?, ?)"; + $stmt1 = $conn->prepare($insertSql); + $stmt1->bindParam(1, $utf8); + $stmt1->bindParam(2, $utf8); + $stmt1->bindParam(3, $utf8); + + $stmt1->execute(); + + $stmt2 = $conn->prepare("SELECT c1, c2, c3 FROM $tableName"); + $stmt2->execute(); + $results = $stmt2->fetch(PDO::FETCH_NUM); + verifyColumnData($columns, $results, $utf8); + + // Start creating stored procedures for testing + createProcedures($conn, $tableName); + + runBaselineProc($conn); + runImmediateConversion($conn, $utf8); + runProcWithResultset($conn, $utf8); + + // Use another set of UTF-8 text to test + $utf8_2 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; + runInOutProcWithErrors($conn, $utf8_2); + + // Now insert second row + $utf8_3 = pack('H*', '7a61cc86c7bdceb2f18fb3bf'); + $stmt1->bindParam(1, $utf8_3); + $stmt1->bindParam(2, $utf8_3); + $stmt1->bindParam(3, $utf8_3); + $stmt1->execute(); + + // Fetch data, ignoring first row + $stmt2->execute(); + $stmt2->fetch(PDO::FETCH_NUM); + + // Move to the second row and check second field + $results2 = $stmt2->fetch(PDO::FETCH_NUM); + if ($results2[1] !== $utf8_3) { + echo "Unexpected $results2[1] from field 2 in second row.\n"; + } + + // Last test with an invalid input + runIntDoubleProcWithErrors($conn); + + echo "Done\n"; + + // Done testing with stored procedures and table + dropProcedures($conn); + dropTable($conn, $tableName); + + unset($stmt1); + unset($stmt2); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt b/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt index b4512c31..3cdee20c 100644 --- a/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt +++ b/test/functional/pdo_sqlsrv/pdo_output_decimal.phpt @@ -42,7 +42,7 @@ try { $expected1 = "7.4"; $expected2 = "7"; if ($outValue1 == $expected1 && $outValue2 == $expected2) { - echo "Test Successfully\n"; + echo "Test Successful\n"; } dropProc($conn, $proc_scale); @@ -56,4 +56,4 @@ try { ?> --EXPECT-- -Test Successfully +Test Successful diff --git a/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt b/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt new file mode 100644 index 00000000..5ec6c6fb --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_output_decimal_errors.phpt @@ -0,0 +1,128 @@ +--TEST-- +Call stored procedures with inputs of different datatypes to get outputs of various types +--DESCRIPTION-- +Similar to pdo_output_decimal.phpt but this time intentionally test some error cases +--SKIPIF-- + +--FILE-- +prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR, -1); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Invalid size for output string parameter 3. Input/output string parameters must have an explicit length.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function testInvalidDirection($conn, $proc) +{ + global $inValue1, $inValue2, $outValue1; + + // Request input output parameter but do not provide a size + try { + $stmt = $conn->prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Invalid direction specified for parameter 3. Input/output parameters must have a length.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +function testInvalidType($conn, $proc) +{ + global $inValue1, $inValue2; + + $outValue = 0.3; + + // Pass an invalid type that is incompatible for the output parameter + try { + $stmt = $conn->prepare("{CALL $proc (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue, PDO::PARAM_BOOL | PDO::PARAM_INPUT_OUTPUT, 1024); + $stmt->execute(); + } catch (PDOException $e) { + $error = '*Types for parameter value and PDO::PARAM_* constant must be compatible for input/output parameter 3.'; + + if (!fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + $proc_scale = getProcName('scale_proc'); + $proc_no_scale = getProcName('noScale_proc'); + + $stmt = $conn->exec("CREATE PROC $proc_scale (@p1 DECIMAL(18, 1), @p2 DECIMAL(18, 1), @p3 CHAR(128) OUTPUT) + AS BEGIN SELECT @p3 = CONVERT(CHAR(128), @p1 + @p2) END"); + + $inValue1 = '2.1'; + $inValue2 = '5.3'; + $outValue1 = '0'; + $outValue2 = '0'; + + // First error case: pass an invalid size for the output parameter + testInvalidSize($conn, $proc_scale); + testInvalidDirection($conn, $proc_scale); + testInvalidType($conn, $proc_scale); + + $stmt = $conn->prepare("{CALL $proc_scale (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue1, PDO::PARAM_STR, 300); + $stmt->execute(); + + $outValue1 = trim($outValue1); + + $stmt = $conn->exec("CREATE PROC $proc_no_scale (@p1 DECIMAL, @p2 DECIMAL, @p3 CHAR(128) OUTPUT) + AS BEGIN SELECT @p3 = CONVERT(CHAR(128), @p1 + @p2) END"); + + $stmt = $conn->prepare("{CALL $proc_no_scale (?, ?, ?)}"); + $stmt->bindValue(1, $inValue1); + $stmt->bindValue(2, $inValue2); + $stmt->bindParam(3, $outValue2, PDO::PARAM_STR, 300); + $stmt->execute(); + + $outValue2 = trim($outValue2); + + $expected1 = "7.4"; + $expected2 = "7"; + if ($outValue1 == $expected1 && $outValue2 == $expected2) { + echo "Test Successfully done\n"; + } + + dropProc($conn, $proc_scale); + dropProc($conn, $proc_no_scale); + + unset($stmt); + unset($conn); +} catch (Exception $e) { + echo $e->getMessage(); +} +?> + +--EXPECT-- +Test Successfully done diff --git a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt new file mode 100644 index 00000000..cd709eb1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt @@ -0,0 +1,78 @@ +--TEST-- +Test reading non LOB types +--DESCRIPTION-- +A simpler version of sqlsrv test "test_sqlsrv_phptype_stream.phpt" for reading from +pre-populated tables [test_streamable_types] and [test_types] +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // test the allowed non LOB column types + $tsql = "SELECT [char_short_type], [varchar_short_type], [nchar_short_type], [nvarchar_short_type], [binary_short_type], [varbinary_short_type] FROM [test_streamable_types]"; + $stmt = $conn->query($tsql); + + $result = $stmt->fetch(PDO::FETCH_NUM); + verifyResult($result); + + // test not streamable types + $tsql = "SELECT * FROM [test_types]"; + $stmt = $conn->query($tsql); + $result = $stmt->fetch(PDO::FETCH_NUM); + print_r($result); + +} catch (PDOException $e) { + var_dump($e->errorInfo); +} + +unset($stmt); +unset($conn); + +?> +--EXPECT-- +Array +( + [0] => 9223372036854775807 + [1] => 2147483647 + [2] => 32767 + [3] => 255 + [4] => 1 + [5] => 9999999999999999999999999999999999999 + [6] => 922337203685477.5807 + [7] => 214748.3647 + [8] => 1.79E+308 + [9] => 1.1799999E-38 + [10] => 1968-12-12 16:20:00.000 + [11] => +) \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt b/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt index a5c31e09..0c4f88f8 100644 --- a/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt +++ b/test/functional/pdo_sqlsrv/pdo_utf8_conn.phpt @@ -24,6 +24,6 @@ if ($c !== false) { Fatal error: Uncaught PDOException: SQLSTATE\[(28000|08001|HYT00)\]: .*\[Microsoft\]\[ODBC Driver 1[0-9] for SQL Server\](\[SQL Server\])?(Named Pipes Provider: Could not open a connection to SQL Server \[2\]\. |TCP Provider: Error code (0x2726|0x2AF9)|Login timeout expired|Login failed for user 'sa'\.) in .+(\/|\\)pdo_utf8_conn\.php:[0-9]+ Stack trace: -#0 .+(\/|\\)pdo_utf8_conn\.php\([0-9]+\): PDO->__construct\('sqlsrv:Server=l\.\.\.', 'sa', 'Sunshine4u'\) +#0 .+(\/|\\)pdo_utf8_conn\.php\([0-9]+\): PDO->__construct(\(\)|\('sqlsrv:Server=l\.\.\.', 'sa', 'Sunshine4u'\)) #1 {main} thrown in .+(\/|\\)pdo_utf8_conn\.php on line [0-9]+ diff --git a/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt b/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt new file mode 100644 index 00000000..d92da15d --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_warning_errors.phpt @@ -0,0 +1,107 @@ +--TEST-- +Test various scenarios which all return the same error about statement not executed +--DESCRIPTION-- +This is similar to sqlsrv test_warning_errors2.phpt with checking for error conditions concerning fetching and metadata. +--SKIPIF-- + +--FILE-- +nextRowset(); + if (!is_null($error)) { + echo "getNextResult: expect this to fail with an error from the driver\n"; + } elseif ($result !== false) { + echo "getNextResult: expect this to simply return false\n"; + } + } catch (PDOException $e) { + if ($e->getCode() !== "IMSSP" || !fnmatch($error, $e->getMessage())) { + var_dump($e->getMessage()); + } + } +} + +try { + $conn = connect(); + + $tsql = 'SELECT name FROM sys.objects'; + $stmt = $conn->prepare($tsql); + + $colCount = $stmt->columnCount(); + if ($colCount != 0) { + echo "Before execute(), result set should only have 0 columns\n"; + } + $metadata = $stmt->getColumnMeta(0); + if ($metadata !== false) { + echo "Before execute(), result set is empty so getColumnMeta should have failed\n"; + } + + // When fetching, PDO checks if statement is executed before passing the + // control to the driver, so it simply fails without error message + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if ($result !== false) { + echo "Before execute(), fetch should have failed\n"; + } + + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + var_dump($result); + + $result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); + var_dump($result); + + getNextResult($stmt, $execError); + + // Now, call execute() + $stmt->execute(); + + $colCount = $stmt->columnCount(); + if ($colCount != 1) { + echo "Expected only one column\n"; + } + + $metadata = $stmt->getColumnMeta(0); + if ($metadata['native_type'] !== 'string') { + echo "The metadata returned is unexpected: \n"; + var_dump($metadata); + } + + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if (!is_array($result) && count($result) == 0) { + echo "After execute(), fetch should have returned an array with results\n"; + } + + $result = $stmt->fetchAll(PDO::FETCH_COLUMN, 0); + if (!is_array($result) && count($result) == 0) { + echo "After execute(), fetchAll should have returned an array with results\n"; + } + + getNextResult($stmt); + + getNextResult($stmt, $noMoreResult); + + $result = $stmt->fetch(PDO::FETCH_ASSOC); + if ($result !== false) { + // When nextRowset() fails, it resets the execute flag to false + echo "After nextRowset failed, fetch should have failed\n"; + } + + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +array(0) { +} +array(0) { +} +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt index 62bc542d..5f1a4f43 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt @@ -431,6 +431,6 @@ Test_9 : FETCH_INVALID : Fatal error: Uncaught Error: Undefined class constant 'FETCH_UNKNOWN' in %s:%x Stack trace: -#0 %s: fetchAllInvalid(Object(PDO), 'PDO_MainTypes') +#0 %s: fetchAllInvalid(%S) #1 {main} thrown in %s on line %x diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt index b8fd577a..a14d6dde 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetch_orientation.phpt @@ -35,7 +35,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_PRIOR ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -53,7 +53,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, -1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -63,7 +63,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -73,7 +73,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, 1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -99,7 +99,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_ABS, -1 ); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -125,7 +125,7 @@ try { throw new Exception( "Not C" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -135,7 +135,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_PRIOR); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } @@ -145,7 +145,7 @@ try { throw new Exception( "Not A" ); } $row = $stmt1->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_REL, -1); - if( $row[ 'val' ] != false ) { + if ($row !== false) { throw new Exception( "Not false" ); } diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt index 000931b6..5170bd97 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetch_style.phpt @@ -257,6 +257,6 @@ Test_9 : FETCH_INVALID : Fatal error: Uncaught Error: Undefined class constant 'FETCH_UNKNOWN' in %s:%x Stack trace: -#0 %s: fetchWithStyle(Object(PDO), 'PDO_MainTypes', 'PDO::FETCH_INVA...') +#0 %s: fetchWithStyle(%S) #1 {main} thrown in %s on line %x diff --git a/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt b/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt index c02d634a..1f1d341a 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_format_money_types.phpt @@ -64,17 +64,9 @@ function testFloatTypes($conn, $numDigits) trace("testFloatTypes: $floatVal1, $floatVal\n"); - // Check if the numbers of decimal digits are the same - // It is highly unlikely but not impossible - $numbers = explode('.', $floatStr); - $len = strlen($numbers[1]); - if ($len == $numDigits && $floatVal1 != $floatVal) { - echo "Expected $floatVal but $floatVal1 returned. \n"; - } else { - $diff = abs($floatVal1 - $floatVal) / $floatVal; - if ($diff > $epsilon) { - echo "$diff: Expected $floatVal but $floatVal1 returned. \n"; - } + $diff = abs($floatVal1 - $floatVal) / $floatVal; + if ($diff > $epsilon) { + echo "$diff: Expected $floatVal but $floatVal1 returned. \n"; } } } diff --git a/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt b/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt index af341baa..087ce296 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_get_set_attr.phpt @@ -1,5 +1,5 @@ --TEST-- -Test setting and getting various statement attributes. +Test setting and getting various statement attributes with error verifications. --SKIPIF-- --FILE-- @@ -8,40 +8,30 @@ Test setting and getting various statement attributes. function set_stmt_option($conn, $arr) { try { - - $stmt = $conn->prepare( "Select * from temptb", $arr ); + $stmt = $conn->prepare("Select * from temptb", $arr); return $stmt; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - return NULL; - } + return null; + } } function set_stmt_attr($conn, $attr, $val) { - $stmt = NULL; - try - { + $stmt = null; + try { echo "Set Attribute: " . $attr . "\n"; - $stmt = $conn->prepare( "Select * from temptb"); - } - catch( PDOException $e) - { + $stmt = $conn->prepare("Select * from temptb"); + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - return NULL; + return null; } try { $res = $stmt->setAttribute(constant($attr), $val); var_dump($res); echo "\n\n"; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; } return $stmt; @@ -49,27 +39,23 @@ function set_stmt_attr($conn, $attr, $val) function get_stmt_attr($stmt, $attr) { - try - { + try { echo "Get Attribute: " . $attr. "\n"; $res = $stmt->getAttribute(constant($attr)); var_dump($res); echo "\n"; - } - - catch( PDOException $e) - { + } catch (PDOException $e) { echo $e->getMessage() . "\n\n"; - } + } } // valid function Test1($conn) -{ - echo "Test1 - Set stmt option: SQLSRV_ATTR_ENCODING, ATTR_CURSOR, SQLSRV_ATTR_QUERY_TIMEOUT \n"; - set_stmt_option($conn, array(PDO::SQLSRV_ATTR_ENCODING => 3, PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 44)); - echo "Test Successful\n\n"; -} + { + echo "Test1 - Set stmt option: SQLSRV_ATTR_ENCODING, ATTR_CURSOR, SQLSRV_ATTR_QUERY_TIMEOUT \n"; + set_stmt_option($conn, array(PDO::SQLSRV_ATTR_ENCODING => 3, PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY, PDO::SQLSRV_ATTR_QUERY_TIMEOUT => 44)); + echo "Test Successful\n\n"; + } // invalid function Test2($conn) @@ -84,10 +70,11 @@ function Test3($conn) echo "Test3 \n"; $attr = "PDO::ATTR_CURSOR"; $stmt = set_stmt_attr($conn, $attr, 1); - if($stmt) + if ($stmt) { get_stmt_attr($stmt, $attr); - else + } else { echo "Test3: stmt was null"; + } } // not supported attribute @@ -117,16 +104,33 @@ function Test6($conn) $attr = "PDO::SQLSRV_ATTR_QUERY_TIMEOUT"; $stmt = set_stmt_attr($conn, $attr, 45); get_stmt_attr($stmt, $attr); - } - -try -{ +// PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED or invalid option +// Expect errors +function Test7($conn) +{ + echo "Test7 - Set stmt option: SQLSRV_ATTR_CURSOR_SCROLL_TYPE \n"; + set_stmt_option($conn, array(PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + + // pass an invalid option + set_stmt_option($conn, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => 10)); +} + +// PDO::SQLSRV_ATTR_DIRECT_QUERY as statement attribute +// Expect error +function Test8($conn) +{ + echo "Test8 - Set stmt attr: SQLSRV_ATTR_DIRECT_QUERY \n"; + $attr = "PDO::SQLSRV_ATTR_DIRECT_QUERY"; + $stmt = set_stmt_attr($conn, $attr, true); +} + +try { include("MsSetup.inc"); - $conn = new PDO( "sqlsrv:Server=$server; Database = $databaseName ", $uid, $pwd); - $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION ); + $conn = new PDO("sqlsrv:Server=$server; Database = $databaseName ", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $conn->exec("IF OBJECT_ID('temptb', 'U') IS NOT NULL DROP TABLE temptb"); $conn->exec("CREATE TABLE temptb(id INT NOT NULL PRIMARY KEY, val VARCHAR(10)) "); @@ -136,12 +140,12 @@ try test4($conn); test5($conn); test6($conn); - -} - -catch( PDOException $e ) { - - var_dump( $e ); + test7($conn); + test8($conn); + + $conn->exec("DROP TABLE temptb"); +} catch (PDOException $e) { + var_dump($e); exit; } @@ -181,4 +185,12 @@ bool\(true\) Get Attribute: PDO::SQLSRV_ATTR_QUERY_TIMEOUT -int\(45\) \ No newline at end of file +int\(45\) + +Test7 - Set stmt option: SQLSRV_ATTR_CURSOR_SCROLL_TYPE +SQLSTATE\[IMSSP\]: The PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE attribute may only be set when PDO::ATTR_CURSOR is set to PDO::CURSOR_SCROLL in the $driver_options array of PDO::prepare. + +SQLSTATE\[IMSSP\]: The value passed for the 'Scrollable' statement option is invalid. + +Test8 - Set stmt attr: SQLSRV_ATTR_DIRECT_QUERY +SQLSTATE\[IMSSP\]: The PDO::SQLSRV_ATTR_DIRECT_QUERY attribute may only be set in the $driver_options array of PDO::prepare. \ No newline at end of file diff --git a/test/functional/setup/build_ksp.py b/test/functional/setup/build_ksp.py deleted file mode 100644 index fb524719..00000000 --- a/test/functional/setup/build_ksp.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/python3 -######################################################################################### -# -# Description: This script builds a custom keystore provider and compiles the app that -# uses this KSP. Their names can be passed as arguments, but the outputs -# are always -# - myKSP.dll (myKSPx64.dll) / myKSP.so -# - ksp_app.exe / ksp_app -# -# Requirement: -# python 3.x -# myKSP.c (or any equivalent) -# ksp_app.c (or any equivalent) -# msodbcsql.h (odbc header file) -# -# Execution: Run with command line with optional options -# py build_ksp.py --KSP myKSP --APP ksp_app -# -############################################################################################# - -import sys -import os -import platform -import argparse - -# This creates a batch *filename*, which compiles a C program according to -# *command* and *arch* (either x86 or x64) -def create_batch_file(arch, filename, command): - root_dir = 'C:' + os.sep - vcvarsall = os.path.join(root_dir, "Program Files (x86)", "Microsoft Visual Studio 14.0", "VC", "vcvarsall.bat") - - try: - file = open(filename, 'w') - file.write('@ECHO OFF' + os.linesep) - if arch == 'x64': - file.write('@CALL "' + vcvarsall + '" amd64' + os.linesep) - else: - file.write('@CALL "' + vcvarsall + '" x86' + os.linesep) - - # compile the code - file.write('@CALL ' + command + os.linesep) - file.close() - except: - print('Cannot create ', filename) - -# This invokes the newly created batch file to compile the code, -# according to *arch* (either x86 or x64). The batch file will be -# removed afterwards -def compile_KSP_windows(arch, ksp_src): - output = 'myKSP' - if arch == 'x64': - output = output + arch + '.dll' - else: - output = output + '.dll' - - command = 'cl {0} /LD /MD /link /out:'.format(ksp_src) + output - batchfile = 'build_KSP.bat' - create_batch_file(arch, batchfile, command) - os.system(batchfile) - os.remove(batchfile) - -# This compiles myKSP.c -# -# In Windows, this will create batch files to compile two dll(s). -# Otherwise, this will compile the code and generate a .so file. -# -# Output: A custom keystore provider created -def compile_KSP(ksp_src): - print('Compiling ', ksp_src) - if platform.system() == 'Windows': - compile_KSP_windows('x64', ksp_src) - compile_KSP_windows('x86', ksp_src) - else: - os.system('gcc -fshort-wchar -fPIC -o myKSP.so -shared {0}'.format(ksp_src)) - -# This compiles ksp app, which assumes the existence of the .dll or the .so file. -# -# In Windows, a batch file is created in order to compile the code. -def configure_KSP(app_src): - print('Compiling ', app_src) - if platform.system() == 'Windows': - command = 'cl /MD {0} /link odbc32.lib /out:ksp_app.exe'.format(app_src) - batchfile = 'build_app.bat' - create_batch_file('x86', batchfile, command) - os.system(batchfile) - os.remove(batchfile) - else: - os.system('gcc -o ksp_app -fshort-wchar {0} -lodbc -ldl'.format(app_src)) - -################################### Main Function ################################### -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-ksp', '--KSPSRC', default='myKSP.c', help='The source file of KSP (keystore provider)') - parser.add_argument('-app', '--APPSRC', default='ksp_app.c', help='The source file for the app that uses the KSP') - args = parser.parse_args() - - ksp_src = args.KSPSRC - app_src = args.APPSRC - header = 'msodbcsql.h' - - cwd = os.getcwd() - - # make sure all required source and header files are present - work_dir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(work_dir) - - if not os.path.exists(os.path.join(work_dir, header)): - print('Error: {0} not found!'.format(header)) - exit(1) - if not os.path.exists(os.path.join(work_dir, ksp_src)): - print('Error: {0}.c not found!'.format(ksp_src)) - exit(1) - if not os.path.exists(os.path.join(work_dir, app_src)): - print('Error: {0}.c not found!'.format(app_src)) - exit(1) - - compile_KSP(ksp_src) - configure_KSP(app_src) - - os.chdir(cwd) - - \ No newline at end of file diff --git a/test/functional/setup/ksp_app.c b/test/functional/setup/ksp_app.c deleted file mode 100644 index 86107802..00000000 --- a/test/functional/setup/ksp_app.c +++ /dev/null @@ -1,305 +0,0 @@ -/****************************************************************************** - Example application for demonstration of custom keystore provider usage - - Windows: compile with cl /MD ksp_app.c /link odbc32.lib /out:ksp_app.exe - Linux/mac: compile with gcc -o ksp_app -fshort-wchar ksp_app.c -lodbc -ldl - - usage: kspapp connstr - - ******************************************************************************/ - -#define KSPNAME L"MyCustomKSPName" -#define PROV_ENCRYPT_KEY "LPKCWVD07N3RG98J0MBLG4H2" /* this can be any character string */ - -#include -#include -#ifdef _WIN32 -#include -#else -#define __stdcall -#include -#endif -#include -#include -#include "msodbcsql.h" - -enum job { - set_up = 0, - clean_up = 1 -}; - -/* Convenience functions */ - -int checkRC(SQLRETURN rc, char *msg, int ret, SQLHANDLE h, SQLSMALLINT ht) { - if (rc == SQL_ERROR) { - fprintf(stderr, "Error occurred upon %s\n", msg); - if (h) { - SQLSMALLINT i = 0; - SQLSMALLINT outlen = 0; - char errmsg[1024]; - while ((rc = SQLGetDiagField( - ht, h, ++i, SQL_DIAG_MESSAGE_TEXT, errmsg, sizeof(errmsg), &outlen)) == SQL_SUCCESS - || rc == SQL_SUCCESS_WITH_INFO) { - fprintf(stderr, "Err#%d: %s\n", i, errmsg); - } - } - if (ret) - exit(ret); - return 0; - } - else if (rc == SQL_SUCCESS_WITH_INFO && h) { - SQLSMALLINT i = 0; - SQLSMALLINT outlen = 0; - char errmsg[1024]; - printf("Success with info for %s:\n", msg); - while ((rc = SQLGetDiagField( - ht, h, ++i, SQL_DIAG_MESSAGE_TEXT, errmsg, sizeof(errmsg), &outlen)) == SQL_SUCCESS - || rc == SQL_SUCCESS_WITH_INFO) { - fprintf(stderr, "Msg#%d: %s\n", i, errmsg); - } - } - return 1; -} - -void postKspError(CEKEYSTORECONTEXT *ctx, const wchar_t *msg, ...) { - if (msg > (wchar_t*)65535) - wprintf(L"Provider emitted message: %s\n", msg); - else - wprintf(L"Provider emitted message ID %d\n", msg); -} - -int setKSPLibrary(SQLHSTMT stmt) { - unsigned char CEK[32]; - unsigned char *ECEK; - unsigned short ECEKlen; - unsigned char foundProv = 0; - int i; -#ifdef _WIN32 - HMODULE hProvLib; -#else - void *hProvLib; -#endif - CEKEYSTORECONTEXT ctx = {0}; - CEKEYSTOREPROVIDER **ppKsp, *pKsp; - int(__stdcall *pEncryptCEK)(CEKEYSTORECONTEXT *, errFunc *, unsigned char *, unsigned short, unsigned char **, unsigned short *); - - /* Load the provider library */ -#ifdef _WIN32 - if (!(hProvLib = LoadLibrary("myKSP.dll"))) { -#else - if (!(hProvLib = dlopen("./myKSP.so", RTLD_NOW))) { -#endif - fprintf(stderr, "Error loading KSP library\n"); - return 2; - } -#ifdef _WIN32 - if (!(ppKsp = (CEKEYSTOREPROVIDER**)GetProcAddress(hProvLib, "CEKeystoreProvider"))) { -#else - if (!(ppKsp = (CEKEYSTOREPROVIDER**)dlsym(hProvLib, "CEKeystoreProvider"))) { -#endif - fprintf(stderr, "The export CEKeystoreProvider was not found in the KSP library\n"); - return 3; - } - while (pKsp = *ppKsp++) { - if (!memcmp(KSPNAME, pKsp->Name, sizeof(KSPNAME))) { - foundProv = 1; - break; - } - } - if (! foundProv) { - fprintf(stderr, "Could not find provider in the library\n"); - return 4; - } - - if (pKsp->Init && !pKsp->Init(&ctx, postKspError)) { - fprintf(stderr, "Could not initialize provider\n"); - return 5; - } -#ifdef _WIN32 - if (!(pEncryptCEK = (LPVOID)GetProcAddress(hProvLib, "KeystoreEncrypt"))) { -#else - if (!(pEncryptCEK = dlsym(hProvLib, "KeystoreEncrypt"))) { -#endif - fprintf(stderr, "The export KeystoreEncrypt was not found in the KSP library\n"); - return 6; - } - if (!pKsp->Write) { - fprintf(stderr, "Provider does not support configuration\n"); - return 7; - } - - /* Configure the provider with the key */ - if (!pKsp->Write(&ctx, postKspError, PROV_ENCRYPT_KEY, strlen(PROV_ENCRYPT_KEY))) { - fprintf(stderr, "Error writing to KSP\n"); - return 8; - } - - /* Generate a CEK and encrypt it with the provider */ - srand(time(0) ^ getpid()); - for (i = 0; i < sizeof(CEK); i++) - CEK[i] = rand(); - - if (!pEncryptCEK(&ctx, postKspError, CEK, sizeof(CEK), &ECEK, &ECEKlen)) { - fprintf(stderr, "Error encrypting CEK\n"); - return 9; - } - - /* Create a CMK definition on the server */ - { - static char cmkSql[] = "CREATE COLUMN MASTER KEY CustomCMK WITH (" - "KEY_STORE_PROVIDER_NAME = 'MyCustomKSPName'," - "KEY_PATH = 'TheOneAndOnlyKey')"; - printf("Create CMK: %s\n", cmkSql); - SQLExecDirect(stmt, cmkSql, SQL_NTS); - } - - /* Create a CEK definition on the server */ - { - const char cekSqlBefore[] = "CREATE COLUMN ENCRYPTION KEY CustomCEK WITH VALUES (" - "COLUMN_MASTER_KEY = CustomCMK," - "ALGORITHM = 'none'," - "ENCRYPTED_VALUE = 0x"; - char *cekSql = malloc(sizeof(cekSqlBefore) + 2 * ECEKlen + 2); /* 1 for ')', 1 for null terminator */ - strcpy(cekSql, cekSqlBefore); - for (i = 0; i < ECEKlen; i++) - sprintf(cekSql + sizeof(cekSqlBefore) - 1 + 2 * i, "%02x", ECEK[i]); - strcat(cekSql, ")"); - printf("Create CEK: %s\n", cekSql); - SQLExecDirect(stmt, cekSql, SQL_NTS); - free(cekSql); -#ifdef _WIN32 - LocalFree(ECEK); -#else - free(ECEK); -#endif - } - -#ifdef _WIN32 - FreeLibrary(hProvLib); -#else - dlclose(hProvLib); -#endif - - return 0; -} - -void populateTestTable(SQLHDBC dbc, SQLHSTMT stmt) -{ - SQLRETURN rc; - int i, j; - - /* Create a table with encrypted columns */ - { - static char *tableSql = "CREATE TABLE CustomKSPTestTable (" - "c1 int," - "c2 varchar(255) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256')," - "c3 char(5) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256')," - "c4 date ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = CustomCEK, ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'))"; - printf("Create table: %s\n", tableSql); - SQLExecDirect(stmt, tableSql, SQL_NTS); - } - - /* Load provider into the ODBC Driver and configure it */ - { - unsigned char ksd[sizeof(CEKEYSTOREDATA) + sizeof(PROV_ENCRYPT_KEY) - 1]; - CEKEYSTOREDATA *pKsd = (CEKEYSTOREDATA*)ksd; - pKsd->name = L"MyCustomKSPName"; - pKsd->dataSize = sizeof(PROV_ENCRYPT_KEY) - 1; - memcpy(pKsd->data, PROV_ENCRYPT_KEY, sizeof(PROV_ENCRYPT_KEY) - 1); -#ifdef _WIN32 - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREPROVIDER, "myKSP.dll", SQL_NTS); -#else - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREPROVIDER, "./myKSP.so", SQL_NTS); -#endif - checkRC(rc, "Loading KSP into ODBC Driver", 7, dbc, SQL_HANDLE_DBC); - rc = SQLSetConnectAttr(dbc, SQL_COPT_SS_CEKEYSTOREDATA, (SQLPOINTER)pKsd, SQL_IS_POINTER); - checkRC(rc, "Configuring the KSP", 7, dbc, SQL_HANDLE_DBC); - } - - /* Insert some data */ - { - int c1; - char c2[256]; - char c3[6]; - SQL_DATE_STRUCT date; - SQLLEN cbdate; - rc = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_LONG, SQL_INTEGER, 0, 0, &c1, 0, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - rc = SQLBindParameter(stmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_VARCHAR, 255, 0, c2, 255, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - rc = SQLBindParameter(stmt, 3, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 5, 0, c3, 5, 0); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - cbdate = sizeof(SQL_DATE_STRUCT); - rc = SQLBindParameter(stmt, 4, SQL_PARAM_INPUT, SQL_C_TYPE_DATE, SQL_TYPE_DATE, 10, 0, &date, 0, &cbdate); - checkRC(rc, "Binding parameters for insert", 9, stmt, SQL_HANDLE_STMT); - - date.year = 2017; - date.month = 8; - for (i = 0; i < 10; i++) { - date.day = i + 10; - - c1 = i * 10 + i + 1; - sprintf(c2, "Sample data %d for column 2", i); - for (j = 0; j < 3; j++) { - c3[j] = 'a' + i + j; - } - c3[3] = '\0'; - rc = SQLExecDirect(stmt, "INSERT INTO CustomKSPTestTable (c1, c2, c3, c4) values (?, ?, ?, ?)", SQL_NTS); - checkRC(rc, "Inserting rows query", 10, stmt, SQL_HANDLE_STMT); - } - printf("(Encrypted) data has been inserted into CustomKSPTestTable. You may inspect the data now.\n"); - } - -} - -int main(int argc, char **argv) { - char sqlbuf[1024]; - SQLHENV env; - SQLHDBC dbc; - SQLHSTMT stmt; - SQLRETURN rc; - int i; - char connStr[1024]; - enum job task; - - if (argc < 6) { - fprintf(stderr, "usage: kspapp job server database uid pwd\n"); - return 1; - } - - task = atoi(argv[1]); - - sprintf(connStr, "DRIVER={ODBC Driver 17 for SQL Server};SERVER=%s;ColumnEncryption=Enabled;DATABASE=%s;UID=%s;PWD=%s", argv[2], argv[3], argv[4], argv[5]); - - /* Connect to Server */ - rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &env); - checkRC(rc, "allocating environment handle", 2, 0, 0); - rc = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); - checkRC(rc, "setting ODBC version to 3.0", 3, env, SQL_HANDLE_ENV); - rc = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); - checkRC(rc, "allocating connection handle", 4, env, SQL_HANDLE_ENV); - rc = SQLDriverConnect(dbc, 0, connStr, strlen(connStr), NULL, 0, NULL, SQL_DRIVER_NOPROMPT); - checkRC(rc, "connecting to data source", 5, dbc, SQL_HANDLE_DBC); - rc = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); - checkRC(rc, "allocating statement handle", 6, dbc, SQL_HANDLE_DBC); - - if (task == set_up) { - printf("Setting up KSP...\n"); - setKSPLibrary(stmt); - populateTestTable(dbc, stmt); - } - else if (task == clean_up) { - printf("Cleaning up KSP...\n"); - - SQLExecDirect(stmt, "DROP TABLE CustomKSPTestTable", SQL_NTS); - SQLExecDirect(stmt, "DROP COLUMN ENCRYPTION KEY CustomCEK", SQL_NTS); - SQLExecDirect(stmt, "DROP COLUMN MASTER KEY CustomCMK", SQL_NTS); - printf("Removed table, CEK, and CMK\n"); - } - - SQLDisconnect(dbc); - SQLFreeHandle(SQL_HANDLE_DBC, dbc); - SQLFreeHandle(SQL_HANDLE_ENV, env); - return 0; -} diff --git a/test/functional/setup/myKSP.c b/test/functional/setup/myKSP.c deleted file mode 100644 index d6188842..00000000 --- a/test/functional/setup/myKSP.c +++ /dev/null @@ -1,132 +0,0 @@ -/****************************************************************************** -Custom Keystore Provider Example - -Windows: compile with cl myKSP.c /LD /MD /link /out:myKSP.dll -Linux/mac: compile with gcc -fshort-wchar -fPIC -o myKSP.so -shared myKSP.c - -******************************************************************************/ - -#ifdef _WIN32 -#include -#else -#define __stdcall -#endif - -#define DEBUG 0 - -#include -#include -#include -#include -#include -#include "msodbcsql.h" - -int wcscmp_short(wchar_t *s1, wchar_t *s2) { - while(*s1 && *s2 && *s1 == *s2) - s1++, s2++; - return *s1 - *s2; -} - -int __stdcall KeystoreInit(CEKEYSTORECONTEXT *ctx, errFunc *onError) { - if (DEBUG) - printf("KSP Init() function called\n"); - return 1; -} - -static unsigned char *g_encryptKey; -static unsigned int g_encryptKeyLen; - -int __stdcall KeystoreWrite(CEKEYSTORECONTEXT *ctx, errFunc *onError, void *data, unsigned int len) { - if (DEBUG) - printf("KSP Write() function called (%d bytes)\n", len); - if (len) { - if (g_encryptKey) - free(g_encryptKey); - g_encryptKey = malloc(len); - if (!g_encryptKey) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - memcpy(g_encryptKey, data, len); - g_encryptKeyLen = len; - } - return 1; -} - -// Very simple "encryption" scheme - rotating XOR with the key -int __stdcall KeystoreDecrypt(CEKEYSTORECONTEXT *ctx, errFunc *onError, const wchar_t *keyPath, const wchar_t *alg, unsigned char *ecek, unsigned short ecekLen, unsigned char **cekOut, unsigned short *cekLen) { - unsigned int i; - if (DEBUG) - printf("KSP Decrypt() function called (keypath=%S alg=%S ecekLen=%u)\n", keyPath, alg, ecekLen); - if (wcscmp_short(keyPath, L"TheOneAndOnlyKey")) { - onError(ctx, L"Invalid key path"); - return 0; - } - if (wcscmp_short(alg, L"none")) { - onError(ctx, L"Invalid algorithm"); - return 0; - } - if (!g_encryptKey) { - onError(ctx, L"Keystore provider not initialized with key"); - return 0; - } -#ifndef _WIN32 - *cekOut = malloc(ecekLen); -#else - *cekOut = LocalAlloc(LMEM_FIXED, ecekLen); -#endif - if (!*cekOut) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - *cekLen = ecekLen; - for (i = 0; i < ecekLen; i++) - (*cekOut)[i] = ecek[i] ^ g_encryptKey[i % g_encryptKeyLen]; - return 1; -} - -// Note that in the provider interface, this function would be referenced via the CEKEYSTOREPROVIDER -// structure. However, that does not preclude keystore providers from exporting their own functions, -// as illustrated by this example where the encryption is performed via a separate function (with a -// different prototype than the one in the KSP interface.) -#ifdef _WIN32 -__declspec(dllexport) -#endif -int KeystoreEncrypt(CEKEYSTORECONTEXT *ctx, errFunc *onError, - unsigned char *cek, unsigned short cekLen, - unsigned char **ecekOut, unsigned short *ecekLen) { - unsigned int i; - - if (DEBUG) - printf("KSP Encrypt() function called (cekLen=%u)\n", cekLen); - if (!g_encryptKey) { - onError(ctx, L"Keystore provider not initialized with key"); - return 0; - } - *ecekOut = malloc(cekLen); - if (!*ecekOut) { - onError(ctx, L"Memory Allocation Error"); - return 0; - } - *ecekLen = cekLen; - for (i = 0; i < cekLen; i++) - (*ecekOut)[i] = cek[i] ^ g_encryptKey[i % g_encryptKeyLen]; - return 1; -} - -CEKEYSTOREPROVIDER MyCustomKSPName_desc = { - L"MyCustomKSPName", - KeystoreInit, - 0, - KeystoreWrite, - KeystoreDecrypt, - 0 -}; - -#ifdef _WIN32 -__declspec(dllexport) -#endif -CEKEYSTOREPROVIDER *CEKeystoreProvider[] = { - &MyCustomKSPName_desc, - 0 -}; \ No newline at end of file diff --git a/test/functional/setup/run_ksp.py b/test/functional/setup/run_ksp.py deleted file mode 100644 index 0d0ff897..00000000 --- a/test/functional/setup/run_ksp.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python3 -######################################################################################### -# -# Description: This script assumes the existence of the ksp_app executable and will -# invoke it to create / remove the Column Master Key, the Column Encryption key, -# and the table [CustomKSPTestTable] in the test database. -# -# Requirement: -# python 3.x -# ksp_app executable -# -# Execution: Run with command line with required options -# py run_ksp.py --SERVER=server --DBNAME=database --UID=uid --PWD=pwd -# py run_ksp.py --SERVER=server --DBNAME=database --UID=uid --PWD=pwd --REMOVE -# -############################################################################################# - -import sys -import os -import platform -import argparse - -################################### Main Function ################################### -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-server', '--SERVER', required=True, help='SQL Server') - parser.add_argument('-dbname', '--DBNAME', required=True, help='Name of an existing database') - parser.add_argument('-uid', '--UID', required=True, help='User name') - parser.add_argument('-pwd', '--PWD', required=True, help='User password') - parser.add_argument('-remove', '--REMOVE', action='store_true', help='Clean up KSP related data, false by default') - - args = parser.parse_args() - - app_name = 'ksp_app' - cwd = os.getcwd() - - # first check if the ksp app is present - work_dir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(work_dir) - - if platform.system() == 'Windows': - path = os.path.join(work_dir, app_name + '.exe') - executable = app_name - else: - path = os.path.join(work_dir, app_name) - executable = './' + app_name - - if not os.path.exists(path): - print('Error: {0} not found!'.format(path)) - exit(1) - - if args.REMOVE: - os.system('{0} 1 {1} {2} {3} {4}'.format(executable, args.SERVER, args.DBNAME, args.UID, args.PWD)) - else: - os.system('{0} 0 {1} {2} {3} {4}'.format(executable, args.SERVER, args.DBNAME, args.UID, args.PWD)) - - os.chdir(cwd) diff --git a/test/functional/sqlsrv/0075.phpt b/test/functional/sqlsrv/0075.phpt index 327c2260..1657af0f 100644 --- a/test/functional/sqlsrv/0075.phpt +++ b/test/functional/sqlsrv/0075.phpt @@ -1,5 +1,7 @@ --TEST-- Fix for output string parameter truncation error +--DESCRIPTION-- +This test includes calling sqlsrv_query with an array of parameters with a named key, which should result in an error. --SKIPIF-- --FILE-- @@ -23,10 +25,25 @@ if ($s === false) { $inValue1 = "Some data"; $outValue1 = ""; +$tsql = '{CALL [test_output] (?, ?)}'; + $s = sqlsrv_query( $conn, - "{CALL [test_output] (?, ?)}", - array(array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), + $tsql, + array("k1" => array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), + array(&$outValue1, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), SQLSRV_SQLTYPE_VARCHAR(512))) +); + +if ($s !== false) { + echo "Expect this to fail!\n"; +} else { + print_r(sqlsrv_errors()); +} + +$s = sqlsrv_query( + $conn, + $tsql, + array(array($inValue1, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_VARCHAR(512)), array(&$outValue1, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), SQLSRV_SQLTYPE_VARCHAR(512))) ); @@ -45,5 +62,18 @@ sqlsrv_close($conn); ?> --EXPECT-- +Array +( + [0] => Array + ( + [0] => IMSSP + [SQLSTATE] => IMSSP + [1] => -57 + [code] => -57 + [2] => String keys are not allowed in parameters arrays. + [message] => String keys are not allowed in parameters arrays. + ) + +) 512 Some data diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index eb4933b7..dec27c55 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -84,6 +84,26 @@ function isDaasMode() return ($daasMode ? true : false); } +function isSQLAzure() +{ + // 'SQL Azure' indicates SQL Database or SQL Data Warehouse + // For details, https://docs.microsoft.com/sql/t-sql/functions/serverproperty-transact-sql + $conn = connect(); + $tsql = "SELECT SERVERPROPERTY ('edition')"; + $stmt = sqlsrv_query($conn, $tsql); + + if (sqlsrv_fetch($stmt)) { + $edition = sqlsrv_get_field($stmt, 0); + if ($edition === "SQL Azure") { + return true; + } else { + return false; + } + } else { + die("Could not fetch server property."); + } +} + function isAzureDW() { // Check if running Azure Data Warehouse diff --git a/test/functional/sqlsrv/TC81_MemoryCheck.phpt b/test/functional/sqlsrv/TC81_MemoryCheck.phpt index 5845da21..40f84a6a 100644 --- a/test/functional/sqlsrv/TC81_MemoryCheck.phpt +++ b/test/functional/sqlsrv/TC81_MemoryCheck.phpt @@ -7,11 +7,15 @@ emalloc (which only allocate memory in the memory space allocated for the PHP pr PHPT_EXEC=true --SKIPIF-- - + --FILE-- getMessage(); } diff --git a/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt b/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt index ebb7334f..a0169225 100644 --- a/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt +++ b/test/functional/sqlsrv/sqlsrv_378_out_param_error.phpt @@ -2,11 +2,14 @@ This test verifies that GitHub issue #378 is fixed. --DESCRIPTION-- GitHub issue #378 - output parameters appends garbage info when variable is initialized with different data type -steps to reproduce the issue: +Steps to reproduce the issue: 1- create a store procedure with print and output parameter 2- initialize output parameters to a different data type other than the type declared in sp. 3- set the WarningsReturnAsErrors to true 4- call sp. +Also check error conditions by passing output parameters NOT by reference. +--ENV-- +PHPT_EXEC=true --SKIPIF-- --FILE-- @@ -19,11 +22,8 @@ $conn = AE\connect(); $procName = 'test_378'; createSP($conn, $procName); -sqlsrv_configure('WarningsReturnAsErrors', true); -executeSP($conn, $procName); - -sqlsrv_configure('WarningsReturnAsErrors', false); -executeSP($conn, $procName); +runTests($conn, $procName, true); +runTests($conn, $procName, false); dropProc($conn, $procName); echo "Done\n"; @@ -46,7 +46,34 @@ function createSP($conn, $procName) } } -function executeSP($conn, $procName) +//-------------------functions------------------- +function runTests($conn, $procName, $warningAsErrors) +{ + sqlsrv_configure('WarningsReturnAsErrors', $warningAsErrors); + + trace("\nWarningsReturnAsErrors: $warningAsErrors\n"); + + executeSP($conn, $procName, true, false); + executeSP($conn, $procName, true, true); + executeSP($conn, $procName, false, false); + executeSP($conn, $procName, false, true); +} + +function compareErrors() +{ + $message = 'Variable parameter 3 not passed by reference (prefaced with an &). Output or bidirectional variable parameters (SQLSRV_PARAM_OUT and SQLSRV_PARAM_INOUT) passed to sqlsrv_prepare or sqlsrv_query should be passed by reference, not by value.'; + + $error = sqlsrv_errors()[0]['message']; + + if ($error !== $message) { + print_r(sqlsrv_errors(), true); + return; + } + + trace("Comparing errors: matched!\n"); +} + +function executeSP($conn, $procName, $noRef, $prepare) { $expected = 3; $v1 = 1; @@ -54,14 +81,44 @@ function executeSP($conn, $procName) $v3 = 'str'; $res = true; - if (AE\isColEncrypted()) { - $stmt = sqlsrv_prepare($conn, "{call $procName( ?, ?, ?)}", array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT))); + $tsql = "{call $procName( ?, ?, ?)}"; + + if ($noRef) { + $params = array($v1, $v2, array($v3, SQLSRV_PARAM_OUT)); + } else { + $params = array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT)); + } + + trace("No reference: $noRef\n"); + trace("Use prepared stmt: $prepare\n"); + + if (AE\isColEncrypted() || $prepare) { + $stmt = sqlsrv_prepare($conn, $tsql, $params); if ($stmt) { $res = sqlsrv_execute($stmt); + } else { + fatalError("executeSP: failed in preparing statement with reference($noRef)"); } + if ($noRef) { + if ($res !== false) { + echo "Expect this to fail!\n"; + } + compareErrors(); + return; + } } else { - $stmt = sqlsrv_query($conn, "{call $procName( ?, ?, ?)}", array($v1, $v2, array(&$v3, SQLSRV_PARAM_OUT))); + $stmt = sqlsrv_query($conn, $tsql, $params); + if ($noRef) { + if ($stmt !== false) { + echo "Expect this to fail!\n"; + } + compareErrors(); + return; + } } + + trace("No errors: $v3 and $expected\n"); + // No errors expected if ($stmt === false || !$res) { print_r(sqlsrv_errors(), true); } diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt index 6b85cf8f..0c1c2828 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime.phpt @@ -29,18 +29,20 @@ foreach ($dataTypes as $dataType) { is_incompatible_types_error($dataType, "default type"); } else { echo "****Encrypted default type is compatible with encrypted $dataType****\n"; - if ($dataType != "time") { - AE\fetchAll($conn, $tbname); - } else { - $sql = "SELECT * FROM $tbname"; - $stmt = sqlsrv_query($conn, $sql); - $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); - foreach ($row as $key => $value) { - //var_dump( $row ); + $sql = "SELECT * FROM $tbname"; + $stmt = sqlsrv_query($conn, $sql); + $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); + foreach ($row as $key => $value) { + if ($dataType == "time") { $t = $value->format('H:i:s'); print "$key: $t\n"; + } else { + $t = date_format($value, "Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print "$key: $t $tz\n"; } } + } dropTable($conn, $tbname); } @@ -51,47 +53,23 @@ sqlsrv_close($conn); Testing date: ****Encrypted default type is compatible with encrypted date**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 0001-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 00:00:00.000000 Canada/Pacific Testing datetime: ****Encrypted default type is compatible with encrypted datetime**** -c_det: - date: 1753-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 23:59:59.997000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 1753-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 23:59:59.997000 Canada/Pacific Testing datetime2: ****Encrypted default type is compatible with encrypted datetime2**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 9999-12-31 23:59:59.123456 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 0001-01-01 00:00:00.000000 Canada/Pacific +c_rand: 9999-12-31 23:59:59.123456 Canada/Pacific Testing smalldatetime: ****Encrypted default type is compatible with encrypted smalldatetime**** -c_det: - date: 1900-01-01 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific -c_rand: - date: 2079-06-05 23:59:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +c_det: 1900-01-01 00:00:00.000000 Canada/Pacific +c_rand: 2079-06-05 23:59:00.000000 Canada/Pacific Testing time: ****Encrypted default type is compatible with encrypted time**** @@ -100,11 +78,5 @@ c_rand: 23:59:59 Testing datetimeoffset: ****Encrypted default type is compatible with encrypted datetimeoffset**** -c_det: - date: 0001-01-01 00:00:00.000000 - timezone_type: 1 - timezone: -14:00 -c_rand: - date: 9999-12-31 23:59:59.123456 - timezone_type: 1 - timezone: +14:00 +c_det: 0001-01-01 00:00:00.000000 -14:00 +c_rand: 9999-12-31 23:59:59.123456 +14:00 diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt index e88ef0df..36df5670 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve.phpt @@ -34,10 +34,13 @@ foreach ($decrypted_row as $key => $value) { if (!is_object($value)) { print "$key: $value\n"; } else { - print "$key:\n"; - foreach ($value as $dateKey => $dateValue) { - print " $dateKey: $dateValue\n"; - } + // print "$key:\n"; + // foreach ($value as $dateKey => $dateValue) { + // print " $dateKey: $dateValue\n"; + // } + $t = date_format($value, "Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print "$key: $t $tz\n"; } } sqlsrv_free_stmt($stmt); @@ -70,10 +73,7 @@ Retrieving plaintext data: SSN: 795-73-9838 FirstName: Catherine LastName: Abel -BirthDate: - date: 1996-10-19 00:00:00.000000 - timezone_type: 3 - timezone: Canada/Pacific +BirthDate: 1996-10-19 00:00:00.000000 Canada/Pacific Checking ciphertext data: Done diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt index d8c272ff..dac610fb 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_retrieve_fixed_size.phpt @@ -38,7 +38,20 @@ if ($r === false) { } print "Decrypted values:\n"; -AE\fetchAll($conn, $tbname); + +$stmt = selectFromTable($conn, $tbname); +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + foreach ($row as $key => $value) { + if (is_object($value)) { + // datetime objects + $t = date_format($value,"Y-m-d H:i:s.u"); + $tz = $value->getTimezone()->getName(); + print("$key: $t $tz\n"); + } else { + print("$key: $value\n"); + } + } +} sqlsrv_free_stmt($stmt); @@ -75,12 +88,6 @@ IntData: 2147483647 BigIntData: 92233720368547 DecimalData: 79228162514264 BitData: 1 -DateTimeData: - date: 9999-12-31 23:59:59.997000 - timezone_type: 3 - timezone: Canada/Pacific -DateTime2Data: - date: 9999-12-31 23:59:59.123456 - timezone_type: 3 - timezone: Canada/Pacific +DateTimeData: 9999-12-31 23:59:59.997000 Canada/Pacific +DateTime2Data: 9999-12-31 23:59:59.123456 Canada/Pacific Done diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt index 7eebb400..67c601b8 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt @@ -59,7 +59,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt index 25d7d15f..ea5285d8 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt @@ -69,7 +69,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt index 77d53147..8bbe9606 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt @@ -57,7 +57,8 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + $errors = sqlsrv_errors(); + if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt index 96c82d63..fcbc4080 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_access_token.phpt @@ -104,6 +104,27 @@ function simpleTest($conn) dropTable($conn, $tableName); } +function connectAzureDB($accToken, $showException) +{ + global $adServer, $adDatabase, $maxAttempts; + + $conn = false; + $connectionInfo = array("Database"=>$adDatabase, "AccessToken"=>$accToken); + + $conn = sqlsrv_connect($adServer, $connectionInfo); + if ($conn === false) { + if ($showException) { + fatalError("Could not connect with Azure AD AccessToken after $maxAttempts retries.\n"); + } + } else { + simpleTest($conn); + + sqlsrv_close($conn); + } + + return $conn; +} + // First test some error conditions connectWithInvalidOptions($server); @@ -112,17 +133,18 @@ connectWithEmptyAccessToken($server); // Next, test with a valid access token and perform some simple tasks require_once('access_token.inc'); -if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { - $connectionInfo = array("Database"=>$adDatabase, "AccessToken"=>$accToken); +$maxAttempts = 3; - $conn = sqlsrv_connect($adServer, $connectionInfo); - if ($conn === false) { - fatalError("Could not connect with Azure AD AccessToken.\n"); - } else { - simpleTest($conn); - - sqlsrv_close($conn); - } +if ($adServer != 'TARGET_AD_SERVER' && $accToken != 'TARGET_ACCESS_TOKEN') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($accToken, ($numAttempts == ($maxAttempts - 1))); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); } echo "Done\n"; diff --git a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt index 51f61b27..f02a6f62 100644 --- a/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt +++ b/test/functional/sqlsrv/sqlsrv_azure_ad_authentication.phpt @@ -54,23 +54,44 @@ if ($conn === false) { // Test Azure AD on an Azure database instance. Replace $azureServer, etc with // your credentials to test, or this part is skipped. // -$azureServer = $adServer; -$azureDatabase = $adDatabase; -$azureUsername = $adUser; -$azurePassword = $adPassword; - -if ($azureServer != 'TARGET_AD_SERVER') { - $connectionInfo = array( "UID"=>$azureUsername, "PWD"=>$azurePassword, - "Authentication"=>'ActiveDirectoryPassword', "TrustServerCertificate"=>false ); - - $conn = sqlsrv_connect($azureServer, $connectionInfo); +function connectAzureDB($showException) +{ + global $adServer, $adUser, $adPassword, $maxAttempts; + + $connectionInfo = array("UID"=>$adUser, + "PWD"=>$adPassword, + "Authentication"=>'ActiveDirectoryPassword', + "TrustServerCertificate"=>false ); + + $conn = false; + $conn = sqlsrv_connect($adServer, $connectionInfo); if ($conn === false) { - echo "Could not connect with ActiveDirectoryPassword.\n"; - print_r(sqlsrv_errors()); + if ($showException) { + echo "Could not connect with ActiveDirectoryPassword after $maxAttempts retries.\n"; + print_r(sqlsrv_errors()); + } } else { echo "Connected successfully with Authentication=ActiveDirectoryPassword.\n"; sqlsrv_close($conn); } + + return $conn; +} + +$azureServer = $adServer; +$maxAttempts = 3; + +if ($azureServer != 'TARGET_AD_SERVER') { + $conn = false; + $numAttempts = 0; + do { + $conn = connectAzureDB($numAttempts == ($maxAttempts - 1)); + if ($conn === false) { + $numAttempts++; + sleep(10); + } + } while ($conn === false && $numAttempts < $maxAttempts); + } else { echo "Not testing with Authentication=ActiveDirectoryPassword.\n"; } diff --git a/test/functional/sqlsrv/sqlsrv_batch_query.phpt b/test/functional/sqlsrv/sqlsrv_batch_query.phpt new file mode 100644 index 00000000..4cfa1445 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_batch_query.phpt @@ -0,0 +1,199 @@ +--TEST-- +Test a batch query with different cursor types +--DESCRIPTION-- +Verifies that batch queries don't work with dynamic, static, and keyset +server-side cursors, and checks that correct column and row counts are +returned otherwise. For information on the expected behaviour of cursors +with batch queries, see +https://docs.microsoft.com/en-us/previous-versions/visualstudio/aa266531(v=vs.60) +--SKIPIF-- + +--FILE-- +$cursor)); + if (!$stmt) { + fatalError("Error preparing statement with $cursor cursor\n"); + } + + if (!sqlsrv_execute($stmt)) { + if ($cursor == 'forward' or $cursor == 'buffered') { + fatalError("Statement execution failed unexpectedly with a $cursor cursor\n"); + } else { + checkErrors($noCursor); + continue; + } + } + + $numResultSets = 0; + + // Check the column and row count before and after running through + // each result set, because some cursor types may return the number + // of rows only after fetching all rows in the result set + do { + checkColumnsAndRows($stmt, $cursor, $wrongCursor); + + $row = 0; + while ($res = sqlsrv_fetch_array($stmt)) { + if ($res[0] != $data[$numResultSets][$row]) { + fatalError("Wrong result, expected ".$data[$numResultSets][$row].", got $res[0]\n"); + } + ++$row; + } + + checkColumnsAndRows($stmt, $cursor, $wrongCursor); + ++$numResultSets; + + } while ($next = sqlsrv_next_result($stmt)); + + if ($numResultSets != $expectedResultSets) { + fatalError("Unexpected number of result sets, expected $expectedResultedSets, got $numResultSets\n"); + } + + // We expect an error if sqlsrv_next_result returns false, + // but not if it returns null (i.e. if we are genuinely at + // the end of all the result sets with a buffered cursor) + if ($next === false) { + if ($cursor == 'forward') { + checkErrors($noNextResult); + } else { + fatalError("sqlsrv_next_result failed with a $cursor cursor\n"); + } + } + + sqlsrv_free_stmt($stmt); +} + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done.\n"; +?> +--EXPECT-- +Testing with forward cursor... +Testing with dynamic cursor... +Testing with static cursor... +Testing with keyset cursor... +Testing with buffered cursor... +Done. diff --git a/test/functional/sqlsrv/sqlsrv_connStr.phpt b/test/functional/sqlsrv/sqlsrv_connStr.phpt index 144ceb94..7c92bd99 100644 --- a/test/functional/sqlsrv/sqlsrv_connStr.phpt +++ b/test/functional/sqlsrv/sqlsrv_connStr.phpt @@ -1,11 +1,18 @@ --TEST-- UTF-8 connection strings --SKIPIF-- - + --FILE-- 'gibberish' )); if ($c !== false) { fatalError("Should have errored on an invalid encoding."); } -print_r(sqlsrv_errors()); +checkErrors($gibberishEncoding); $c = connect(array( 'CharacterSet' => SQLSRV_ENC_BINARY )); if ($c !== false) { fatalError("Should have errored on an invalid encoding."); } -print_r(sqlsrv_errors()); +checkErrors($binaryEncoding); $c = connect(array( 'CharacterSet' => SQLSRV_ENC_CHAR )); if ($c === false) { @@ -50,7 +68,7 @@ $c = sqlsrv_connect($server_invalid, array( 'Database' => 'test', 'CharacterSet' if ($c !== false) { fatalError("sqlsrv_connect(1) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($utf16Error); // APP has a UTF-8 name $c = connect(array( @@ -67,7 +85,7 @@ $c = connect(array( if ($c !== false) { fatalError("sqlsrv_connect(3) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($userLoginFailed); // invalid UTF-8 in the pwd $c = connect(array( @@ -77,89 +95,10 @@ $c = connect(array( if ($c !== false) { fatalError("sqlsrv_connect(4) should have failed"); } -print_r(sqlsrv_errors()); +checkErrors($utf16Error); echo "Test succeeded.\n"; ?> ---EXPECTF-- -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -48 - [code] => -48 - [2] => The encoding 'gibberish' is not a supported encoding for the CharacterSet connection option. - [message] => The encoding 'gibberish' is not a supported encoding for the CharacterSet connection option. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -48 - [code] => -48 - [2] => The encoding 'binary' is not a supported encoding for the CharacterSet connection option. - [message] => The encoding 'binary' is not a supported encoding for the CharacterSet connection option. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -47 - [code] => -47 - [2] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - [message] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - ) - -) -Array -( - [0] => Array - ( - [0] => 28000 - [SQLSTATE] => 28000 - [1] => 18456 - [code] => 18456 - [2] => %SLogin failed for user '%s'. - [message] => %SLogin failed for user '%s'. - ) - - [1] => Array - ( - [0] => 28000 - [SQLSTATE] => 28000 - [1] => 18456 - [code] => 18456 - [2] => %SLogin failed for user '%s'. - [message] => %SLogin failed for user '%s'. - ) - -) -Array -( - [0] => Array - ( - [0] => IMSSP - [SQLSTATE] => IMSSP - [1] => -47 - [code] => -47 - [2] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - [message] => An error occurred translating the connection string to UTF-16: No mapping for the Unicode character exists in the target multi-byte code page. - - ) - -) +--EXPECT-- Test succeeded. diff --git a/test/functional/sqlsrv/sqlsrv_data_classification.phpt b/test/functional/sqlsrv/sqlsrv_data_classification.phpt new file mode 100644 index 00000000..ab06cce6 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_data_classification.phpt @@ -0,0 +1,312 @@ +--TEST-- +Test data classification feature - retrieving sensitivity metadata if supported +--DESCRIPTION-- +If both ODBC and server support this feature, this test verifies that sensitivity metadata can be added and correctly retrieved. If not, it will at least test the new statement attribute and some error cases. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true); + $tsql = ($isSupported)? "SELECT PatientId FROM $tableName" : "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (1): failed with sqlsrv_query '$tsql'.\n"); + } + + $notAvailableErr = '*Failed to retrieve Data Classification Sensitivity Metadata. If the driver and the server both support the Data Classification feature, check whether the query returns columns with classification information.'; + + $unexpectedErrorState = '*Failed to retrieve Data Classification Sensitivity Metadata: Check if ODBC driver or the server supports the Data Classification feature.'; + + $error = ($driverCapable) ? $notAvailableErr : $unexpectedErrorState; + + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (1): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($error, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } + + // (2) call sqlsrv_prepare() with DataClassification but do not execute the stmt + $stmt = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt) { + fatalError("testErrorCases (2): failed with sqlsrv_prepare '$tsql'.\n"); + } + + $executeFirstErr = '*The statement must be executed to retrieve Data Classification Sensitivity Metadata.'; + $metadata = sqlsrv_field_metadata($stmt); + if ($metadata) { + echo "testErrorCases (2): expected sqlsrv_field_metadata to fail\n"; + } + + if (!fnmatch($executeFirstErr, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } +} + +function isDataClassSupported($conn, &$driverCapable) +{ + // Check both SQL Server version and ODBC driver version + $msodbcsqlVer = sqlsrv_client_info($conn)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + + // ODBC Driver must be 17.2 or above + $driverCapable = true; + if ($version[0] < 17 || $version[1] < 2) { + $driverCapable = false; + return false; + } + + // SQL Server must be SQL Server 2019 or above + $serverVer = sqlsrv_server_info($conn)['SQLServerVersion']; + if (explode('.', $serverVer)[0] < 15) { + return false; + } + + return true; +} + +function getRegularMetadata($conn, $tsql) +{ + // Run the query without data classification metadata + $stmt1 = sqlsrv_query($conn, $tsql); + if (!$stmt1) { + fatalError("getRegularMetadata (1): failed in sqlsrv_query.\n"); + } + + // Run the query with the attribute set to false + $options = array('DataClassification' => false); + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("getRegularMetadata (2): failed in sqlsrv_query.\n"); + } + + // The metadata for each statement, column by column, should be identical + $numCol = sqlsrv_num_fields($stmt1); + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff($metadata1[$i], $metadata2[$i]); + if (!empty($diff)) { + print_r($diff); + } + } + + return $stmt1; +} + +function verifyClassInfo($input, $actual) +{ + // For simplicity of this test, only one set of sensitivity data. Namely, + // an array with one set of Label (name, id) and Information Type (name, id) + if (count($actual) != 1) { + echo "Expected an array with only one element\n"; + return false; + } + + if (count($actual[0]) != 2) { + echo "Expected a Label pair and Information Type pair\n"; + return false; + } + + // Label should be name and id pair (id should be empty) + if (count($actual[0]['Label']) != 2) { + echo "Expected only two elements for the label\n"; + return false; + } + $label = $input[0]; + if ($actual[0]['Label']['name'] !== $label || !empty($actual[0]['Label']['id'])){ + return false; + } + + // Like Label, Information Type should also be name and id pair (id should be empty) + if (count($actual[0]['Information Type']) != 2) { + echo "Expected only two elements for the information type\n"; + return false; + } + $info = $input[1]; + if ($actual[0]['Information Type']['name'] !== $info || !empty($actual[0]['Information Type']['id'])){ + return false; + } + + return true; +} + +function compareDataClassification($stmt1, $stmt2, $classData) +{ + global $dataClassKey; + + $numCol = sqlsrv_num_fields($stmt1); + + $metadata1 = sqlsrv_field_metadata($stmt1); + $metadata2 = sqlsrv_field_metadata($stmt2); + + // The built-in array_diff_assoc() function compares the keys and values + // of two (or more) arrays, and returns an array that contains the entries + // from array1 that are not present in array2 or array3, etc. + // + // For this test, $metadata2 should have one extra key 'Data Classification', + // which should not be present in $metadata1 + // + // If the column does not have sensitivity metadata, the value should be an + // empty array. Otherwise, it should contain an array with one set of + // Label (name, id) and Information Type (name, id) + + $noClassInfo = array($dataClassKey => array()); + for ($i = 0; $i < $numCol; $i++) { + $diff = array_diff_assoc($metadata2[$i], $metadata1[$i]); + + // Is classification input data empty? + if (empty($classData[$i])) { + // Then it should be equivalent to $noClassInfo + if ($diff !== $noClassInfo) { + var_dump($diff); + } + } else { + // Verify the classification metadata + if (!verifyClassInfo($classData[$i], $diff[$dataClassKey])) { + var_dump($diff); + } + } + } +} + +function runBatchQuery($conn, $tableName) +{ + global $dataClassKey; + + $options = array('DataClassification' => true); + $tsql = "SELECT SSN, BirthDate FROM $tableName"; + $batchQuery = $tsql . ';' . $tsql; + + $stmt = sqlsrv_query($conn, $batchQuery, array(), $options); + if (!$stmt) { + fatalError("Error when calling sqlsrv_query '$tsql'.\n"); + } + + $numCol = sqlsrv_num_fields($stmt); + $c = rand(0, $numCol - 1); + + $metadata1 = sqlsrv_field_metadata($stmt); + if (!$metadata1 || !array_key_exists($dataClassKey, $metadata1[$c])) { + fatalError("runBatchQuery(1): failed to get metadata"); + } + $result = sqlsrv_next_result($stmt); + if (is_null($result) || !$result) { + fatalError("runBatchQuery: failed to get next result"); + } + $metadata2 = sqlsrv_field_metadata($stmt); + if (!$metadata2 || !array_key_exists($dataClassKey, $metadata2[$c])) { + fatalError("runBatchQuery(2): failed to get metadata"); + } + + $jstr1 = json_encode($metadata1[$c][$dataClassKey]); + $jstr2 = json_encode($metadata2[$c][$dataClassKey]); + if ($jstr1 !== $jstr2) { + echo "The JSON encoded strings should be identical\n"; + var_dump($jstr1); + var_dump($jstr2); + } +} + +/////////////////////////////////////////////////////////////////////////////////////// +require_once('MsCommon.inc'); + +$conn = AE\connect(); +if (!$conn) { + fatalError("Failed to connect.\n"); +} + +$driverCapable = true; +$isSupported = isDataClassSupported($conn, $driverCapable); + +// Create a test table +$tableName = 'srvPatients'; +$colMeta = array(new AE\ColumnMeta('INT', 'PatientId', 'IDENTITY NOT NULL'), + new AE\ColumnMeta('CHAR(11)', 'SSN'), + new AE\ColumnMeta('NVARCHAR(50)', 'FirstName'), + new AE\ColumnMeta('NVARCHAR(50)', 'LastName'), + new AE\ColumnMeta('DATE', 'BirthDate')); +AE\createTable($conn, $tableName, $colMeta); + +// If data classification is supported, then add sensitivity classification metadata +// to columns SSN and Birthdate +$classData = [ + array(), + array('Highly Confidential - GDPR', 'Credentials'), + array(), + array(), + array('Confidential Personal Data', 'Birthdays') + ]; + +if ($isSupported) { + // column SSN + $label = $classData[1][0]; + $infoType = $classData[1][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].SSN WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("SSN: Add sensitivity $label and $infoType failed.\n"); + } + + // column BirthDate + $label = $classData[4][0]; + $infoType = $classData[4][1]; + $sql = "ADD SENSITIVITY CLASSIFICATION TO [$tableName].BirthDate WITH (LABEL = '$label', INFORMATION_TYPE = '$infoType')"; + $stmt = sqlsrv_query($conn, $sql); + if (!$stmt) { + fatalError("BirthDate: Add sensitivity $label and $infoType failed.\n"); + } +} + +testErrorCases($conn, $tableName, $isSupported, $driverCapable); + +// Run the query without data classification metadata +$tsql = "SELECT * FROM $tableName"; +$stmt = getRegularMetadata($conn, $tsql); + +// Proceeed to retrieve sensitivity metadata, if supported +if ($isSupported) { + $options = array('DataClassification' => true); + $stmt1 = sqlsrv_prepare($conn, $tsql, array(), $options); + if (!$stmt1) { + fatalError("Error when calling sqlsrv_prepare '$tsql'.\n"); + } + if (!sqlsrv_execute($stmt1)) { + fatalError("Error in executing statement.\n"); + } + + compareDataClassification($stmt, $stmt1, $classData); + sqlsrv_free_stmt($stmt1); + + // $stmt2 should produce the same result as the previous $stmt1 + $stmt2 = sqlsrv_query($conn, $tsql, array(), $options); + if (!$stmt2) { + fatalError("Error when calling sqlsrv_query '$tsql'.\n"); + } + + compareDataClassification($stmt, $stmt2, $classData); + sqlsrv_free_stmt($stmt2); + + runBatchQuery($conn, $tableName); +} + +sqlsrv_free_stmt($stmt); + +dropTable($conn, $tableName); +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt index 08f42b26..8087d1d0 100644 --- a/test/functional/sqlsrv/sqlsrv_data_to_str.phpt +++ b/test/functional/sqlsrv/sqlsrv_data_to_str.phpt @@ -1,5 +1,7 @@ --TEST-- large types to strings of 1MB size. +--DESCRIPTION-- +This includes a test by providing an invalid php type. --SKIPIF-- --FILE-- @@ -8,7 +10,7 @@ large types to strings of 1MB size. sqlsrv_configure( 'WarningsReturnAsErrors', 0 ); sqlsrv_configure( 'LogSeverity', SQLSRV_LOG_SEVERITY_ALL ); - require( 'MsCommon.inc' ); + require_once( 'MsCommon.inc' ); $conn = Connect(); if( !$conn ) { @@ -59,6 +61,16 @@ large types to strings of 1MB size. die( "sqlsrv_get_field(6) failed." ); } + $str = sqlsrv_get_field( $stmt, 0, SQLSRV_PHPTYPE_STRING("UTF") ); + if ($str === false) { + $error = sqlsrv_errors()[0]['message']; + if ($error !== 'Invalid type') { + fatalError('Unexpected error returned'); + } + } else { + echo "Expect sqlsrv_get_field(7) to fail!\n"; + } + sqlsrv_free_stmt( $stmt ); sqlsrv_close( $conn ); diff --git a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt index 7f751324..92b7f0ae 100644 --- a/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt +++ b/test/functional/sqlsrv/sqlsrv_empty_result_error.phpt @@ -153,7 +153,7 @@ echo "Null result set, call next result first: #############################\n"; $stmt = sqlsrv_query($conn, "TestEmptySetProc @a='a', @b='c'"); NextResult($stmt, []); -Fetch($stmt, [$errorFuncSeq()]); +Fetch($stmt, [$errorNoFields]); // Call next_result twice in succession on a null result set echo "Null result set, call next result twice: #############################\n"; diff --git a/test/functional/sqlsrv/sqlsrv_escape_braces.phpt b/test/functional/sqlsrv/sqlsrv_escape_braces.phpt new file mode 100644 index 00000000..3c3eeae0 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_escape_braces.phpt @@ -0,0 +1,70 @@ +--TEST-- +Test that right braces are escaped correctly and that error messages are correct when they're not +--SKIPIF-- + +--FILE-- +$test[0], 'pwd'=>$password, 'LoginTimeout'=>1)); + + if (strpos(sqlsrv_errors()[0][2], $test[1]) === false) { + print_r("Wrong error message returned for test string ".$test[0].". Expected ".$test[1].", actual output:\n"); + print_r(sqlsrv_errors()); + } + + unset($conn); +} + +echo "Done.\n"; +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt b/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt index ac2c1cd0..abd4bb7d 100644 --- a/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt +++ b/test/functional/sqlsrv/sqlsrv_statement_format_money_types.phpt @@ -63,20 +63,10 @@ function testFloatTypes($conn) for ($i = 0; $i < count($values); $i++) { $floatStr = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); $floatVal = floatval($floatStr); - - // Check if the numbers of decimal digits are the same - // It is highly unlikely but not impossible - $numbers = explode('.', $floatStr); - $len = strlen($numbers[1]); - if ($len == $numDigits && $floatVal != $floats[$i]) { - echo "Expected $floats[$i] but returned "; + $diff = abs($floatVal - $floats[$i]) / $floats[$i]; + if ($diff > $epsilon) { + echo "$diff: Expected $floats[$i] but returned "; var_dump($floatVal); - } else { - $diff = abs($floatVal - $floats[$i]) / $floats[$i]; - if ($diff > $epsilon) { - echo "$diff: Expected $floats[$i] but returned "; - var_dump($floatVal); - } } } } else { diff --git a/test/functional/sqlsrv/srv_007_login_timeout.phpt b/test/functional/sqlsrv/srv_007_login_timeout.phpt index c8fa5cf1..eda8cf3a 100644 --- a/test/functional/sqlsrv/srv_007_login_timeout.phpt +++ b/test/functional/sqlsrv/srv_007_login_timeout.phpt @@ -9,17 +9,42 @@ Intentionally provide an invalid server name and set LoginTimeout. Verify the ti $serverName = "WRONG_SERVER_NAME"; -$t0 = microtime(true); +// Based on the following reference, a login timeout of less than approximately 10 seconds +// is not reliable. The defaut is 15 seconds so we fix it at 20 seconds. +// https://docs.microsoft.com/sql/connect/odbc/windows/features-of-the-microsoft-odbc-driver-for-sql-server-on-windows -$conn = sqlsrv_connect($serverName , array("LoginTimeout" => 8)); +$timeout = 20; +$maxAttempts = 3; +$numAttempts = 0; +$leeway = 1.0; +$missed = false; -$t1 = microtime(true); +do { + $t0 = microtime(true); -echo "Connection attempt time: " . ($t1 - $t0) . " [sec]\n"; + $conn = sqlsrv_connect($serverName , array("LoginTimeout" => $timeout)); + $numAttempts++; -print "Done"; + $t1 = microtime(true); + + // Sometimes time elapsed might be less than expected timeout, such as 19.99* + // something, but 1.0 second leeway should be reasonable + $elapsed = $t1 - $t0; + $diff = abs($elapsed - $timeout); + + $missed = ($diff > $leeway); + if ($missed) { + if ($numAttempts == $maxAttempts) { + echo "Connection failed at $elapsed secs. Leeway is $leeway sec but the difference is $diff\n"; + } else { + // The test will fail but this helps us decide if this test should be redesigned + echo "$numAttempts\t"; + sleep(5); + } + } +} while ($missed && $numAttempts < $maxAttempts); + +print "Done\n"; ?> - ---EXPECTREGEX-- -Connection attempt time: [7-9]\.[0-9]+ \[sec\] +--EXPECT-- Done diff --git a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt index 459169d9..c60e23de 100644 --- a/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt +++ b/test/functional/sqlsrv/srv_228_sqlsrv_clientbuffermaxkbsize_option.phpt @@ -44,7 +44,8 @@ function fetchData($conn, $table, $size) echo "Expect this to fail\n"; } else { $error = 'Memory limit of 1 KB exceeded for buffered query'; - if (strpos(sqlsrv_errors()[0]['message'], $error) === false) { + $errors = sqlsrv_errors(); + if (!empty($errors) && strpos($errors[0]['message'], $error) === false) { print_r(sqlsrv_errors()); } } diff --git a/test/functional/sqlsrv/srv_569_query_varcharmax.phpt b/test/functional/sqlsrv/srv_569_query_varcharmax.phpt new file mode 100644 index 00000000..6d4bab35 --- /dev/null +++ b/test/functional/sqlsrv/srv_569_query_varcharmax.phpt @@ -0,0 +1,123 @@ +--TEST-- +GitHub issue #569 - sqlsrv_query on varchar max fields results in function sequence error +--DESCRIPTION-- +Verifies that the problem is no longer reproducible. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $database, "UID" => $userName, "PWD" => $userPassword); +$conn = sqlsrv_connect($server, $options); +if ($conn === false) { + fatalError("Failed to connect to $server."); +} + +$qualified = AE\isQualified($conn); +if ($qualified) { + sqlsrv_close($conn); + + // Now connect with ColumnEncryption enabled + $connectionOptions = array_merge($options, array('ColumnEncryption' => 'Enabled')); + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + fatalError("Failed to connect to $server."); + } +} + +$tableName = 'srvTestTable_569'; + +dropTable($conn, $tableName); + +if ($qualified && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = deterministic, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = AEColumnKey))"; +} else { + $tsql = "CREATE TABLE $tableName ([c1] varchar(max))"; +} + +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to create $tableName"); +} + +$input = 'some very large string'; +$stmt = sqlsrv_prepare($conn, "INSERT INTO $tableName (c1) VALUES (?)", array($input)); +sqlsrv_execute($stmt); + +$tsql = "SELECT * FROM $tableName"; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read from $tableName"); +} + +sqlsrv_fetch($stmt); +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + +if ($fieldVal !== $input) { + echo "Expected $input but got: "; + var_dump($fieldVal); +} + +$tsql2 = "DELETE FROM $tableName"; +$stmt = sqlsrv_query($conn, $tsql2); +if (!$stmt) { + fatalError("Failed to delete rows from $tableName"); +} + +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read $tableName, now empty"); +} + +$result = sqlsrv_fetch($stmt); +if (!is_null($result)) { + echo 'Expected null when fetching an empty table but got: '; + var_dump($result); +} + +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); +verifyFetchError(); +if ($fieldVal !== false) { + echo 'Expected bool(false) but got: '; + var_dump($fieldVal); +} + +$stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>"buffered")); +$result = sqlsrv_fetch($stmt); +if (!is_null($result)) { + echo 'Expected null when fetching an empty table but got: '; + var_dump($result); +} + +$fieldVal = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); +verifyFetchError(); +if ($fieldVal !== false) { + echo 'Expected bool(false) but got: '; + var_dump($fieldVal); +} + + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt b/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt new file mode 100644 index 00000000..34f11240 --- /dev/null +++ b/test/functional/sqlsrv/srv_570_fetch_varbinary.phpt @@ -0,0 +1,143 @@ +--TEST-- +GitHub issue #570 - fetching a varbinary field as a stream using client buffer +--DESCRIPTION-- +Verifies that a varbinary field (with size or max) can be successfully fetched even when a client buffer is used. There is no more "Invalid cursor state" error. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + SQLSRV_CURSOR_CLIENT_BUFFERED, "ClientBufferMaxKBSize" => 51200)); + } else { + $stmt = sqlsrv_prepare($conn, $tsql); + } + if ($stmt === false) { + fatalError("Error in preparing the query ($buffered)."); + } + + $result = sqlsrv_execute($stmt); + if ($result === false) { + fatalError("Error in executing the query ($buffered)."); + } + } else { + if ($buffered) { + // Use the default buffer size in this case + $stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED)); + } else { + $stmt = sqlsrv_query($conn, $tsql); + } + + if ($stmt === false) { + fatalError("Error in sqlsrv_query ($buffered)."); + } + } + + fetchData($stmt, $data, $buffered); +} + +function runTest($conn, $columnType, $path) +{ + $tableName = 'srvTestTable_570' . rand(0, 10); + dropTable($conn, $tableName); + + // Create the test table with only one column + $tsql = "CREATE TABLE $tableName([picture] $columnType NOT NULL)"; + $stmt = sqlsrv_query($conn, $tsql); + if (!$stmt) { + fatalError("Failed to create table $tableName\n"); + } + + // Insert php.gif as stream data + $tsql = "INSERT INTO $tableName (picture) VALUES (?)"; + + $data = fopen($path, 'rb'); + if (!$data) { + fatalError('Could not open image for reading.'); + } + + $params = array($data, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + $stmt = sqlsrv_query($conn, $tsql, array($params)); + if ($stmt === false) { + fatalError("Failed to insert image into $tableName"); + } + do { + $read = sqlsrv_send_stream_data($stmt); + } while ($read); + sqlsrv_free_stmt($stmt); + + // Start testing, with or without client buffer, using prepared statement or direct query + $tsql = "SELECT picture FROM $tableName"; + runQuery($conn, $data, $tsql, false, true); + runQuery($conn, $data, $tsql, true, true); + runQuery($conn, $data, $tsql, false, false); + runQuery($conn, $data, $tsql, true, false); + + // Clean up + fclose($data); + + dropTable($conn, $tableName); +} + +require_once('MsCommon.inc'); + +$conn = connect(); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); +} + +if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $pic = '\\php.gif'; +} else { // other than Windows + $pic = '/php.gif'; +} +$path = dirname($_SERVER['PHP_SELF']) . $pic; + +runTest($conn, 'VARBINARY(MAX)', $path); +runTest($conn, 'VARBINARY(4096)', $path); + +echo "Done\n"; + +sqlsrv_close($conn); +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_error_encoding.phpt b/test/functional/sqlsrv/test_error_encoding.phpt index 76302e20..c14b41ce 100644 --- a/test/functional/sqlsrv/test_error_encoding.phpt +++ b/test/functional/sqlsrv/test_error_encoding.phpt @@ -4,11 +4,35 @@ Encoding of sqlsrv errors --FILE-- 'UTF-8' )); +require_once('MsSetup.inc'); + +$connectionOptions = array('UID' => $userName, 'PWD' => $userPassword, 'CharacterSet' => 'UTF-8'); +$conn = sqlsrv_connect($server, $connectionOptions); if (!$conn) { die(print_r(sqlsrv_errors(), true)); } @@ -22,27 +46,15 @@ sqlsrv_free_stmt($stmt); $stmt = sqlsrv_query($conn, "select *, BadColumn from sys.syslanguages"); if ($stmt) { - echo 'OK!'; + echo 'This should have failed!\n'; sqlsrv_free_stmt($stmt); } else { - $errs = sqlsrv_errors(); - print_r($errs); + verifyErrorContents(); } sqlsrv_close($conn); +echo "Done\n"; ?> ---EXPECTF-- -Array -( - [0] => Array - ( - [0] => 42S22 - [SQLSTATE] => 42S22 - [1] => 207 - [code] => 207 - [2] => %SUngültiger Spaltenname %cBadColumn%c. - [message] => %SUngültiger Spaltenname %cBadColumn%c. - ) - -) +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt b/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt new file mode 100644 index 00000000..6309af7e --- /dev/null +++ b/test/functional/sqlsrv/test_error_encoding_with_language_option.phpt @@ -0,0 +1,55 @@ +--TEST-- +GitHub issue 929 - able to change the language when connecting +--DESCRIPTION-- +A test similar to test_error_encoding, created for GitHub issue 929 +--SKIPIF-- + +--FILE-- + $userName, 'PWD' => $userPassword, 'CharacterSet' => 'UTF-8', 'Language' => 'German'); +$conn = sqlsrv_connect($server, $connectionOptions); +if (!$conn) { + die(print_r(sqlsrv_errors(), true)); +} + +$stmt = sqlsrv_query($conn, "select *, BadColumn from sys.syslanguages"); +if ($stmt) { + echo 'This should have failed!\n'; + sqlsrv_free_stmt($stmt); +} else { + verifyErrorContents(); +} + +sqlsrv_close($conn); + +echo "Done\n"; +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_largeData.phpt b/test/functional/sqlsrv/test_largeData.phpt index 4fa2d830..72feabf8 100644 --- a/test/functional/sqlsrv/test_largeData.phpt +++ b/test/functional/sqlsrv/test_largeData.phpt @@ -1,119 +1,158 @@ --TEST-- -send a large amount (10MB) using encryption. +Send a large amount (10MB) using encryption. In a Linux CI environment use a smaller size. --SKIPIF-- --FILE-- total_read = 0; return true; } - function stream_read( $count ) + public function stream_read($count) { - if( $this->total_read > 20000000 ) { + global $limit; + if ($this->total_read > $limit) { return 0; } global $packets; ++$packets; - $str = str_repeat( "A", $count ); + + // 8192 is passed to stream_read as $count + $str = str_repeat("A", $count); $this->total_read += $count; return $str; } - function stream_write($data) + public function stream_write($data) { } - function stream_tell() + public function stream_tell() { return $this->total_read; } - function stream_eof() + public function stream_eof() { - return $this->total_read > 20000000; + global $limit; + return $this->total_read > $limit; } - function stream_seek($offset, $whence) + public function stream_seek($offset, $whence) { // For the purpose of this test only support SEEK_SET to $offset 0 if ($whence == SEEK_SET && $offset == 0) { $this->total_read = $offset; return true; - } + } return false; } } +function isServerInLinux($conn) +{ + // This checks if SQL Server is running in Linux (Docker) in a CI environment + // If so, the major version must be 14 or above (SQL Server 2017 or above) + $serverVer = sqlsrv_server_info($conn)['SQLServerVersion']; + if (explode('.', $serverVer)[0] < 14) { + return false; + } + + // The view sys.dm_os_host_info, available starting in SQL Server 2017, is somewhat similar to sys.dm_os_windows_info. + // It returns one row that displays operating system version information and has columns to differentiate + // Windows and Linux. + $stmt = sqlsrv_query($conn, 'SELECT host_platform FROM sys.dm_os_host_info'); + if ($stmt && sqlsrv_fetch($stmt)) { + $host = sqlsrv_get_field($stmt, 0); + return ($host === 'Linux'); + } + + return false; +} + set_time_limit(0); -sqlsrv_configure( 'WarningsReturnAsErrors', 0 ); -sqlsrv_configure( 'LogSubsystems', SQLSRV_LOG_SYSTEM_ALL ); +sqlsrv_configure('WarningsReturnAsErrors', 0); +sqlsrv_configure('LogSubsystems', SQLSRV_LOG_SYSTEM_ALL); $packets = 0; +$limit = 20000000; -$result = stream_wrapper_register( "mystr", "my_stream" ); -if( !$result ) { - die( "Couldn't register stream class." ); +$result = stream_wrapper_register("mystr", "my_stream"); +if (!$result) { + die("Couldn't register stream class."); } -require( 'MsCommon.inc' ); +require_once('MsCommon.inc'); $conn = Connect(array( 'Encrypt' => true, 'TrustServerCertificate' => true )); -if( $conn === false ) { - die( print_r( sqlsrv_errors(), true )); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); } -$stmt = sqlsrv_query( $conn, "IF OBJECT_ID('test_lob', 'U') IS NOT NULL DROP TABLE test_lob" ); -if( $stmt !== false ) sqlsrv_free_stmt( $stmt ); - -$stmt = sqlsrv_query( $conn, "CREATE TABLE test_lob (id tinyint, stuff varbinary(max))" ); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); -} -sqlsrv_free_stmt( $stmt ); - -$lob = fopen( "mystr://test_data", "rb" ); -if( !$lob ) { - die( "failed opening test stream.\n" ); -} -$stmt = sqlsrv_query( $conn, "INSERT INTO test_lob (id, stuff) VALUES (?,?)", array( 1, array( $lob, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max')))); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); +// In a Linux CI environment use a smaller size +if (isServerInLinux($conn)) { + $limit /= 100; } -while( $result = sqlsrv_send_stream_data( $stmt )) { +$stmt = sqlsrv_query($conn, "IF OBJECT_ID('test_lob', 'U') IS NOT NULL DROP TABLE test_lob"); +if ($stmt !== false) { + sqlsrv_free_stmt($stmt); +} + +$stmt = sqlsrv_query($conn, "CREATE TABLE test_lob (id tinyint, stuff varbinary(max))"); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} +sqlsrv_free_stmt($stmt); + +$lob = fopen("mystr://test_data", "rb"); +if (!$lob) { + die("failed opening test stream.\n"); +} +$stmt = sqlsrv_query($conn, "INSERT INTO test_lob (id, stuff) VALUES (?,?)", array( 1, array( $lob, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max')))); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} + +while ($result = sqlsrv_send_stream_data($stmt)) { ++$packets; } -if( $result === false ) { - die( print_r( sqlsrv_errors(), true )); -} -echo "$packets sent.\n"; - -$stmt = sqlsrv_query( $conn, "SELECT LEN(stuff) FROM test_lob" ); -if( $stmt === false ) { - die( print_r( sqlsrv_errors(), true )); -} -while( $result = sqlsrv_fetch_array( $stmt )) { - print_r( $result ); +if ($result === false) { + die(print_r(sqlsrv_errors(), true)); } -sqlsrv_query( $conn, "DROP TABLE test_lob" ); +// Number of packets sent should be $limit / 8192 (rounded up) +// Length of the varbinary = $packetsSent * 8192 + 1 (HEX 30 appended at the end) +$packetsSent = ceil($limit / 8192); +$length = $packetsSent * 8192 + 1; +if ($packets != $packetsSent) { + echo "$packets sent.\n"; +} -sqlsrv_free_stmt( $stmt ); -sqlsrv_close( $conn ); +$stmt = sqlsrv_query($conn, "SELECT LEN(stuff) FROM test_lob"); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC)) { + if ($result[0] != $length) { + print_r($result); + } +} + +sqlsrv_query($conn, "DROP TABLE test_lob"); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); sleep(10); // since this is a long test, we give the database some time to finish + +echo "Done\n"; ?> --EXPECT-- -2442 sent. -Array -( - [0] => 20004865 - [] => 20004865 -) +Done diff --git a/test/functional/sqlsrv/test_scrollable.phpt b/test/functional/sqlsrv/test_scrollable.phpt index 7e67a597..524da151 100644 --- a/test/functional/sqlsrv/test_scrollable.phpt +++ b/test/functional/sqlsrv/test_scrollable.phpt @@ -1,5 +1,5 @@ --TEST-- -scrollable result sets. +Scrollable result sets with a simple test for an expected error. --SKIPIF-- --FILE-- @@ -69,6 +69,13 @@ for ($i = 1; $i <= $numRows; $i++) { } $query = "SELECT * FROM $tableName"; +$options = array('Scrollable' => 'dummy'); +$stmt = sqlsrv_query($conn, $query, array(), $options); +if ($stmt !== false) { + fatalError("Expect dummy scrollable to fail!\n"); +} +print_r(sqlsrv_errors()); + $options = array('Scrollable' => SQLSRV_CURSOR_FORWARD); $stmt = sqlsrv_query($conn, $query, array(), $options); @@ -205,4 +212,17 @@ echo "Test succeeded.\n"; ?> --EXPECT-- +Array +( + [0] => Array + ( + [0] => IMSSP + [SQLSTATE] => IMSSP + [1] => -54 + [code] => -54 + [2] => The value passed for the 'Scrollable' statement option is invalid. Please use 'static', 'dynamic', 'keyset', 'forward', or 'buffered'. + [message] => The value passed for the 'Scrollable' statement option is invalid. Please use 'static', 'dynamic', 'keyset', 'forward', or 'buffered'. + ) + +) Test succeeded. diff --git a/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt b/test/functional/sqlsrv/test_sqlsrv_phptype_stream.phpt index cdf1453510012a233933bed64095435bb02bffb6..908e5d4d4766b54a2d0dc2aca64ad26053a4983d 100644 GIT binary patch delta 143 zcmeAV+!{FHbG?STZ*g#HNoj#zW?r(orZq2@0uZPq=jY`q*eVnk<`fr|#e;aM$t4;p z#i>PQsYN;vd7wyUex82;h%AQb%uLgO>9)01NK4EqPF2uUsD>B|GCD1>Br(Ues3^Zk RL&+_1vLB<&<_t!4bpT!QERg^J delta 80 zcmdlQ*dI9IvqOA-UUI62l5eqder|4lo?d2NvXZ7XFBdPD0uZPq=jY`q*eZai)Z`Kk YkPJvXGfhE55iB`5oY8#q1V&YL02_4|)c^nh From 8aaace8e469e6a570e784614bfabcebfd53e2805 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Tue, 3 Dec 2019 13:28:29 -0800 Subject: [PATCH 5/7] 5.7.1-preview (#1064) --- .travis.yml | 4 +- CHANGELOG.md | 33 ++ Linux-mac-install.md | 88 +-- README.md | 36 +- appveyor.yml | 8 +- azure-pipelines.yml | 4 +- media/os_development.PNG | Bin 21753 -> 25526 bytes media/os_production.PNG | Bin 22158 -> 25457 bytes media/php_versions.PNG | Bin 11432 -> 12929 bytes media/sql_server.PNG | Bin 17235 -> 19816 bytes source/pdo_sqlsrv/pdo_dbh.cpp | 72 ++- source/pdo_sqlsrv/pdo_stmt.cpp | 52 +- source/pdo_sqlsrv/pdo_util.cpp | 4 + source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 12 +- source/shared/core_conn.cpp | 47 +- source/shared/core_sqlsrv.h | 14 +- source/shared/core_stmt.cpp | 101 ++-- source/shared/version.h | 2 +- source/sqlsrv/php_sqlsrv_int.h | 3 + source/sqlsrv/stmt.cpp | 23 + test/functional/pdo_sqlsrv/AE_v2_values.inc | 163 ++++++ test/functional/pdo_sqlsrv/MsSetup.inc | 2 + .../pdo_1018_emulate_prepare_natl_char.phpt | 120 ++++ .../pdo_1018_quote_param_str_natl_char.phpt | 93 ++++ .../pdo_1018_real_prepare_natl_char.phpt | 131 +++++ .../pdo_sqlsrv/pdo_1027_query_timeout.phpt | 198 +++++++ .../pdo_569_query_varcharmax_ae.phpt | 81 +++ .../pdo_sqlsrv/pdo_AE_functions.inc | 488 +++++++++++++++++ .../pdo_ae_azure_key_vault_keywords.phpt | 29 +- .../pdo_sqlsrv/pdo_aev2_ce_enabled.phpt | 93 ++++ .../pdo_aev2_encrypt_plaintext.phpt | 136 +++++ .../pdo_sqlsrv/pdo_aev2_keywords.phpt | 60 ++ .../pdo_aev2_reencrypt_encrypted.phpt | 109 ++++ .../pdo_aev2_wrong_attestation.phpt | 95 ++++ test/functional/pdo_sqlsrv/skipif_not_hgs.inc | 36 ++ test/functional/pdo_sqlsrv/skipif_old_php.inc | 10 + test/functional/setup/AEV2Cert.pfx | Bin 0 -> 2654 bytes test/functional/setup/ae_keys.sql | 91 ++- test/functional/setup/setup_dbs.py | 2 + test/functional/sqlsrv/AE_v2_values.inc | 163 ++++++ test/functional/sqlsrv/MsSetup.inc | 2 + test/functional/sqlsrv/TC34_PrepAndExec.phpt | 1 + test/functional/sqlsrv/TC42_FetchField.phpt | 1 + test/functional/sqlsrv/TC43_FetchData.phpt | 1 + test/functional/sqlsrv/TC44_FetchArray.phpt | 1 + test/functional/sqlsrv/TC45_FetchObject.phpt | 1 + .../sqlsrv/TC46_FetchNextResult.phpt | 1 + .../sqlsrv/TC48_FetchScrollable.phpt | 1 + test/functional/sqlsrv/TC51_StreamRead.phpt | 4 +- .../sqlsrv/TC55_StreamScrollable.phpt | 1 + test/functional/sqlsrv/skipif_not_hgs.inc | 36 ++ .../functional/sqlsrv/sqlsrv_AE_functions.inc | 518 ++++++++++++++++++ .../sqlsrv_ae_azure_key_vault_keywords.phpt | 34 +- .../sqlsrv/sqlsrv_ae_fetch_phptypes.phpt | 1 - .../sqlsrv/sqlsrv_aev2_ce_enabled.phpt | 113 ++++ .../sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt | 138 +++++ .../sqlsrv/sqlsrv_aev2_keywords.phpt | 71 +++ .../sqlsrv_aev2_reencrypt_encrypted.phpt | 110 ++++ .../sqlsrv/sqlsrv_aev2_wrong_attestation.phpt | 93 ++++ .../sqlsrv/srv_1027_query_timeout.phpt | 120 ++++ .../sqlsrv/srv_569_query_varcharmax_ae.phpt | 96 ++++ .../functional/sqlsrv/test_ae_keys_setup.phpt | 8 +- .../sqlsrv/test_stream_large_data.phpt | 1 + 63 files changed, 3757 insertions(+), 199 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/AE_v2_values.inc create mode 100644 test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_1027_query_timeout.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_AE_functions.inc create mode 100644 test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt create mode 100644 test/functional/pdo_sqlsrv/skipif_not_hgs.inc create mode 100644 test/functional/pdo_sqlsrv/skipif_old_php.inc create mode 100644 test/functional/setup/AEV2Cert.pfx create mode 100644 test/functional/sqlsrv/AE_v2_values.inc create mode 100644 test/functional/sqlsrv/skipif_not_hgs.inc create mode 100644 test/functional/sqlsrv/sqlsrv_AE_functions.inc create mode 100644 test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt create mode 100644 test/functional/sqlsrv/srv_1027_query_timeout.phpt create mode 100644 test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt diff --git a/.travis.yml b/.travis.yml index 0618bebf..e368b84e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,10 @@ env: - TEST_PHP_SQL_PWD=Password123 before_install: - - docker pull mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu + - docker pull mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 install: - - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP2.4-ubuntu + - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP3.2-ubuntu - docker build --build-arg PHPSQLDIR=$PHPSQLDIR -t msphpsql-dev -f Dockerfile-msphpsql . before_script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 38271771..c18663be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.7.1-preview - 2019-12-03 +Updated PECL release packages. Here is the list of updates: + +### Added +- Support for PHP 7.4 +- Support for Red Hat 8 and macOS Catalina (10.15) +- Feature Request [#1018](https://github.com/microsoft/msphpsql/issues/1018) - support for [PHP extended string types](https://github.com/microsoft/msphpsql/wiki/Features#natlTypes) - Pull Request [#1043](https://github.com/microsoft/msphpsql/pull/1043) +- [Always Encrypted with secure enclaves](https://github.com/microsoft/msphpsql/wiki/Features#alwaysencryptedV2), which requires [MS ODBC Driver 17.4+](https://docs.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver15) and [SQL Server 2019](https://www.microsoft.com/sql-server/sql-server-2019) + +### Removed +- Dropped support for [PHP 7.1](https://www.php.net/supported-versions.php) + +### Fixed +- Issue [#1027](https://github.com/microsoft/msphpsql/issues/1027) - Fixed how drivers handle query timeout settings +- Pull Request [#1049](https://github.com/microsoft/msphpsql/pull/1049) - performance improvement for fetching from tables with many columns - cached the derived php types with column metadata to streamline data retrieval + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - Issue [#1050](https://github.com/microsoft/msphpsql/issues/1050) - With Always Encrypted enabled, insertion requires the column list for any tables with identity columns + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Data Classification metadata retrieval requires ODBC Driver 17.4.2.1+ and [SQL Server 2019](https://www.microsoft.com/sql-server/sql-server-2019) +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) + ## 5.7.0-preview - 2019-09-05 Updated PECL release packages. Here is the list of updates: diff --git a/Linux-mac-install.md b/Linux-mac-install.md index 2a96c89c..fb317515 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,50 +1,50 @@ # Linux and macOS Installation Tutorial for the Microsoft Drivers for PHP for SQL Server -The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, Apache, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 18.10, RedHat 7, Debian 8 and 9, Suse 12 and 15, and macOS 10.11, 10.12, 10.13, and 10.14. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver##loading-the-driver-at-php-startup). +The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, Apache, and the Microsoft Drivers for PHP for SQL Server on Ubuntu, RedHat, Debian, Suse, and macOS. These instructions advise installing the drivers using PECL, but you may also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver##loading-the-driver-at-php-startup). -These instructions install PHP 7.3 by default. Note that some supported Linux distros default to PHP 7.0 or earlier, which is not supported for the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.1 or 7.2 instead. +These instructions install PHP 7.4 by default. Note that some supported Linux distros default to PHP 7.1 or earlier, which the PHP drivers for SQL Server no longer support. When installing PHP 7.2 or above, please read the notes at the beginning of each section below. ## Contents of this page: - [Installing the drivers on Ubuntu 16.04, 18.04, and 19.04](#installing-the-drivers-on-ubuntu-1604-1804-and-1904) -- [Installing the drivers on Red Hat 7](#installing-the-drivers-on-red-hat-7) +- [Installing the drivers on Red Hat 7 and 8](#installing-the-drivers-on-red-hat-7-and-8) - [Installing the drivers on Debian 8, 9 and 10](#installing-the-drivers-on-debian-8-9-and-10) - [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) -- [Installing the drivers on macOS Sierra, High Sierra, and Mojave](#installing-the-drivers-on-macos-sierra-high-sierra-and-mojave) +- [Installing the drivers on macOS Sierra, High Sierra, Mojave, and Catalina](#installing-the-drivers-on-macos-sierra-high-sierra-mojave-and-catalina) ## Installing the drivers on Ubuntu 16.04, 18.04, and 19.04 > [!NOTE] -> To install PHP 7.1 or 7.2, replace 7.3 with 7.1 or 7.2 in the following commands. +> To install PHP 7.3 or 7.2, replace 7.4 with 7.3 or 7.2 in the following commands. ### Step 1. Install PHP ``` sudo su add-apt-repository ppa:ondrej/php -y apt-get update -apt-get install php7.3 php7.3-dev php7.3-xml -y --allow-unauthenticated +apt-get install php7.4 php7.4-dev php7.4-xml -y --allow-unauthenticated ``` ### Step 2. Install prerequisites Install the ODBC driver for Ubuntu by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv -sudo pecl install pdo_sqlsrv +sudo pecl install sqlsrv-5.7.1preview +sudo pecl install pdo_sqlsrv-5.7.1preview sudo su -printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini -printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini exit -sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv +sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv ``` If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. ### Step 4. Install Apache and configure driver loading ``` sudo su -apt-get install libapache2-mod-php7.3 apache2 +apt-get install libapache2-mod-php7.4 apache2 a2dismod mpm_event a2enmod mpm_prefork -a2enmod php7.3 +a2enmod php7.4 exit ``` ### Step 5. Restart Apache and test the sample script @@ -53,10 +53,10 @@ sudo service apache2 restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Red Hat 7 +## Installing the drivers on Red Hat 7 and 8 > [!NOTE] -> To install PHP 7.1 or 7.2, replace remi-php73 with remi-php71 or remi-php72 respectively in the following commands. +> To install PHP 7.3 or 7.2, replace remi-php74 with remi-php73 or remi-php72 respectively in the following commands. ### Step 1. Install PHP @@ -67,14 +67,14 @@ wget https://rpms.remirepo.net/enterprise/remi-release-7.rpm rpm -Uvh remi-release-7.rpm epel-release-latest-7.noarch.rpm subscription-manager repos --enable=rhel-7-server-optional-rpms yum install yum-utils -yum-config-manager --enable remi-php73 +yum-config-manager --enable remi-php74 yum update yum install php php-pdo php-xml php-pear php-devel re2c gcc-c++ gcc ``` ### Step 2. Install prerequisites -Install the ODBC driver for Red Hat 7 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). +Install the ODBC driver for Red Hat 7 and 8 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). -Compiling the PHP drivers with PECL with PHP 7.2 or 7.3 requires a more recent GCC than the default: +In some versions of Red Hat 7, compiling the PHP drivers with PECL and PHP 7.2 requires a more recent GCC than the default: ``` sudo yum-config-manager --enable rhel-server-rhscl-7-rpms sudo yum install devtoolset-7 @@ -82,8 +82,8 @@ scl enable devtoolset-7 bash ``` ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv -sudo pecl install pdo_sqlsrv +sudo pecl install sqlsrv-5.7.1preview +sudo pecl install pdo_sqlsrv-5.7.1preview sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini @@ -91,9 +91,9 @@ exit ``` An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): ``` -pecl download sqlsrv -tar xvzf sqlsrv-5.7.0.tgz -cd sqlsrv-5.7.0/ +pecl download sqlsrv-5.7.1preview +tar xvzf sqlsrv-5.7.1preview.tgz +cd sqlsrv-5.7.1preview/ phpize ./configure --with-php-config=/usr/bin/php-config make @@ -120,7 +120,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Debian 8, 9 and 10 > [!NOTE] -> To install PHP 7.1 or 7.2, replace 7.3 in the following commands with 7.1 or 7.2. +> To install PHP 7.3 or 7.2, replace 7.4 in the following commands with 7.3 or 7.2. ### Step 1. Install PHP ``` @@ -129,7 +129,7 @@ apt-get install curl apt-transport-https wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list apt-get update -apt-get install -y php7.3 php7.3-dev php7.3-xml +apt-get install -y php7.4 php7.4-dev php7.4-xml ``` ### Step 2. Install prerequisites Install the ODBC driver for Debian by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). @@ -143,23 +143,23 @@ locale-gen ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv -sudo pecl install pdo_sqlsrv +sudo pecl install sqlsrv-5.7.1preview +sudo pecl install pdo_sqlsrv-5.7.1preview sudo su -printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini -printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini exit -sudo phpenmod -v 7.3 sqlsrv pdo_sqlsrv +sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv ``` If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. ### Step 4. Install Apache and configure driver loading ``` sudo su -apt-get install libapache2-mod-php7.3 apache2 +apt-get install libapache2-mod-php7.4 apache2 a2dismod mpm_event a2enmod mpm_prefork -a2enmod php7.3 +a2enmod php7.4 ``` ### Step 5. Restart Apache and test the sample script ``` @@ -173,9 +173,9 @@ To test your installation, see [Testing your installation](#testing-your-install > In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. > [!NOTE] -> Packages for PHP 7.3 are not available for Suse 12. -> To install PHP 7.1, replace the repository URL below with the following URL: - `https://download.opensuse.org/repositories/devel:/languages:/php:/php71//devel:languages:php:php71.repo`. +> Packages for PHP 7.4 are not available for Suse 12. +> To install PHP 7.3, replace the repository URL below with the following URL: + `https://download.opensuse.org/repositories/devel:/languages:/php:/php73//devel:languages:php:php73.repo`. > To install PHP 7.2, replace the repository URL below with the following URL: `https://download.opensuse.org/repositories/devel:/languages:/php:/php72//devel:languages:php:php72.repo`. @@ -194,8 +194,8 @@ Install the ODBC driver for Suse by following the instructions on the [Linux and > If you get an error message saying `Connection to 'pecl.php.net:443' failed: Unable to find the socket transport "ssl"`, edit the pecl script at /usr/bin/pecl and remove the `-n` switch in the last line. This switch prevents PECL from loading ini files when PHP is called, which prevents the OpenSSL extension from loading. ``` -sudo pecl install sqlsrv -sudo pecl install pdo_sqlsrv +sudo pecl install sqlsrv-5.7.1preview +sudo pecl install pdo_sqlsrv-5.7.1preview sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/sqlsrv.ini @@ -216,7 +216,7 @@ sudo systemctl restart apache2 ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on macOS Sierra, High Sierra, and Mojave +## Installing the drivers on macOS Sierra, High Sierra, Mojave, and Catalina If you do not already have it, install brew as follows: ``` @@ -224,18 +224,18 @@ If you do not already have it, install brew as follows: ``` > [!NOTE] -> To install PHP 7.1 or 7.2, replace php@7.3 with php@7.1 or php@7.2 respectively in the following commands. +> To install PHP 7.3 or 7.2, replace php@7.4 with php@7.3 or php@7.2 respectively in the following commands. ### Step 1. Install PHP ``` brew tap brew tap homebrew/core -brew install php@7.3 +brew install php@7.4 ``` PHP should now be in your path -- run `php -v` to verify that you are running the correct version of PHP. If PHP is not in your path or it is not the correct version, run the following: ``` -brew link --force --overwrite php@7.3 +brew link --force --overwrite php@7.4 ``` ### Step 2. Install prerequisites @@ -248,8 +248,8 @@ brew install autoconf automake libtool ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv -sudo pecl install pdo_sqlsrv +sudo pecl install sqlsrv-5.7.1preview +sudo pecl install pdo_sqlsrv-5.7.1preview ``` ### Step 4. Install Apache and configure driver loading ``` @@ -261,7 +261,7 @@ apachectl -V | grep SERVER_CONFIG_FILE ``` and substitute the path for `httpd.conf` in the following commands: ``` -echo "LoadModule php7_module /usr/local/opt/php@7.3/lib/httpd/modules/libphp7.so" >> /usr/local/etc/httpd/httpd.conf +echo "LoadModule php7_module /usr/local/opt/php@7.4/lib/httpd/modules/libphp7.so" >> /usr/local/etc/httpd/httpd.conf (echo ""; echo "SetHandler application/x-httpd-php"; echo "";) >> /usr/local/etc/httpd/httpd.conf ``` ### Step 5. Restart Apache and test the sample script diff --git a/README.md b/README.md index 842a99b1..c89e8a24 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ **Welcome to the Microsoft Drivers for PHP for Microsoft SQL Server** -The Microsoft Drivers for PHP for Microsoft SQL Server are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2008 R2 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-2017) to handle the low-level communication with SQL Server. +The Microsoft Drivers for PHP for Microsoft SQL Server are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2008 R2 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server][odbcdoc] to handle the low-level communication with SQL Server. -This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.1+ with improvements on both drivers and some [limitations](https://github.com/Microsoft/msphpsql/releases). Upcoming releases will contain additional functionalities, bug fixes, and more. +This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.1+ with improvements on both drivers and some limitations. Upcoming [releases][releases] will contain additional functionalities, bug fixes, and more. ## Take our survey -Thank you for taking the time to participate in our last survey. You can continue to help us improve by letting us know how we are doing and how you use PHP by taking our December pulse survey: +Thank you for taking the time to participate in the [sentiment survey](https://github.com/microsoft/msphpsql/wiki/Survey-Results). You can continue to help us improve by letting us know how we are doing and how you use [PHP][phpweb]: @@ -25,7 +25,7 @@ Azure Pipelines | AppVeyor (Windows) | Travis CI (Linux) | Co [az-image]: https://dev.azure.com/sqlclientdrivers-ci/msphpsql/_apis/build/status/Microsoft.msphpsql?branchName=dev [Coverage Coveralls]: https://coveralls.io/repos/github/microsoft/msphpsql/badge.svg?branch=dev [coveralls-site]: https://coveralls.io/github/microsoft/msphpsql?branch=dev -[Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/dev/graph/badge.svg +[Coverage Codecov]: https://codecov.io/gh/microsoft/msphpsql/branch/master/graph/badge.svg [codecov-site]: https://codecov.io/gh/microsoft/msphpsql ## Get Started @@ -40,22 +40,22 @@ Azure Pipelines | AppVeyor (Windows) | Travis CI (Linux) | Co ## Announcements - Please visit the [blog][blog] for more announcements. + Please follow [SQL Server Drivers][sqldrivers] for announcements. ## Prerequisites For full details on the system requirements for the drivers, see the [system requirements](https://docs.microsoft.com/sql/connect/php/system-requirements-for-the-php-sql-driver) on Microsoft Docs. On the client machine: -- PHP 7.1.x, 7.2.x (7.2.0 and up on Unix, 7.2.1 and up on Windows), or 7.3.x -- [Microsoft ODBC Driver 17, Microsoft ODBC Driver 13, or Microsoft ODBC Driver 11](https://docs.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server) +- PHP 7.2.x (7.2.0 and up on Unix, 7.2.1 and up on Windows), 7.3.x, or 7.4.x +- [Microsoft ODBC Driver 17, Microsoft ODBC Driver 13, or Microsoft ODBC Driver 11][odbcdoc] - If using a Web server such as Internet Information Services (IIS) or Apache, it must be configured to run PHP On the server side, Microsoft SQL Server 2008 R2 and above on Windows are supported, as are Microsoft SQL Server 2016 and above on Linux. ## Building and Installing the Drivers on Windows -The drivers are distributed as pre-compiled extensions for PHP found on the [releases page](https://github.com/Microsoft/msphpsql/releases). They are available in thread-safe and non thread-safe versions, and in 32-bit and 64-bit versions. The source code for the drivers is also available, and you can compile them as thread safe or non-thread safe versions. The thread safety configuration of your web server will determine which version you need. +The drivers are distributed as pre-compiled extensions for PHP found on the [releases page][releases]. They are available in thread-safe and non thread-safe versions, and in 32-bit and 64-bit versions. The source code for the drivers is also available, and you can compile them as thread safe or non-thread safe versions. The thread safety configuration of your web server will determine which version you need. If you choose to build the drivers, you must be able to build PHP 7.* without including these extensions. For help building PHP on Windows, see the [official PHP website][phpbuild]. For details on compiling the drivers, see the [documentation](https://github.com/Microsoft/msphpsql/tree/dev/buildscripts#windows) -- an example buildscript is provided, but you can also compile the drivers manually. @@ -65,13 +65,13 @@ Finally, if running PHP in a Web server, restart the Web server. ## Install (UNIX) -For full instructions on installing the drivers on all supported Unix platforms, see [the installation instructions on Microsoft Docs](https://docs.microsoft.com/sql/connect/php/installation-tutorial-linux-mac). +For full instructions on installing the drivers on all supported Unix platforms, see [the installation instructions on Microsoft Docs][unixinstructions]. ## Sample Code For PHP code samples, please see the [sample](https://github.com/Microsoft/msphpsql/tree/master/sample) folder or the [code samples on Microsoft Docs](https://docs.microsoft.com/sql/connect/php/code-samples-for-php-sql-driver). ## Limitations and Known Issues -Please refer to [Releases](https://github.com/Microsoft/msphpsql/releases) for the latest limitations and known issues. +Please refer to [Releases][releases] for the latest limitations and known issues. ## Version number The version numbers of the PHP drivers follow [semantic versioning](https://semver.org/): @@ -88,7 +88,7 @@ The version number may have trailing pre-release version identifiers to indicate - Build metadata may be denoted by a plus sign followed by 4 or 5 digits, such as `1.2.3-preview+5678` or `1.2.3+5678`. Build metadata does not figure into the precedence order. ## Future Plans -- Expand SQL Server 2016 feature support (example: Azure Active Directory) +- Expand SQL Server feature support (example: Azure Active Directory, Always Encrypted, etc.) - Add more verification/fundamental tests - Improve performance - Bug fixes @@ -109,7 +109,7 @@ Thank you! **Q:** What's next? -**A:** We will continue working on our future plans and releasing previews of upcoming [releases](https://github.com/Microsoft/msphpsql/releases) +**A:** We will continue working on our future plans and releasing previews of upcoming [releases][releases] **Q:** Is Microsoft taking pull requests for this project? @@ -127,20 +127,24 @@ This project has adopted the Microsoft Open Source Code of Conduct. For more inf **Documentation**: [Microsoft Docs Online][phpdoc]. -**Team Blog**: Browse our blog for comments and announcements from the team in the [team blog][blog]. +**SQL Server Drivers**: Please browse the articles for announcements of various [SQL Server Drivers][sqldrivers]. **Known Issues**: Please visit the [project on Github][project] to view outstanding [issues][issues] and report new ones. -[blog]: https://blogs.msdn.com/b/sqlphp/ +[sqldrivers]: https://techcommunity.microsoft.com/t5/SQL-Server/bg-p/SQLServer/label-name/SQLServerDrivers [project]: https://github.com/Microsoft/msphpsql [issues]: https://github.com/Microsoft/msphpsql/issues +[releases]: https://github.com/microsoft/msphpsql/releases + [phpweb]: https://php.net -[phpbuild]: https://wiki.php.net/internals/windows/stepbystepbuild +[phpbuild]: https://wiki.php.net/internals/windows/stepbystepbuild_sdk_2 [phpdoc]: https://docs.microsoft.com/sql/connect/php/microsoft-php-driver-for-sql-server?view=sql-server-2017 -[PHPMan]: https://php.net/manual/install.unix.php +[odbcdoc]: https://docs.microsoft.com/sql/connect/odbc/microsoft-odbc-driver-for-sql-server?view=sql-server-2017 + +[unixinstructions]: https://docs.microsoft.com/sql/connect/php/installation-tutorial-linux-mac diff --git a/appveyor.yml b/appveyor.yml index ae779335..ef9870ef 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,7 @@ environment: TEST_PHP_SQL_SERVER: (local)\SQL2017 SQL_INSTANCE: SQL2017 PHP_VC: 15 - PHP_MAJOR_VER: 7.2 + PHP_MAJOR_VER: 7.3 PHP_MINOR_VER: 11 PHP_EXE_PATH: x64\Release_TS THREAD: ts @@ -39,7 +39,7 @@ environment: THREAD: nts platform: x86 -# PHP_MAJOR_VER is PHP major version to build (7.2, 7.1) +# PHP_MAJOR_VER is PHP major version to build (7.2, 7.3) # PHP_MINOR_VER is PHP point release number (or latest for latest release) # PHP_VC is the Visual C++ version # PHP_EXE_PATH is the relative path from php src folder to php executable @@ -83,8 +83,8 @@ install: } - echo Downloading MSODBCSQL 17 # AppVeyor build works are x64 VMs and 32-bit ODBC driver cannot be installed on it - - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.4.1.1_x64.msi', 'c:\projects\msodbcsql_17.4.1.1_x64.msi') - - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.4.1.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL + - ps: (new-object net.webclient).DownloadFile('https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/msodbcsql_17.4.2.1_x64.msi', 'c:\projects\msodbcsql_17.4.2.1_x64.msi') + - cmd /c start /wait msiexec /i "c:\projects\msodbcsql_17.4.2.1_x64.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL - echo Checking the version of MSODBCSQL - reg query "HKLM\SOFTWARE\ODBC\odbcinst.ini\ODBC Driver 17 for SQL Server" - dir %WINDIR%\System32\msodbcsql*.dll diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f048788..23b9d72c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -60,7 +60,7 @@ jobs: - job: Linux pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-18.04' steps: - checkout: self clean: true @@ -85,7 +85,7 @@ jobs: sudo apt-get purge unixodbc sudo apt autoremove sudo curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - - curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list > mssql-release.list + curl https://packages.microsoft.com/config/ubuntu/18.04/prod.list > mssql-release.list sudo mv mssql-release.list /etc/apt/sources.list.d/ sudo apt-get update sudo ACCEPT_EULA=Y apt-get install msodbcsql17 mssql-tools diff --git a/media/os_development.PNG b/media/os_development.PNG index 5bd527901fe02cc98ea93ced0f231a351c8f0e57..6f2bd8611515d0fca455d7ffbd728f6b884735b2 100644 GIT binary patch literal 25526 zcmce;XIxY7ngxoYAR^#j6qF(g0wTRe2u%>_pr9axBE5$WQX?u@5Rl$Mq={5%p@o1b zy|++80@7>f0YZ|y(KBbxnK?7}%>8gbNiW+nuyDrip7uQoif>W0#};RSWg0g%BTotn z`j(T|sW#Uy))W+0n(FuOJ$`PsKE;q^Kh254Fisq;%%;Lg~BP|j~^F9uAeN6o|XgNE?S-O2i|V9o&(OELh-|2OTcelT;=Mg z1Qtc53_syU6S>oMqP)8HO44Ts9Spl0A5Zw4oAJtR?Dm^vcPY3(hm6-0=0us+TatnW zHF1W&=ncDF??n0>rJ2=XufZlyJ#*TD_uFX<+E?XVSu)t60lUj@aI6WEZch16=$yTU z<0z41Zm9D%q7O?%*ekGcy^ym%MuNE_?7O_hc`0^PzRvN)3nr0qYIbwNPatV2BsTG4 zr`BYSj=7&xT5GN}9i=I$|y`w-D+{ z@>`D@QLA67NgL(Te5p@BUDB>cm7^Eo-CSjj(J@g?ss;M{J(3O?X6o+@p_kL-mut}F zxhCUi3d!6wS|QMpsB5X2_e$eL$^=ICibH*umayMe=a-}O`k;X8@ChAP4%J?`VPw^w zsBu>B>a?ieB%4g{06Uv-H_lia5ghhfPS+KGvC@I=NlqfFb`nQ zH;=Vy8w0r}IgSaF;uZ>1*yZ-xewDD^f$p}iVSU&ooa3m|aQ*e;!|$d6B_V!c`z=$| zLW@Ls|DCf@SX23KB-qqJ20Z+Q>PDkb_u4Wo(i_Dp)G$NWyir1n+^b9vjLskx*LNSu zyUjcTuesucrkb)juBPlSg)$_q*AEq`Dm=q4Q-;){-Icqh_8_)B@&Q^QHdNK%P~ z#b;h=rdU(kKsWG)C}Kv*ZMeN$mpmF;|8l3M#kY2q1&z4aW|PQn0^NPulCD}$nwDB` z)T!q|Td^5$P;Q*I+Bc$GgX04jdbgLTPZK2fudYkInq6Td-E z=yfV`?`~9{I$J>HsHV^VM4YEAEyvnM^5CyL>Ac*RYCYGm8lXpz(Jt+dI1Nh4ns>)v zea%LxQDbpM5-<0P3$BP~j_;;m^~7z1-ZjK&EVKvge92SuaZ8;HFmYUx&A3gMPWI*l z^~g56Oa!HOJ6UvEi&J(L*HFj0wK3p40}ui|-(!3mHgG{@-FK?W#g6!TUX(39$;5B; z#kLD43rglD7ir)gC(Snu$t)N7Px3#0RW-hVw-}Xs?qgdYe590?UyG8PUzQldNbeZh9(4>FzHsRA_!k4{`zi4&{h}Ht~Yr3J#A6?EVvPYIqimD!`xXgl z6@lrpT9H!egCcsp*L(1AmBp=Kx}N5ssTk>n`Lwtm?B~&@Dtd~^#~!i90}owEJ**@y znW)#KXwBs@!AEQ z)Q75fp(Y=TbGxNOK-5d@9q3kaGoB=R^|H`a|IFvRFFVHE#CVdN3GSgH>n5)EAn&FU zDnI)MzS=7?qgbkXQBQtG1)0+?|Aky`mQ#QXD@2|1G=uBcT$JhB6Str6)7vQtpt2|p zkuHsYsANvCfbrh5=-rXM6l3n9#%Bf9nOb$Pl$s>tvQf2tZ`KkvK?+0rLr#^O%GG#y zz}*jh!9|d@%m6MCYu-{h6qM|8>czdk0&b7aRyi+mkOvaVNR`@K8E?{w=1*|5+v~?; z9)bKZ1RAAt7+$(&rkt0Li4Vp4wS}&Xk!B~1SXPMB^YUky>`QzpU%1@!ab0EaKD`xe)?Q*jL&4Tq(f%ZtmuN_uLSd7V5%7Z2BVs}zW`eVWP$U-rlZA={kctBnR# z;S>B)%4>EjuMcsLAtOCM?I9`DWUoHYB1qu(X|sTv&a0Ajv@i%7RcN+?d_wWrG4(XU zMG#v8n{)1&Z#_y%C{rI2dP#`dl|3e2+hORc0c{>+%I?wy4L<8qKjOfdr`1OBzb@{S zvZW8N-teB5bVlra0(mcR5EvoE&yp7%OmVNbPgV6Sdf$P^yLIoc;dyqupA$bMd1WY- zPV#48OCsyqOw5LGF4MURP}lB2tKt{KfJGKxQQ zJGd{sO0s~CJnLL7>P9ocGKy)#Sw*>%c4U~z&EuA3Z0*NzalHp3vwPx}-~G-bpycGR zCd!vnb#eKm>p|J57iPYpxl@Fk+i2Jjv(=#A^_H-Ps-}0YhVN~)UP!mahnRl|GbZ*a z8g9td$ZV&*OJJfS5~r?Vs)hs-DN&K~anfEF#Mkm66&SA{%NMX0dw|<4FiRr-#dr3) z<=)zyWL-K8!gE}oy~u@-6V9Y4!*`>U%=zhu&ohC!9h`UCnl@C?t?ex)VT35SDAf4s zyFk8&aL2kT6^9Q6aBr;bG{@>|is>$Y9y8t`Scs%5Yal4?EQh=+nWa zjV4r1V&R;c`Z{hkv)kmYt%cee7@Mz!FGkr>7-gcYgu{O7+myVVMlyutqQ)eA0PVH~N9MFk~l>o5_})66N0;y%wiS zUEQ?=6P6x-$|460AqU8>J#BL?(ES~@LLn=kGVx8g;sa7fozDTwR4-D{f;@HV#V5+6 z4UNet_IZSd7Wt9+R2IdxYwPccZ}vD><*HOP$d5SAtzLDf>5C1%ru*oDYwP~4lD4N4 zhN#;wg7{+?esW@}3#Ggvm^}o8TZRwy;x&qkPtN$8ysmy6hB=EHv9oI(F6;dQNvyia zz_AgKmOs_(M@4b}YRGkFi+fDmPiYlM$uGu}QqPzsLUgqpT1M~c*1y`hK=D@I@lSD{ z9r|w<@DDyw19iOR2SoVuTP zWAmb+dPL4z3BqA8t$QpmNa~7GD_%`r(rgS{3QNhEQ({`d)=1!Hb z;oNtG8T{~Tg22J4z?ia`d2W&)luhS}JdzCote7u(V9&OhOTUobUAbLlGkM&YGt{fI$w2|QQN(J_3BY_J} z4J2z~cv5YT`_MT%d2@DXzlOsH3hw8`R*7g7Ay=L}S7YLm51b@L?0u>Ju94DsUky!? zA|sJmo+}im#W0c%GnLtvs>Q7f!&@-gib!tLxlB@!!qrqiSh z)c&)1mB&M5(rSz!kDx0HO^+%!UuJY{$R$?LbL2{uRp*oL2I0eBCJfb%g24;d$gwXr zSC`Wc>jIH5#?2Ao&Nk%Co=%>P49_tkQ|sjH_3wlGQ(q9WOBGmdr_=b?Htb~DyBsN0 z*a-4G?~3ZoKh1qND0>Gwb^f|Q!Pe+_sY7Pjb@=@+QK`74vbrq!>NOO=z2YYyt$b;B~rSZPrv#+y<{hJu()g6!?ez) zf22gs-`=D?CSktrTJ}yp|6uj3@{qTIt(l{aX@PJ)cly4JFH%@iF1TJ{d1i+>O=^K{ zxfbiuUVU~P!bkW_x z3)YxweMSdpdmj7f3-of*p%{%g%*+77A5*UX`HN}CyI2>s!V!;pZU=fynaSz(-p>+i zq{^?HD&#PA*N^WxpZFRpYouE$*iU@IsWnF~4T&JD#}r!QZSntL;CYi_n z3?s6u9J2tZHKZceUwYiiJU7nVX!C|==x{@HM>p}KbvYj^Vi&`dZ}3qmizkvppkABt zPQ_Yy4udZkB6Jw1N2ryO#*PJ?{$ul3JUG-3X#Rm7pX`;;In-n9;TLQ!ZY+ z3a`D{V?9Wl6LKjX8f28Px*qJ;rI@T!wq|3r!VaD|pAf#AzlN%ZrA#MgmbYDsVO27)#?K7gir+0b|lX*{U3ekd=_=}hn9 z!h<37!?SuhVMHBXx9e>pT~3^)9G+}RQN9|e03>RPp8+suz}y}o7(eG~5gct*T3CC^ zY?u=K=W}<98&4f+bZ&whDYA^r82QFM{^INM7v5dSNh_Mz4viU)P60LdE1Fz}8Edi_ zsd}({x}>hYuMsry%S+5w8X`30ld}*uU0swITYdkhmzAUTrNbwsMJlh1HhGkleT{rk zLWMhLUZzT)-KZQnW}Sb%;ZaK8K^Ruj4cVxVt?Ej(>Xn%f-gHf{X-StVFlj+2T?XsP zeA{Ci^6J6m8u|Fo-;B_+o4zZ;PkfxuXVWGe&T=Q{WaM40RU{uQfSj?;8D$JTIyv6E z$&p@KBHoZYQ^tPCY7D={zy}4LJj1+Cg0t7eOY#I2GV`QX#adEVCzU^*)1OPxPrHB&NPIyYA^L37MH1b#{OkbCoHhn zWqE0$o1-Rqsm4aM4##uAyuR(=IW%}7j^9Q@OfI^2uT*A5P{kJZrFgPo-Y<#OU&ArKp1c^F@MnB#SDJ~H=o-gdJDgWA zWlXC`jvFtHq&WcW5OsK{37#K0ami&@_|5GIwK+|_^YXipC79T>Pa$L&MGduIaY|Rz z4t}74pyovg4O14dETk7D!^-x8E!=P4rRsFAju9F9mcDBYM*tw!_txYSZ0~!x3)cC& zQ2xzt%Xgu~29CRuCcfP@4yQchP=k%5g(?FN*Amh8&%e^ey!5E z2l{K`2yMh49=nE1>~vJ1q?fkmUEzY1MAvul z5xiBhmbbc^@-NBbV_$67$x-2CU7lcuvlnl>v90fwV~E`cV5N@%T(t}4AmdRRg@ThC zeWg|rHv3%@xY0_&z=4j(S&L|;PzczfhhIoPaLm2a;nACrXD_pYHR8g@uwl4qI&;+6HskNj_@jcASkB^m9rEil=+sC~oF}Q05BCMM zc+04eE-iK@hykzo74tYLH3=g&G5!YHBOhN$g<)SrKt27#VHyT3M(%6wOFQQZnN3?N z`f?2#wgvF`i#4`DJ>aaek!rc;B6*bisNx8P^QXkJ(Et*H{<`s*%qM8z-HRj{U*+Vy8*}`%I%r0)uW3kZJ<>;iM=c94CieK8joX(-j7z)4D z)6ANn`Dw~1E^I2(;7QIBR>Yx9;Qiw%2Rc6Q_r!+|uNt||g1@gwsU)n`b&DDCs$NWw z4`3%O_IpLdSc`E>|20p`NwQo(t(@4&WKPfmE6f!u%cEPOz<~_zJDxrk+YtC z`EohWeX3R$sM=7@1JK$T4X?UA8>139VV;B1My9#^qfLSDnthib*Ebt_#C%vM%-=>H z^YQWN$#p^SphWt|4>V2lXr^2a%3P-ZH#$oHLq}(}6+aA@VVBSJ&PrhDx_`Z7XA|3( zWyRo1ppio#I8Y4=ZW+}=Dl{aagg3B`tX}T}t|{pSo@(V)Sqh2(?cLKv#-Ue-Pc0IL zE*g2Fo93&pI>}r(qbbcZtJI=GZ!Bmmm{z$Rpl_QDQ#56O~?J0;OsYv{wqH- zM`Iq0MyK(#-q(vk=yA{)U8_hFOmqP2c`CaB{-iqOvt?cU08wMk5K+!V7ZY-Em8!dZ zqr}u;k>=JD&+}-9!b4Ce&D6XZdknrHG~@J?FRxRgZTAf8(Fw5oygza<7wAq6FP7@lo*z!K$cth$D7*w_h=1$)rC~HEhZ;AjKU1Megw7V8B5AS9{&oqc#$v0xoPqL^k2HuMRtCHFQr;9e%G&=Q%P;E*ZK zr&RyIatThQtf~2L9Fly?ZAs0egisYSPy6|}S!eB*hRNd~bx2K>iT&rY#Us7JMdg_B zEszw@v>FAZt2xPdQat-(7q`}1XPHQd0`3hf9g1BX>fgk+P*qvF=O`g@neg+ffA&Mx z+sOWyHz~-e#|G!wRYyTgQ>o`%_;5w>s>+FkgqRh@@QOC)!b{Ew2DC{{u8LE3E|=AE z{&{uxQB+SgF@_1plfQ<#5}Ma0Oj$ht&zv_TMTn!T{uCuXFRN?W8yiQNd*2o}+Cx;{ zyk|9!2`oOly4`c+LUvv}8eEP~Iz{n9j@LiL3@PDw-6C=5#+0*$*i|0Mv1E6B%7v>1 z@n3IsNkdXTJCu=Eeh$CK`#*3sHulglU_q2QL8-JZt_w`0*uryVYs`;IJRz;oqj))b zRAK6kjH#tW|C*h#OUwwGFFd(vJNepvw};YVt&64Hbi(jKE0kqIhyszBI`{{lYM!Yz z{et0dLk%bw9j7gnUZO1yJpt|Jy%O8EbB1e?&yD_-9>1RUewSnC0%(LF*ymp&93A~8 z<`hL!#$O26_apF;rNjq4E9v;1o^Jp-jQM^>DV1g&AHl~%K6(Xz2cCP1{j8;`etzNt z%3S?j;Eoa4^5rYTcjzgm+GFtgIpF(eB*G5Db+%e^Ax7ZUP)*6Y26e(~%u{+vxCJAL;e+oE=P0o|vm!{||JT z`#;fTj!WB{yw#%ahGCWE(L~kAmPvADP8DlLn4E5@72daWs(o)P;k|-Oh7%hF#eI!i zl!&lhwCloLR{r&JtJa}q8S^g>Guzb+*c_;0TqX$T$BOJM+Ei6> z@`1Tyob82OZ*g0x`e{Aul8$ky%o%Z1mo!Q!$K*WAwAZxzHbdH&!XhstQXhkhW6WYz z0$eIT2S-p+yeQzjwglVQQQ++(&U^aUi>2c*`uK?~^-D2YQa0I%n8{zm=k;z6|JX{s z4M?-Y6KTeTPK`_`%6MAIRaDJY^*v^8-91Mq?tU6i@h;(k#x_Y9`}FA=cEfx1M1}dI zfd2biWcyivq-`+MgkEWvc5Ek8&O$yOswxv&RCJ~dAU^SAi2>}nxFvh_E)Rs@o(&{V zAi6=p9%i8Zx0rkSgCK$SB9C=rF?Zd?AW1jy4ktsc?n@zNYTsD{T;IabHcVtCTFxEcBPCebbL&+?dbiw$bJThYz}y@_P#UzE{elrnrIO zWM0~Hk=nUST*N3>FgmaY<#DmcrcEVizzr~^#rT!QS9Fv2lFgEpl*GnAB%@rEK?<~b z>!Itf=Mv9X!=9%kr~E5E0gmDio$&k&(WQK`iPn9SR#a)QgUQ!1u;}2RH|qj$wFKBV zh=jhiE`8wIoUBFpK`IROvw-_&p;B4ZUXS0(BB;ls+V?^5p)Ncd#Kc!&&d#`5Kj;aHIf68QO_S?VM~(!6fp*FCrI%Rv0@Eo%XqX=___!m|Y_%G&Io7U4)9!}D zoR&`Yef%!87-rh$zPd>Kl08#r)w;K`sJvr5f%4zOnHH2Da)Veh9Q7`oaSyujZNU{3 zAFtFFy%(W^=i#*2kdSe*agQMqJ!%;m4P3f{)#^4TQV!}z%!F4>TRyafjjfE@Xnz$e z(da@%o9rm;4U`KJ)UNxO)ZJ1_kHv7H~{nTEkvb-u-h z`Km}*JSt_4*QQ zNVchwv6ji7Zr+tt0-DTn`ws}sZvO+JI3fd8N;FaQbrV;Pl%3ITzmG2D4kV*ReW;8P?PSATstpJ=Fo0^3Ib_tVYxLsv!2ZZ< zx`0&6QVQbaGIGzkcCOMTpJx#LQ5)>6SL!8W+o<)uJ2P=)^nK1+oSGPj;irx|OL0Oa z3-|KnwzG33!-?7-`CZk-tW+&v1F2e`EsKET1zg|&^;?ug@^NxzJkoDN8Z43Ai;vF? zDrHZ(b+f}-U&?D$lO9%1$7g7nr@nT704X0>YxhD@T0rbmqt7 zuQbPnyw(Ge&GNCv(FwK7kd6rp;3V?z3-vqo9M`_NUamZ2uZa)>Y2Sf-Ov%L5^Bgmk z^UR$&(G23ryQe)q-Oq=ZC(Qd#IxUZGCxnQxPoCaLKnVN>&*qpU%s+!CyZ)VM5yFI5 zicdN-OkB*zoDO%esyA+_aLn|qbh569_ql9W3vZ_&%{FR)oO2IfU|C%}bhLQF@r~!G zjyi0lAT(8Iz>;%}WM!(?R<(9-W<@R6%slyxrYce=Xj;HsY@n>6m*AaxcQQULF7^rh zY~{$EclOt!axE%vlPVJ=$|L6_F7n32ti>Ny0+8D6A(aGYFQ^4waWN9+&2>z4DgH2C zI{Y54L)TnOXJp(H0OIl-^Dr;A^;0gN-B)f2d~`$@Z+{=ls()m)&~-L4-x{jmZ>P}` zXrxR9z*eC?3Ij_Bdi8K4qjRuSr7opfH}rH;!D8 zgbuZewW%a~Ly1RNjCH~H!DG;!li_4KcbW*&&8+^L{}kr#a-lR_UsGd{$_rlID6QMk z`S;S?XSbEpX1~x%$;~iDN3Vf_#(6zJJ*ViVZ_e<~0IFvND97{|#-mgBHVd%l#?yR0 zpC{^4qc=F^U4Z?J=>mMfUd_(I zJt3Pf4`6kC1euVGKI6@G8QV#xg$GtO9vz4fBihmV)V;~z(#wu6iRJ|~z$ga+qa3hJ zdboBySaMQ%4yYLOY2qwfU7jc?sQ+=0r;mQr%eT+Zo){TW#*ty&%-T1t@E zc@j%3Pbrt4z$6c*COGPKO4rj5ZBYoEU}iKS?`aSuBG_TL$ZSj4@NnTNaLLxd(N0;Y zhr#nM@bUiw=J-@gSFjMwbK?ZcUF)|s4ksDpZex8$p7>k<)+~eFF9|hsxQqHIUkvy~ zj0->yqbyUUJXBuIX=P6{dzZL-q@fv<8X-?X_yuRn|5s4HA2XD%+tmZY3Q*IteZ(Wl zM4zJe0{win`-B5vOYPj~r);9HCmpTMN2^}{i>SEkT^f+iy4jZ|&BY+M`%#vByaUCL zSw(dm?kuifqM&%K`ic65>E{sQfGS7)ea@RxP{ZxR1~wV5_Vtqb4H?iFC4~q-5GPy{ zq1)!w&4}YKsZok#2lJ>#vVGGBT7J)EFVj-gFOdkiL~F6->OrS|)P(fZ(f*9%lTutDQANI=3|GxaphjhWgpp}s zoaB``<&6_34cJgqF!Q(~xn~i{oq4#Fy>JWn&#_VxokF`u}$Gg}@;F(V0; z#)1VrU3%AP=6TxZ**c_nx`)gXJ%;wJ`nHtnx%A4o1Q7QGG)53Y#t|`I@2_}P0ru71 zaO+PnH=_Gt*UpSmU2pylPWoL#QA&T6zvV#Wrc398iW}o`pg5qmul=@6cQ6q-d|op~ z_xY;=M()4zuhkTYfX+H`<_=C5pWcrgevj!cRvV=cs2h~HhV=SwCcD4_xikFZ<^Uzd zMJJ$+G-g#Owh2z^MW##cr5Z1YnZ#Ta=y?=kQ5u)Oo=|onuEw4}phTd@*2u%hbizZX zCdH`hI+L~N5Q7E4>}M6GAX7T3V&Swt?P|BU$I&A2(-}~NfX@Dphg}N$@`!5^sF#QV zecJGA%U-qw93VvRYXaDt*>kOX(G>cc2J57kQhO`QXq3m*<%&yEZdZ8;tpk6(kt&aV zOqdb2^Vvp*zS4H#ljzUBJ7v{lNiK*5p6oU2+&K^y(7ZQB0esD@WTuB$gW^=&r8DQ~#kB+*w4?1GjxPbw){}O0Jh=Z># zF?P(^BmBC(5S;c~tn-YA+>!1Pnkzw0G)XY z5=4oil%~x*J+pxVm<|}uZ_|OTah28#?)UHvd(ei*6cnqILxo)mF28Z?*M0^)8mjj; zDjcVNIAudd$D^;0N$%CCF3#}} z0to8BdWiy^6#u`?ZknTN7C-_RA@-~_ zBBzeUz-K4|PykTQjeWB!FT40Wbwuia2a#}~><-&rg{Bls8ZG`YFcjXKd@^j)Mc+;| zC9R+6W8KA*490%88r9J&!v?kBIcC>nK-La<%cpYyUyMZorDuHDE*!Dw4RtxD!+Lt- z1f`Dj-*`9t%@R=vcm9NTa5>)MKs>@6dcq7zTS4$z1@EL3z;L9vLG?S;(KPxuA4FwC zZkOUk$NogXl5Ws^Tw36CPP!P9p>g?@2Z7(!m*6Mt^LISejAHs9(%((=V9$jQa~{&t zZqqLa`fo;vcq}e*NC#-CCs6}C^cjohQq38#f7|$Kx`l&O7D(F3^ljsi^M717RRSjU zf^lALr~bwYpLZ7fy68aY>3s!&Sv-DTF{~j1etI|%N~xbC;$X<(RqtAWjizQ)If91i zc0t&{iZuD433h6VrlRKzC%obkk0fl=&zs`>6VV=Bfh) zh+{_;%4{`ZZSA#PhF>dUpk}SSYb|M=8Qt1q$B}@SKQ;r~1miJYrD*?M12_yGWx?xq zVde+%>Rr{DwNR^?g%l@e>^=~DZ=xsc<}0ixj2{T@wCvs-*A4u|xV_hH+j8{aMN$ z*dTmu%YUU0rb9(?E*Txysv4{!zpvk=cZfyy5er&mP9i&FI8N3juv5Aq3Uv|$bWjlx(>>eC~w znJ97jIw!DyQK8j))yBP?_kl6#8@?(%<#IVAl>>2$7ipZ4I+` zO4+y4EmgkHt=k%_l$kyQ?AFtH`I(7NqCRA=UqPO1)S20cA$kz_FqJfH;#ALqt7um23VlZZ09Oz zr(Jv}uYYI!QsQgta)5q(%5=upW@!}Sa?AD3RW|D8Rqe5sSEhgh-VwRD`Tck7>WZvh z616yJ0GIhoxXC9x)>gbwq>DSI;{e4VETAJ8e@$9PaHvC|vl*z-yUFjXtE3E}8Wm|= z)9YM-_k7t==Zh}TeNvPK%53CXJPJ6V*Ty<6ZZ3ikJuwLy?qBk1meqzbF~wcR#Wyyr zeaC|@4jRw-2?*Hcj5=+*c6Ar3Ti;qu+`Vc_w;WouB(%S>RW%~-mIkzh)ugz>fjog$ zn@@z^1F_t&$}i1hEfa(EIr!;4;I8h60o0S-VO8xam6b4*;!`Z$cgPUO^xp{Jdz2t@ z3j$~7tEtenp{yZ4bml}_fA<-HVx!9nmo?RbtM1sUqP^#U#_@yGei{a++l=B?k_;i1 zu%{n;Q+(N-gAga>3D~W`3Rqg&5V(;` zc7FA)=nc&+YOsC4jwP z=l@Wp{_|9ze^?Q`)}1UK(=|WW6haA9tp@oz=^kwk`6rnE3cz&mT6;9tpuNQ0_fXpH z83x!>dGcXjeHJO}&GneYOc-YUdcc~fG>p_>3Jm+r-=(}nCDyHlZgAa)JF~AVOZg3Y zqi(gt^OYP0MFl4S$U=T(0#-?Rit@&^MJTDb4@%U~)ti$xAF*CK}k7i=%ybK5W# zb`$%}?P=p|3jch6gz8+sQ{-4J#f`ekC!~#fT!JrR)fC_kc+IR%ru=#&@^B$_YKHpy zb4B1zFI11}4mnoMO*oatXRAg{h)%91+hs(b2pOUJH8HR);VgV>bNtObf&q!rNyZ{T zQ~dDjh>>;dy44sBpitZusCpmW4Vbz5H-!S=wQw3aA&nS82kLfJQ6_4_>>tsy`A4&h ztJHHYTlUaDKR=RCl(W`!W?jQLZSBC%dt$;;`DA+3f@NiHjL>^{;x#@=0~?4zcu*nW z)tJ;S(^+c_KJB4R9NswkLr`ZR3sqqxE{?;uXwN=5Xh8&)gQtI$vXX_w%)|t6pZETn zk#yie8Jq0${>bs*W|y?1PCn_wCUt-bIRb~M=l2aU8~eC@(X}&?H`8i3C>L+1A>1y2 z`C(aRlgsIgyX{;>z}WWto=+Ue7bY1_*i>h3LJu{3mEk-siw>co4kJf zmHbWbZ*=e-TD#K=o&uo{(&_mHkpnZT*Zp4C`BOBZfvUR7mK4ed(0FC8Sz@ZLMtNw; z-Qdr-Q5)&>gv!Y$VJ{mKTE-l@hzXv8wFP6>sv4RhK5l-l8BN=y)Ayz#y_{Xm_1n$I zgaC~Y>im{P9GEZ13dC1;k`(%>?URJVE-6sX zxPWG_s|t$R9-MBb8JJba8O)@Se*0bC`hXgiLHFo}Wm!WOvLX16NXfk1%|ktP8I}4V zPtRz6mLt9?2CCBPpC0DqwH-F;Ri^OZk%3=?v& z9*t$UlihPg42lhDtv(a#q%!TOS{fmgScYli(m>IPD?zh8?=bnNo7R5AIy6YzF;oC` z8DMO+GnB;m6J$>;yn^sxT0iNihoo4#u%irB7S_Cm&YC+IUCakH-X0k+e3CMscwUlk z!jG-uN73$#5sY;b4-Hr|ryVp}HP@Hs<)?cOgf_?YIq^UBj@(w0VLLVK=W*@z(79Br zQ){$=--e$9ueG~j%Xd|W+%F`W%^5FA#+e7tIyamAW0kHN3_O!2|E)@=d}DSCb-asB z{})yIk0|ugXsh|wQ6e5=nf*tP|7mtsv{U(KjK6HZoVKkjmJ_Q)o{QD(%$MKetHm`E<`!Y3#i>l0qS@aZCR83m3S~C?7zI0?h2q<} zqS_6SM^Ahk05XE5^fDU*z~|-vsjzarl5ZlucyYu5m{iJM_Y}yf5(sdC$!g;@D-*yk zTC)M3F_uAv-7E%emr&HQ3|1?v@nO6{p-uf)wXI(*&mD8Q3lCqj*WI1+H|kWcj{;&} zUEYjsFAo2teo6|?-37$U<@0BygN`b}fn*}SD}<67{ad`K0%*glaM;m*({yp@Qrh2< zMoDTISJM-bce$L;^#;&j@>G^W+F@g z3VtxRG>{F@mI{YW9RNb>fc0m~SgTlCyb-+mD%$L97D zJolF_WCQL&)#VPvcIwxrw=uFI^YAZNqo>_!J=kQRL#i~oc2guDXm=*77?r)^q*3Qd zEpImcRRZ>ydrP>`-(|Kh2JHH9F*6Y$sPFWE&dLNTC*F+b-+zM%)qjPiqI3i8gk0IY z$Gp+zdkK=J*~8IQO|r!KLcK4=5ydv6z#qFxlJ>}&u&iaTh5zt)x@pTT=vuoKCStZ8p zP_$rE5yQkg8H4#8e87>MSG=jRT&&`>-*d(#i$O?1c5UDdvuF7?>$X)X57zQwn5NrE+UbL0`xxLW#JCniS<+?8{_sbDxWF4Yc~=q3j}3vp?Z96_c(Iydqso z&^Ip!#%#E&0*|Zzh$fo!pV|-odtm=xwBHHZ8S3{st(4E64ib@Y{p(;v?Hmj1lH_lw zM6=oZpDz-A{E0WvVscrsAoIc@DAa#w$;=uf`Q8)+bY6mpyz*N#e)xK?i1)we?*AtrpX5m2Q^QbV*D zp<)1OZ>_giiw{pIXnz-&6zKAI6+cq6dcZ_D+u!t|!S23)uKBt*pQeG>>qAW)$o!tZ z3gAu3UnE5OHnixRqIf2C;)uzJM**`6oP+9oPHb$&RbN1J9T_9j(xG+J{DQQLTt9-4@`SmNuU{_m58kgcBTtMPg91AiQE-l`f z&?sf-Sb38Z@t`Hho{8dx-~E!g2Uvq_^Q)4DzKuKS9xZgBv6~cQ^|x!UMa86!yI&Y` zkIb9BDue)fnNGJEMiauacK-O<96*bu5Vpxcy0-+r^^ZnLmoU;~qmi&tAh-vF3;(l3 zHgixDzI-0ANwh&z!vWzDf0}?aClEW<2h?0a#;m2>k~qJ<05raPRhK=__Ve?x?qMbV0o+j0r zs;rH^koL}PTr=10(!-X}G68O+((fFIK6x3}F*HT}H|?(fMfh>u-Yq(D%;f%E zTiB>fOi#beNrboK#DoRoYk+#mb3bdmz?lv)nvY^bz6;csO3~(9JZ9SaJex(!pEeis&;`KH7Y@Ld`_cmd zN3|73tMl|-1I@4WswP$iEjK)5cnUgl#*yyGXIo36d(qmeqeK%jzaA%2r>zR49dAr? z-inEFh9*G_GBG)}*5Axu0UNXw@(3Yy^1GG=RU`*VySU)72j6alC#MueG;Hy3k1M$~ zBz(URkN%JgI!)})IVE=-bkDGR?TnCfY?f0ELdd4${D?_nYqGdq&coa7%R~87GbaVZ ze=;QCY=@UN3;?Dh8}sUbPHXiUaYa_Uk*@QN`PmIW-n5#JF-9E;iu_|eCZ;)0R^6_5<^;wQfkT`DwL{DUswQFoAcNR3H2=N~`W^IU zd#AUKsG z3?i?~-GLnsdwSsW8?E!Zam?6YEzIuM#2R9s?9K-h_n%ih#%&U7TfjGKBHr}L3*dqd z5I&v98;E7I+P$UX-<24Oyz3|JP1XlKZzQ}iyNPNz()bV00l?Rj`pm6I5;fMi&U|7R z46lM(b&g{AT#{iu5#%%xh8t4HikWJ+=Kii0=@2JsQQt!xlPgf`xALt*&phadPO>?a zkGs^L*o7ud$%mykiE6b;pSKfcYteac(h;Gne574UKmqvseFV3J`1t^~QWl9?e(gu$ zo>R3ApYlna1S^FC?sOP{cYrrRYGozY2I8?qcIn{B%(_3;wR;Y?6v`Zi zn1lhM{B5w>*W!KBU7&!va`=0;;xniJAJQmK=O~FdALLDitsnXx#|oJVg$jhX|BTkN z5SXCC1WBAPWdjS_E0XouXaJl2Id2(k(L{V@&QLW3$e%=Fg{F=?V8lE~34=?PDUDXC z+GHd>%a|&1G9>7y{Ka4Xz@xi$<+sal3W#sF(?utBi+2-ICB_%|1m?Ld@+mE< zz~RKzZ+G;t|@Or5Kf?3o%hiD zTKK9NTs8Pn&rTa1b_D!;0;cl8N$aHb)V&gK4G+uueMW-D9gdpo%uL)?0U-|?cL|&d ztp}NYn zjoFE_>hrby;*1sx1n=*-ib_sR=)FH6R^ZWZs0(fdd6#F}UV65!?l?XGSg zzHD`O9!M1?$ITh2KLC;%IhO^r&kYX_zmK$@a(Kenju`b340qSG_-cFXXqGb+4MfvT zE&IP)wrY)D%#4XQP%hW$MM$4ZSp9%X*z?dU2e{>Pa_XR^sz%^2B@ku31k5}7>Df2g zIDWv;19bty8SL)jdv_;1e(Ug1RGu$mKXIIPeW(U`L-c*yD>u4cH>CC$g1*pToD7GMA$ z`W7?`P_*`K#?yIXC~S(Fcj#@PrELbx!XZ!amjVPrQ0~-$__?yF0ZbLkH?NEBJ(=Uz#qhX*vAkv!8%B8FRs7um&XwN%`9o0nZxzhgiB0Bl}7SwY_5b=|eN!=T+a*c+}v>myL|c!k~k=1hyn=JY*WRXlm0qi$BXlbwUHc8jpy zG%cm`uR2@Qkrr>xz_*62>~b5A-U3Z8w4R39-ce#Lu|DVmIK(O`((`h!+1;@8%fFL$ zSg@4{>hMqP7zI%D2=e(})%Gt}2Dp<szaywCGK*Lz*>GJnp@ecf~4zxjRd@AtF4oh%fE`rp${ zFdpW3=6HA4GBP*7>3jO_Q5Jv`^@#Mt{!vUoiWOc1jF5pncZHG5KvC*&3fc34-y&7H z>{~@-t<5*q9X73JIU=~Y{33XP%)wNZJ^Iq^1%y@(EihZKKyy;?xZ4G`#g+_VsAt0@ zF@6I=LxBS6IE&(d$i=TD*xIZ~Bc&&qjR$wZ#&7kMcS}~eky^6%@KKibKz^NkWF2|U z+nag47vP{rZ~?2qLu+1UNX38s_yHt7Qo1CRz+ujNjBR8<3&rBCxR~p`82u+m0A`%P z0)eppc$eHfe|k75;ITB&U<~plFF!0_?diX-Fr6YA&T49O%OF<${Y?1-XfWir+=y29 zo=aT~67m;Y5}CdeoVbBO%LW=!K=?B|CV6@jM;8gatPRPhj!c^nI&|ddYP|WX{k1&sZ9ALxbl5i zTsmwKWWFTyUP*6#*5>z4h^7Is+$YA-?RvFgc>~Va3#W~pvuoui_AI6Kc{LUl3q1de zSJ*p83e6b$Kv;haRw#U9MrbgHP|-E1{wO62bj6wfy_1PzGk_4x;-Q zEMstxTXqsG!i<4v6y3(VjIItaTd9)JjAKaHV%ue~K0`6abFVwd^^?50;d;M^{f*R`2I=8wdSIK zxJD!#5A1HknDwDxnIP{ph_zOKij{`cQaR|l$(U59KN|Ksn@S}bhCnW=j%dTn52A^p zIs9ApBCq$A5@gaWbEhY~z;?p^0-%>#jGso5f4{{hF>W!JF@N#anHHmlOh0R^;^ho< z_kkjO(R*$@Uh2cOnj$b<*i*>Y^b?=qN!6}+2=h~bK;1JN_r8L<0X_!)7f;Ll8tZwO z&pqhYcz(I?Z$-jK+)&bqSc|R}rbFhcLTCGJ;$C^7+H_Y%7vsV@1Pi^#wct-|h0V}_ z3|t0CpjEtz_{&jSU@1X9k@NbF#yR_4(?B+1GFNxY)#)izk!v7fcj|eQ<3^MxR>cn3 z<7N%I4ukTb+H?I5_2siv46U;BeBMg-WWu|cs^4}s2JTvXY;PQ|d9RGleAN1+f}d?k zlB1Fm&YO7r5fxz=jD{%qdg$24)DiHgIL0e9nxj?uLv9SMm&;y+XL=nkMPhhP2o8W-wec}97F?;mjlB5EM9`pXoc=khA2Qj5 zkk)(wf7|v1G(;F%X}gTZPOdpoRLtaMzH2rM2FrKWG`i^rJMeJF#{{>l z)uC>;Z(hq6#G3W}v0e0?^(jqoIN~v2QhPfr;Y7w+i&q^hUMk@J+LFP$rqQCMy4HRx zArKS&U5PgpVeeO2Y#|!l$OLHPcOU3!vI!eH<{OJ;y<(i1MZ!O{yx{q8J%VRJEhm)F z?H;){WwfTsTx+HSI8>hs4f_WmGVE=2Ut-(aDV4=&6TG>R)h*Ur9PVxILAwLs&-{1J z9snKgq6VRdV&B^>#1;^+F$^G!yQn@uw9PeX@>1p-)u*xHs!P8=SI6}lZs!s^4{D(0 z*8fU~2j%+hls{4|%Jl{XkzFygZohfz)C@Q8`M%;-06;K2f@MF?J%=#hXj5ya8dm}! zYA8doHu4Cc&mn+1BDT;0^xOXpym~nQKz;c^F=iFO*L{bY1li$!gOVPNs;rWn5qP;=fvDRVm1KG zC7Y@n(&!GhK+?gLW%biNi4x&!$OuAl#q3vgO~=Z`aUhR_GYdQ59c9?H24~ayr4v|= zRji}>8=LI}qC>w2HKKItSqZCK>DVd^3JL@!bYTIcIRhMA@-RkvO z52YfD1!&k!WIA6t4}VTv+C5bl2FOxh?FC2{k-YI#WEYdd*!`19GBA6#4_Q`gJs0mO zJ&EWr7$oipq8sO>o#Gza)78uj=aok)Olw~QwwSH-%uSgXJ2tlW!WoBaE&BoHLJP5e zfxFreY^8ZJ_7g!b>xA)h@rSn|pN>*@%o5t=m+l}#o_WLeFc)3PaUJi5cY{DSH#CSR zT0?tI8#M77?+FF$E`b}3KNRoN*1RLH^~wlz&?iTvmi1kTEeA6X`@$C>J(_~Dw-4`$ zn6y?x4zzHNKU7>0J@Y$n!mPGf1R^j?x>%XZ?E0HQnHHBKVvl}`B{U4Qj;X%bJMjn@ zL#aGWhMV!1?Vz^>rdiHhM_1BO1FMFIqk3;H1pxe}xPwP^{U)O=fIyTjYQ}5pemJGz zowGFlvS`_^sH2(^bG)ERUPrh1N~99Uu+`Gnfuh&fc@u0SPjCsQjb;I-y+{uS3xx3V zqV@&=94(gVeVD{2lO5~XTL>v7r%uascl@V9XAQ5M++nSt9vCwLRP5wzIyHZ*?GDJ^ zw+@Qkgtr3_@A>L3^lB2mNf40J0;K-c32^lw0Yr-sXsx-jh$-L}XRd((8~jT&Kd-0L z{?SXff6)MP-$7T+?~~SVsmU$`+5tmAt8%?n+YzRI97MQTyR&I=(wPe^2eV?@wxvvLwWV(~ z(-gEKk775quOvo2Mbk*t{%g_+N_0%=0$5wPQ;}=OuTe!8Ub&fie6bg*V4|8858}%8 z(y^r5u)MsGT1d@Xl~(KHs6&P$sPaExK>5C;coX+kR5)fVp#Z#be9)@hg=eoA1oYj*rcdr`TJKas= z+0mfu<30Kj^4>&%>3Gu%XBX=d)1jqjz66<~WSwNkqpN=2Ydl``#D($%p6EVeLF}h( z8+tC;8Ki!rx}M~85o?Seh^O;8`gy*uPvKe0aukxNz9R0eF^38muXGM+k)u5BljRZ| z-nn$iR4vy{O?>4STvAZfITy!c3T_l(3?VlIW^lPY;SkUKhRP``sI=Tl1Hb6eBjDNc zPoSPRioO((2MKE~OnKbH@zZ&|?r#D?%;2aoIUAEv4(0Rd}MB zq2uRzyUymFJQR{siW(sJsO}i4(nTevy}|?Oa|uGjA3vz`N8^F^SeBMOJzi8YlG^QB z8`NvJt$jLr{!UHXLBav_}C{W0M+VxQ|yo_SJeT0mB@ z0b3BO5K?i+O8!x4UW{kwk@EJP4KFrb2Ko9W#_(!pAz&z4%+oIs(WgzPq`tuqu9UcuQUd-yJ$kPbE%i*7wtWmEL>) zP9c;b$)Yv45Z~$aKj4PcNq1(z?dP=@RE*x$(jfWAa*40PGRYcPCj+GAjOOAj0= zhtX*!RL?RjBj;;>@|k|vF>8I#kMMJ~Z%7(v?9 z`PYR?nz2%>z?B0s7;;i<3{U$Mi0rux==+@|ywhQFE#P;)O@heQfc|!yXOJfjGE`;E zzn@{C7+F0~1+*3*;e{9lxRvbH(7eb>dp(O`rFt8$&Tmc&GKk$;EldYKY(0h%fl$~= z>>TBOL$vZ0ascjfE*c9Ah~O+-u5i54W~F>5{xuf)f65X4+rWo#CnCfd){dD)3ohw`RC^@OeVF?X)fqRg5XcM<2AH#*5XY~OU+#Nv6l`W)L16NgQ7am=Q4ZUQBLWCp- z1M}9|PHhYuuc#R~c@PNxi_uaJI}!1_eIeZjjvT8MkF5dYh#6J3e9H%PSpki+j=uCn z90Tq!&>Ws_;xs0&n5`s?ByDm}HY=ULT>Mx0w5@ILvog^0wxVm6H8Vk$Ei-uFXEnr6 zk7^ffHv_2lNqk-3h9m9{39?_ifs) zYVVI~+!vKtB8}Kkgxeee)l)D@-*zhgY&7Lblqdbl)Q?y6_)ZR(#rX6Hqc~!g2^Ajk z8yVX*c4KO`*7RmbD_6`))=avOWcs!&ocA6nnMzc@-M@R}P#MNkGoE5EQ`VfNf*U6!caCLjd zR7?3GrOUO{U_uoGx~QLCza+dCR+*aNCAxXb=b;vc^M+N+K93jQL4(EoU_4kUhlUt> zL8UCywYoNc4mHob*Er_TG&Cg9E=z1iH`LPI3N-si*H%^QLkbA3p;FGW-T*q3h5C5`Hm{Zn8) zoZ08Vb7gplP-1+Dv3Qj?uqLQ&hR_~zq3R1_WL!oVymdS`%B^S zFPE_TM?gOsVqBmpu{{$ieZfJX(i+F$vxoVx`wk#{ct&asFk&Y7kIvR5@c__5N|<@P zwA;8gy=ue^W+5DgO;Hn<=AXHiAxr3P6nMhQ{ct!k46zO&zNMooF z!{4*E80O6Rl{|wa!*+$ROUx+2{6nr`MnKgUA2sJg593;j`S~@jDkFcp69}#s2jhhR z5O=RC%@;JSc#$Zjo0asai7QAciZi@i+I21&hAJ?Nu{;Nlw*JV7f>N6tEqz`sR$Ao^ z7!W6jeqHE|Qe=)q_X)8RU4{mGI&=2agX%2|a2rl%tF2n^pQKI6mwu>lJu|qGgNc%Q z>Gd%OO^_x-!E{cPi@@#6Pd;VyzC3cV?zSM_t9HFv#M9(WzahO;O_U~L?mXOcYi#k{ z%x=i6UjY!V4B&xVqNXFgNZZZoZ>1LeGJpe3-6?C}#GQ_#ZOY?IV4wMM5$2yUDUG_M z&M0f82PC<(8aFRW0YPbd({&NgoF#Q1uqAvi0FpaSWbXo`t@@%^BgF=?D|?SHWt@F# z>x{){MOeF$mKku92F7b1Jp#V31HK<&YU#A<$md7`1_d1zfmfl*z;P~s1lBK=o-&O- zJ98}yi>`a9LJONnb8*yPht&=5TL5}?!4;4_Wh!&+919+kRh+6lLeW77i9QiQK=fM% z%|?Ltt%L`EYC$`>7vlZ=o^IRZrw}pC#J065|ALw?#3cRMZ(%E&JJyoC1hM<7JcVjibv6=TSgbj#?31PG)@}1;DZ`Mf#kYn6wGZmN30iW|9^!H6mrH@KkAnLP(iC0U428 z+LPM_1=gV~^WNT`r`Dm|yH14eOb-uC|G^pHWbf_17^!+V`0qYc)l80#2A7nTb>-#f z+Z`<3&#XIh=1hA~P>`v&bLZSFY;c@%?D$np0D^@g$#bG5C6|z^w zO-_lIGTMroZYNQj!yvEhf>Y_bBH;sQt902_`CKnzV_ddy@*IA&aqXE^rLL#eI?X!V zzsit(Xby+9F_xEP!D@Ex-%Is~waAqB6tT(_oZDQrv$yxACQ}aC$&YsE76zr!0^xjI z27IIh`ZVO8# zT@@qw^5jKslk2`mxDf>8r#UUW+dZO~`GegNn2pPeMI_@Z%4Bn_;U$-*K z%hEW_1-{(oA;M*$e=$1{JGp7^qU2jCE#gkxW#KPgDCF=_58AljeOkf1Dyi>Fwuc=r zjXQuD%I%1!r%UJbuS?pBa%DQVlSkP_WbVeK^=opIZV`IwIbE)Z$+Gn3ku8zbOxB|J)g=F>;g;5EgY-7TFfEc=4lleHlq$4vs2zZKYB z4m}DlO}MmW<3LkhcU;oKKgnPh}PIOze za=RP+-Praf<-L;7V7P1vc#XYHf%05t5lpuE6|HOS7|vj5lX%8EWpA^%BN|~}FRd*h zkw~tV%h;`CGU~D~VY5)iu!biy9Qs*MCserCJjk{zD z-TfWo-QpHc!O)QH!Dd0q>!bkT$A0VFl>GSJoF}FHO_^Ea?)}Z-4}6g)5A|*Ar5+be z63iJZXkh7*@P0U*-2UVzQCK$t#>UcE^dfl`SUqZ3LT3LfPUpCN=@8X0Mf6lT`=#`L z)_4|7BE3CiFnXsXGzxXy2yO%Sl9%#>3u53Ej5$UQ^ykHFe_jyf8gfI@KFr(GbiyOc z_FK^G9vSxdW`$=0W)9r^CzQZC9OCRCuz+^yS}8B^0*}GJ<7?hyJ?#nY5$9Eo& zbW`S;Q4n<9!}D;12BKQ!r$E_~MYzq!_=8n6H>1R|2k5S6lQDNT(TI@^95}6rT<04x z8j-;lFhwCrO*<7kGqv%#+uDH#!GjcvF(~{B5c@s~4C0*u4EhNh#f0x}Lri%#2 zG6=Y^^p|8phe_FRUVa3uBxI;o!8}dh#6JDn)}EN`<+3~}yvyK_m3Md%`gVkl88y>} z)>JHs=NlL?$8#TKMkS&oMJc8CQP`#u%r8w<#9Z&Y>vQ{=$)dddcUQ2&U)Sl&?n{f3 zjM})r_fVrAq8cD0?LOYc(A^Giloln>0a^h|59hku1Aa?h*kNvMqR=dZJE!?k=I3LO zPJBQ3?{f2eqPWl85=Aej*?WQbD3Wh-7OmAbIYH$T8b!sM*V3rOwF8^59>l!hTHmxy zbwpTKR9fb<%wzPe%*j+D`*LQDpC~9P4}CkxJE+WoS!7fsPcfqK7tVpFphSJ|{Dlmf zE{j|g>_^8kV_u3&*kGLlJ&23k;N^9>A17Fk9N{It$JpXx-wD4q8aa?zAf9Y4x`wS2 z;>iOougRzhWHt(FhQu(7s{HuzgaysXEOX=Lml!#$u3#Pu%aokpX+vI>U6c&PwO%hK zj%_znoE0$-O`0!wja?is{fOJbjeDwlp-cA-5&E>J7+M(Y;So*Fs2cZqsgE>~)h=Gf zej`rAnB1lkt-~7fa2bpP9$fTYeO8#|%$C!4_2*G1HVE&elZZuWL|8n{ExMj-#`M3{ z=#)tmJ@w1C!X!f_WkIB1dxubdx$kZ% zAwX1W$FpB9t36;{5X9;v<(UoMv3Ijv@l&o~#(6f-2r)GI;QjWF|yTM3Ld z+zjiaq<$*h-gYN*Z3ofFq%AClpkEX78$2J{#e&@|{juLmRH9&(6^r|@b(v1DxwY1H zSDfhR(|re|Qq}`I;ndm+q^)q@G+QxvCZV zl(9mVGdDR~Jl|Txtt~?$o9nS_U$QUvP=ozsbw&QD%bE5Is>V_A`N>q*I`L?Tukaok zH+aIN$<4CPb;WhQe*aDehw5b0HCoz3qSNr#d|wquu147yW~a|1wCt2od3%DY(wie} zolbW@5iHzpdkOB+g^d!(Xte#FEN6#$cf@JCHOQq+CH?(NZB{Jn!=m`db-3BmJAu4h znk>^LN8TRR(FRcdaO+N6DoWNFT(BxB9^!^y%Uc(){(X6~tQTt1Rd(@r-o}2m#uuz+K+$;o)dWi_mdIgzE6( zOqq1N6O<4n8?0ij8BIm8TV)gRD-`OhZrgj%$_k?tsYSSqr2gjg9$DB+>AgN4*Fft# z8qVe++}pBlMLKOxnTLOWtzARlYATM)p+sfc_XH+VhNY>MPD} z*@-TW-SARkCK}F*Yf+G!AxEO(BIUg;x@Qo5=A@xG52$$i2HCP>r~oDdJEsnjC*Je= zO}+=o9d{gAq1^FDkwR+8zT0Vjxaz50u72_=%blzYe2V<&AJ>8V21O=b|oh@N6c>qzUEREQQK`Y(DIwWHK)EnB3RJkx6}(yejF3ahGUv^V4kk z-k|Nvno#rmH>@!v%uCh}gz|Z^M{8dyz4yoh`*-0TT?|q519xj8dV=6-ae1iM3=*u? zl7M~(Z#q0wUyLah6L5C}@&(ob5a+fsTmK6uQItdd5Bd7N*I!}P`htBMo%GIf5_m25hk418%oPfOrE-*tdnmb&kms~* z?00ym24be6PBMh=td6gsazXK}x6y#ZB!WgjriMG0%RVx~^x( z!QY_r)_#4wyPkHtyP$@6zR+&AP=J}35nnN$$U4uyi6_S4{&n2G}3i}0_IX>A8FL9+s$3{~q{1qom zdL${X&8~=B1mDngC=r99s*`r_wQTTs-blM#T@-uN_q}TOWqGxXu->uYTVgM2!5c3TQe&LfT1orlF z?g=R0gzSZArsxy$dj)hg@D|i^L(Z~G9?&ZyY?rSktbtmNijif7^|7r{{D~vOvY#3L zd})-Ts-?I?b!xY6pGkEwa4rDkg~{k%-FKRc~l$I2Z-uf`Q=QgW}Q$%0bC2Tw7*=KdW$oPap%&ChrjKo>8Tq zsJl*YE~YmjR9gPZ0`J?Ysp~{LT;kZ6xI6qFr}!X1PgCPKz|(3|2~PgxDlC$QC&O1Kf~4 z`jJmn?s3XotvIcVTARWib#C+1xl8QXR&r|xuN2P>gevcnh;AcE%$Uaqp{A)^(Wj5~ z@fb|!r%=XYvNY>nt~^oX6_d7_n~4$%7l-Zb5Qf~BT4Jjh6Df{1M28@U7-;RdQ(vV_ zvK=1Q!wyx4K)4_-l|%N3eKk=oEu?oh3J*Lzm2E@Drt9V%1v;y{Clz0+eSkPJ6_el4!(A#&iWI5^F7> zHPQ5<(M^ph={7Bk>UeX(*M&Uj1JmZ_=lsEH#V zW;_um-ha*Yu0QK6h^}RM93*$J3rdeSv%~gtSc9hQdEAM=u6$) zz^x5+OZTJhT+`$h_5%p*sK&mgO0kKLnqxBI<=X-J<=A@$zsifbvi|PFln-kS9gmN1 z-2=5a3QfSw8Y#`x4WwlZbo4}@u$j5J%Eb$MS9Axn@4*Dj-W@6nEX_nY*){qM#R)D< ztPQNnMBZpwBrOw%07-~TI@sK>N5?Ab)b)I)fwA{qeZ`Ctd&l`ZF^fAC3daS|^S+xB z)#xNc*>ZQ9G;jYqf86t@zI7JnU3XuSbCWtNkHuu>yPj$v?a<9HQAAjW=?Jsr1c%qk z^es)C1IsFIfy7etRXG8|_>rT6g5Cf1 z5BqEyD5DN(ob|XzfiQ*9KH&@acvJ2zf-yJ?*WflC;R@=$5I&kRua|iRlz;cTySq6f zBO_-9o5Soj2NFpns9U&7NkXAEQmNE>GgwPens=&ljP>S1v`34PR9$PM7DI35g1XWndTqrx=1F?ImXO=u z7Bli}mSv{&`TaS8P+L%f+^L9AT(V5DfZFjl&W1o9`nmgfH&+LotIC~kyB%87*VEI} zmz0wq&-9(s;dM0n(2KYwRU{FEMY`|OEl-w7w&YkVW>;*>GIB}2b(}I(G@sd{w>nhE zrl7;okY>-1<}(O8?0lIU^~nOokNNEAk(`n11)n+P=-CAv>aWE5eJM)%29k~6PnR_x+2IBtT_VFt$$QwX~vC~1t9=8U=< z7VU%2hR<98r#tmh0XgFMxcex4Ss8Jnf-H2|sw4LgZQT>@BDap-NZkLvQPZ1}$JgV& zj4OM#uQV@<|Mbyc01bX=%#z`t<`ilc!$?1B!tkdaHu7b+TUU{SYvpFoJ>-^QF>tXT zbtA(+r2k1%u?>^aW7*6AB1UsEeL_n90jwPfWl7uQ!gaFb6uH7X0w53zbFfTq2h7bh zO!FkvUrf(DnK%{b)23-VTCc}V*mhl`)o)1~_eqmVjY17ZkWO|-1wr%C(Z9ZQ^3b!+ z*LBphK*6I$ymii2ZDB*3;@ql&x{I*}9f4{65uOfICnh!xk47B}@n&OSLY*40tnk4qYf383=?kUGnT& za;wJM!*o)k@a3?P!Hw8`4}i*73KO>-hch2W-Kpu+WohzK8c}JTF7W0kmL6vZKi2>r z7lqHlKK{Y5gmERgYr--i3ECf}Q{VJG1oD`*+itzNmgU-As}uZq^DiDWP@0@QKOQP<^7*E44%*4E|uayy@9!z9X*dCY-3ym;c5ZEez^JAtwU zsPg>XUXzABEWvFT4EXJc2H5aC#2NWp#H$O@rcYfa%%6iC2nktVWnr#0;1xbVNMEcA zfMJ1UqXl}*v{Lb`gZyzP-ktN43Pz9g`KXX;i%GRDcsQ`?zx?6JZ*dYA?D$Q887)jG zr+kt(M#Cf-wOh7}?_EjXAtB9=dzOa>F>~+4CJo z=){5@3!oh4)Bzs7j+NABR^6RBPMjiVr(y~ndlP-^xAq6BH|>avWXsY;?*pl^*0snU z_8qXq#vr|kOk?yqD|WwLKeqP6FrU9MT+n0Zp-=MYY%xN0*V=B{vK+7q>@IKnB_o&c1B6UQq2+qA9S={Lhqsg~&pki!c4Id0s)a7(dCb1m4Y*!y z=&mW?S~ma4{1cd7!hv}glJiLi^8H$@`MFVy0AF9W>2YM5hTz#g{>oBhR~)j+n;a8a zgja5P&pR{vq9M6)EKpl`pZB}!n3bwyvJ~SvfV&NNoZP*#a^=gF9|oE#5dru*TJ5Q! z6he30F=!pzvgiP6$sW|erg9!$M9d2-rBKo+38KR>$^;|*+*sAICc&`+wpNwiD7=@u z82z56o_s~uYMBr0(uYIF3(ERYSSb6 zF4gA?jFCl!Y5DWCZR)El|4Mhx?fyb{j?cSrn_zY#8C~MG&w5{n}&5G{FV5mDudci@mDKj9;FcEK!v%7P#wL z!tK~Jz`7t7R^aE>@o3IrDEQ5F$8~L?RNspt%8WrSeCO-<-=CsbHz;4f*edK@q3{uQ z2qZlm6PyHZ>P^TKf$MqxH0@ZbG8|3mp6p4U?A~z}ZNeys$_Wb(Pphe^Y0G)IdGr4U zPoaVa7@9_->G`&dGLPG?*8fdNw~VVNzK8+Qg_caOk}hKI zeXf$A@Px-Kw@t*wkNEmVAExNXDeRNE$ zs)yUYOOMwPY6WQdN8K#Y{n?8dw{_d;i6;K6iPjoWGL>)dS%>u>nqN8eEG;c9UfzC*5ZKO&2UfAT7zt&#vSJ`7 zv&AT#mSQqvZL!$wl?-J`&2qWvYEBFcF~iP2v>}uDgIrR!pmEjI4oW?gs^qdJGdARf zFTm9A`(C?LYeV_^A$nJCQK(ji-ej#gvO?qF=++G<=b7cY-aBY$oir>(R+;4*@$r8Y z&=VNHhmT>DO9^d6j2l88v6O{i*JPoW(n?V7kHq|f$_1j=q}UbNk>m6^pmca%3ABOF zyZs0kI$C%tyKj3kC{q08#To~*&prri=M@EElhDEoyi6cSEEQkaF>h zw{bg=OSS(XHa5H6;KyNuG8&6{Ac)!njKev~Aj(|5MIzVuSZYxE%8`{5FD-Z}hgmEm z7x^`6(64IsNlT<>f0P|Lx_BPHKXWO)7*MN;u*6%;ZSRW7T#QF5y{NQtd>^&Yj|a3+ z*pvK;)O|E7qf86snGJDaiFK+XVyX&?SAP)eoxS)rsDm#1Sgy&ni{9|-3#Zy`MdIY- zBm-#iugn-|Ohpm8jz0RLM>-t1S-at3K0dheDvj5v3Jd9@pBLg`1CNuqGNsMYC2fqt zqB)%AILiK@CN;=&Td-QXlCN2P^)5o;eN#jEw?}AX*;}f!M0pf+qyZL3fvT*eWN5 z@u8m*(J|SPBfVP#$}qC95O!VS8D`{7&{4GtAAa{6Dc%03a?FCdw_XO@-F!0MX!))* z*}kM<#&eBA3w%FYba(Rl#0B`o1&%ONIXT#BG4|rVHOPA);0w8&Z!i!VynCEPdkU0Gv$(?^F5;CsumSwg>W-n4rTr{3by$%Okl z!b6wVCd7jay1BR^#Yd;xVEa#;Cpx|c`e4JX-kNG&V^F^K(z*txpXw~ufGa$7-U4xH z?HOMkztl(T5_~+t(mejohpgi}P-pPQ!okm=<{4Pc1K(H2{8aZgaqSIoVHTHyLFIXG z?(`E==~i4MuCwyfYy1W}j=QJP+*aB3nff^YF#U+jf8tO^(d4ellH@A#R}D*LL8z*# zirA=e|!Yjhd~it1o!81=IcRT}++>w_inkN=2mHB-#@c9GVKn8M(IL zG|R<~(CH@$g3RO|Lf+!i27$(NZ_bn!RZm_gK9SESb_7r;Jst*56cUkWto;F0rSbA* zx!IVGnLb#NPH-Z-Z~Af~Py3L0Nin8R5m`hFbe}?tyLzt7ysNzPQjdVox3h$%T6xVY zw>*=0Z~r%oawwHvGU9ebuzFeZ+muU^3l}cb8X=XNw&nGw(8LSGewlP-FL2C`JKrkkJKclsONk!4u7DusHi9+ zQO#xz5)Q4Cr;kNUCa@5qLoBIJJ)^*qf8iXFxv3N;t4r&20M{D@nW&GV@jLO6AAc{?*69A$6mO&Zrt-i z;^Zdga3vLclsri|SNslJ~)MW2W+Ziyk`mJU-w!8NYOuWqrtujyC|ug z!;$^D;{f4`$Ek@0xHUfv7d`eKq(HPGgryDru@)@o*Ij?dGgV(>tiWcBvHuUv!rk9o z2G~xC8L@OIi9vcdaJb%fsqT>l-=3B=pnW|%Qk%7X$nd|S4FHa5=l^AAPdmfUm;f`s zC{cWaCjai?oFdK68snLR(p#e7B2~?ox(R zVnh0y(gE(@=v0)L+ojDMgMBPZ$?%9u5U$xy!dDxV*DtEOS8W@b^5?-^Q^%VdWaG=8 zttor7sZywWla)CXoX0NB&M^e<5V?Rl%PczR^TNpK<$Wq`0*Ick5$L4JWk zpB&P)B}H@;(`Gz>`pp{M0erYlDa2V>DdRG~7NOjUiDz9sJ>Hd5HE2MMIT5oKT3#30%_J16>Qs%5KTp2~ z5V$T|loF*>v-L9wN5#_C{uPF$6apcX)}NrBCf4t8l5aXm*pr$GPghoi3)b{7BjEz% zBKZ3*){C(6svg^2cO7vL108fqe~DpIvx5XKJs;eW_$cMpYJ!^5Wg+|isLNj5u$|= ziC3lJr}Tbt+}GE4l(C`#nXX)ZX9|1Adc>prC!Lxz2uSu@DUfvL$}UH?ijLCFxc1Y} zhOn-=A^v=UdS3Uk{ zjV+|t>+kpm_|~)+eqn0=F1G&nIb82~P=w+mIkyAu0B*h!sHN6k%*5#nr(&x5L!8F6 z#+o=?Xfz9$K>xbeKmU0tRPw5CxN_ShNCR6^9n_;T|C(|^%IZBon%Y?LH!R<4-2kr? z5;#1{n0Xor7S%b~BZE_>aB)jwU-tw^B5SruFKFso*YklYXJ~cut{JZ1fzmpeGp^)1 z-4sYiOp+I|6>?c)x=pyTTX@DYqH<_Y^Fy^Nhsfca{#(ZE%KEqbT#HM?>*Cfvws7e7 zThkx45~zGiRt?$`A#49raoE3q{~bP`?~g+9^NzdmDi#?TZM3nm3G_lSf#ozQIoW}3 zn=Y5jOBDR!;fw-}jHH6L#Kgo}le!!mxYo$d<_6w=%;ksx1ztJS-+H#^(WE{!QdCL^ z_Z%~_9B$}0*iPDA3$&k(T?YE~;SFWpoe(l$uf2LlhyI!jaRQl;#W86~V4glB# z6-i^~obVSVfp4=nM!nn#(z3}TWW~1>Voih6k!mDyJrhU%6)4K*y!~#2 zzDSX|)~9#r)X3u%ihtOOe`(Wtq+Z_Y^}fqEr057Lw7{z(jMzAwDwBuWdxg^~#0Ufe zmj})ArCK1QE9VsI_+X9yj;$0$ymH+SJg*(H&{f|Hwc7MrF_;A!r=U&z&5;z!Ba~L3 zGgW}64<$wG2%c_FrACg>*K@xiJpcyKEcZ6jampH$&(3;P>zJ?A6z0->MLkTf(`8K>Uj7& zOmwg^uPTAzq?U1uV(e)C4Mg1}ugCWqL6Jok!=UFvz(zG1m$Z=e$Z{At3B}VAJ`Kr0 zL|*Q=<)s9ye;PK>fld#FW?z0`M=Qc#J_Kovibdv+DSX;aGFQ=&c9d$r|Ail4QkCl7cDw!$}5-3xz> ze$(gFR=vXYw1R3<7mJ0qw6e0fFi~uQ%V^idSiBuQV~XiKFlR=_4Q1i$jT_Be3OobX zc$K%SL>g5VaiFjHocg~#w!{G^Z3{ewHYwkZ_*olRsqn=OJ7OgfcdwDv>R%#vZOy0< zvZF_9_4RYU?AU2v8<4YjZWE@3;Ww`Z3^1!>blz%Ni+7!mU7VY(I~#!6NvS+^+$mi8 z70l)e8Uo}R(zl`^!Alu$0UJnB{-fOy$)ESoveDda9L|f;1+>X zM)Y7=4t0}YE>rnIll+J3(CXJo4r?&tlPGC#Lnz2W)9~K%~%h#aPgzqkFtSiv>8#f-`qvirGkI-t4z=u=$LQ z9|f)1uzxt%?Qd{+8WMm%rjS|d@luG&%J}zh75K5JyJ2JZb#q?W>2a9?e}>s<5cMBR2h%rvE5O8&HJ{Viv`qoZ<^Nt-_WwK>|3??F+=!zb z(N*pU88=bhlk}xzAiX6+xWZ{w{gmsvNo<7|gghpEd z17N^Ml~V`#6QQW2qz%wfvs>Ze;nAcP;239(w+_sKq z9Qv8KOkcbQ)sx*7O0R{qiYsE?+!NEAE^GTiQ6DteMe-N%#G*R&ShC-ZY}0#)Y72$l@rKLgfR7zO02U&_t<)IcmBf#-i-3^iHmf6Tr>v{$*|L7 z(~-(+HbT8#Lb5mJY%3T)uo?HRysl+yhWjA|wT}vNb()Dw!5q%_!uV~e0`zR(IbWF5 zSNmM*+}H`AjcqiiOnw@?bMl|p;tNt9m=|5Sld@@+@M7cwpcaJ~T=+tt)&aD{wTP@> zE@JcNPd^K+PWPxt?}+!YwYk{fl?}X%%CNrg5~(rDkJyDm_<;i z6{yw&_`E9G?Jv%tAQ>3J9lRA@3v=HrARi4KxWnk|bgj$+y_dg(#FPe*3{!sGpz=y$ z)T_>i=+HGaHRI39f0>^U78cedCM_0h57%+5sHjN(ZsHo7%7)|&)v$rlq3@;z!s;Kg zJ(%l4pLusRS*5$KJZ{o3u~=Dp^i|T$_ur|@R5P+X(L_A$$%MTRR%nGQ*=Xu!sCB4IY2><7G<0FAt1H~{51<23 zwH~bf=SJ9$CDs+llat+OXpCtolN?3R)qp)PuMk2>gPWv~d!b%Ss&ZLdzOSCN{im|# zpbwB&Xoi76zm?ufTT?&WU^1}MmFX2`!k}t~jn*sV#f9x*;a*EHZujU>{1nU;DC$v*u*O)f>)_7T;sfi1tik&aW(~7(hXPDObNm1I@$qHEIq8JooKJ|3veq zSnNf00%cx-i=8&!1y!#et}}$)%zS8ndMMbaB|5sP2kJ2PVjI+=)U0Ny7ez*As`Ksu??Sl9n^jK!-*(T$=|D_+fb z+!Ak2?({4_6X-Zbp8vl`VAm9QSGiAWe^F?shaUh;ivZC14?5XX8Cl-`MU0&G8db;X zMOF0p9Z*3qDQq?e(m3|AZN$jEl6PcGMOiSruIp2{V$4KU z0g-t%c-zwnUpxYvJro?}#9m+|ZnirtX-ihD$e&g#m4GqRyWyAlxKA?>yJkdXetMEQ zS>5XEDUVj}6PCb7-Z|aO^?-LW6mvdiHP_6Eja+R4YA78Q+UxHM$BqGufx*Y7$!2_x zT9u20svHdTO(r#`OTu^4(<>odPIu@TUe&qFqY=IO47@EhKm zY(b+?=^7Dctg7hnQS7WQvps#OQ6eAj_g48J z;NF6J0Pl^pg(^b7=lQ8K=@Mk3?zPYXseW?h`TB$80H^xW3)68Av%`BfCYR4`A^>ZUEMMb=(tR7M9_*ZE@>m`CODfwo9jM+uyMTPsj2-J3OWCIyi5a`! zn{U&ncbZXM8Hm{-bf?;`b6z~zC3LlI-28ryA%~?8SK$=mFQ*##WR%*CH9vl4`lz(~A->e7I zKK+-P%9fH5Dcf0g%dqOi?SKzzc|ZTTT@bc2ePC3Bx{ZGkdjEO~|LSr7^_YKAfd9Hu z|I$-cJMrtGr#w?(f}SeSXUdoo0xKu_;=lGU%uRr6p${kb7AtnY(uBTHJB94fTfO{>R8Q!TfaZOX8pB+&! zgHCprhn5o!r#pqdEZhIWzxCyt_&+z6(MxM44V9&VFk$!|4x2UF1h>uljG6}D__5YE zWG=F5Bv9lNl%EHHfxufAG@%+*cCMh4CoQQ|xfnNBWqq%!tLy7XXwl7EgR7eAhV4lF z#;8;Uppq=q7pgo33uZ>e4uVO?lHR>Oq4NsuL+5gDHpMu&g8Dn^5v%n{+Rqj8m~oYLRKW$q-0GeQEV% zV!yDk=Wr=P4O-2tuIMa=K^~KF5A7}q^O8uW4M?b+dW+uJxkn(1AxC;1F5tk${K(Jz z(|CSXXiCqv{T`{L5&Y5jR579A(`p`%mw&F(tRWYpm{_oVsmjcaZu=~r+O6tcIk3wk z)kGiRqc=@%t zy1VLMTHyU;2Ov88=te3uc^;GBuwJNlY^cScif#>Pq9}^+oK4SlopptW@wiHpSwP1R zyNOe0kCcwRXe%JJ>fQ%)Mzg`m%=qdo2S5MIS-uslIB=H3$Mip`n(PZyP4=mI5A%k9 z@jWn55W8=EgCStM@RI~|SxfG%$epSgj?@USKoH2=D}d{OdolgjKb&awQQTx zHoxI}1(ar+R^32WR6D&ka?}>O^)i@DBqfO?!H?PH=n z1lwWU2SP613ZNuJsOofJWULgG107uke1}goT-n=`xhp4_-GinBcRG_EZQ)wotN>rfW~wM_lJrxva8Mx4n)9DE|5LHU@hUf1KCcn7+%S&>~ZvE=y<>aQjnH!SE{57Je;k_3x`r84?2dYmja9w%iWKQ>2QKtWj>! zLYs_%DqK6Tbw7@y%p58Qu`RUtT<7J@g{tOEEf9Z_2tljYnQjcp9tmCrp8jT1WECjs zoP3fsyL`8Tzo(L>8D{<)&9wd-10zmF6bkEecQaaLoM8UR@dd5rz}7(}g8w#x+!a5r z5>p82j&?>R@ku!fS5^?z2#rLV@KmI^yzX= zx&`XRs+qo@8Gnz-_re^|4V31x-R#VuBRD(%T150{%na2aEO2A+po*&-o{<)G--kLe zje;~Dfw9`!u-He}Y_jx?ILxFt$y?2RO5Ew)Q=t;~+n*`ylTMX}OLrvE-m51@*jQ_A z#_Jf)Gnwr4{eJYSfyNRrLRepYRpSI79~SfYO=&D|A~iAgOqDxg?8OV1?eE;cJn^i? zVwkkZkdQ47s^Ut5<2xK%g0cPK*npN>(h4$}+(`~vv(^?@waXS*>F=Ec5v1FrocFIi zPtdxpw`oDp{B4+lwNC@yAI7hpFXLZj`BM)(ZcJKAv)86L=Vcy!zgKlXf&d<24XtD( zFH0GHg!!|`kvi(@EdByt}6)^`6$@Ss3Ic=fH$j(i0r7#mv`LzkqRgt(J?MO;Pf4ZDCQSdx~9>Z(s3o7LSh4Tw^DF@vFe>F_M)>At$|5 zP+>&Eh4&L7N9cZ)U z0B{3io&PWcHVf?UBVfLXn1Qyo^ZIJGdH@)-ikvWNT@)vYxeKv`P3 z@?LoNeY*JY51+s@XTfbp{^MMq^nWwg=i=o)!Z#}@t)9v!fUS>A27ueD01X#hX~({G z4LCL{rbH3|jmG@=dDa~PYH1aBb!$1Ts`kO)X7s35I~VlShN4*A-F9BuU`X!>FvzIM zxyj@^Gbcbv?{LrMl*=YKm^t%mGp$b4-4ul%S$Z+DeRyEtaTjQsdDfHiG=3->SW!Q76QuY9uR-wvH)Cbj-8L`dk(JreZFJaw& z2QyP)crce zS3wBi)3L~XELF-gX|8Ul zKfnT17%W^PP)Pa^XXUcYJ*p8TdOzZ<>aH=IV}G?YfhTf<{_gAq$9f68oFII|fLI9d m?ccuh=D+#&-TQ>f8O${)1+xQ!8^8%be%rDC*Me5Ktl4ZxcAKLIdi^qXU_B7d;fqKB&)Bt{ocj=a|lcKp+}*wFi12(D5zc z>)M%9z)$ENocRj89CO!Gy$34lW?2H>oUl>SRsw-aBB+VaP6F@Gx~LhsgFv)Rl&@p0 zkgqR5py$)-50oDJfY&fIsRG`CSOWI!&(^&XnlP)GT^<#MatF(TDTYb0KJoIYH&>4J zg~oE%f4wWw5$Nln2cgGi`@VDu0?*EWR=!+AQ-$__6hHMrf=2h|14c`FCLQIksR<<; zO^>uQYhq8{%D6`ODRH)|nvCs)W%V^_aILLNi`Hhq&1ceVx;|Rt0~U(0A8FGhP5@ha zw;&+n=q&J$>ndk}eFWV-e{uF02=wwAXB!pp_RfRI)4+&o|sG)&_nQTC$ z@@$<$To<96OB;5QvVLFpV&9t@bU~Wmh#LHq$E`G)!LA&>dImJgI{iApu*t(%WC>wgqxN2+ zgO2=a#Y=|s;EIuYxDMK0va`H%Q8~zDdz5^`eLto%>X0;)T>YSA!m3P9B;Pr?-t4-IbH zsEy8Kowo7>B!oy zO|JxMSD_PonV&M&4X7u5U?yd!|kwJ{1f`uUw-uUSvMG;@>rNWl6Z z9uEA0<9n(?EUHvy5LkL+8_I-Y=S)5;1z~Z zB_ua)dYPP#SedFMpeC#;!eq7%h;I=jI^_8} zeSOf0?iRBF{RgMO+?~D)PUsB>pB3u#6^`yeZ-Ba76PfAFLMp3dY2X{Ly-6l6G~_Ldq#xTyHi3~ue4~lPK3j^F zB8J^9Hv%&}C9+q?Jki_d>V;PA$+&~mEplBs`4V&SoARU+Yr?n7@ht5-&K`wo^^c-g zmtCj_S1&Dn%<4L1MJjRapKbW@Yf8+-P4sZ-F8JoF2oZe;Yr<>Pl4{zD^pbE~WGZyn z$Wz7mMUF$ewYp~52ddq;_Y*abw`lmCXp2fX81qgmT5A_39dv3!_FJ7h4=+jcU+qfA z&p)Dx>YQ>|Ux(&>o~bfrks+n%*(dtGF1CYvikPIwsngwp&#!C!U{$Q^D(s3D+Y*>0 z)!+yg(ogK2#*dhuMdY9FjA<)OG_+UaGK!k~{GC0_kia*1ZE|H!ps~he8=B}z$CftM zol$?G)o$Ih=WA4K+?|EHndQ>ooz?>y-C-t;+d@1jKZWXCf3o<5`y;_`Sb834p@4(zu^3zNKOWF4IE}R|Lsn0zXpIy)RlzJ@!^jgexM4WtW5Dsq z;uB!uid%ji;iSM`1Hx;M&U!aGs+Wk{1Nsi9NG@YAi%xUzF;@w-MMXzWR&vpBVWn6< z@5Bm{`~JX0=sB42L%&BYfwp5Nx#Am}`2^n;mP$lo?@D+d=z9ALGP^Pg(GKssif@{A z8cw*4+MO?{tZno*+|#5%H8vvaEWKFNbjkqy z0Ksy{W72maM%sBs7k_M^=jaZu{jz?A_lp1L=4K#+g8a^X>*e7(GxS$NM6^%f|PxZhgfl!d5WXu>{$$iCNx+97`INAX z5$JR1bEC4`NEKxWC*|H<-yF-S_kEB!QF#YcHD`oSb2;=!-olJ+A)HL;TbUpzc7I|;z|p2+)eIMv%FIojS`jw(3EgFz zu>6JE2GC1Uj>nPs;|+U)_i8tY*B`=@pvD6bvLOv4`1S=oJ_^ujQhDiGabjF zy->k<(xiOrP0s?(Ge+~c7Yw-QZ0Gqbn*g5m)|k8#Q%j84 zNt`Rje>+2QAY!1qyQ_MM&(1w){$O>tTJcD#N3*HUo2_O}XZ~Ip+2IopXVbzrFPno$ zNuRN3Cu1UJE)zo3x2l7A&rg#Oq3u{}(B|bLWoQQ%r+2nhbG?ecyB2CB%T9KRG$Tg` z^KT(`vEv`Gl4FAFKkHtd*gdJ(f=#N%3%p(H5VfX#U+L^MmAN?S+8ZiOZyOKy&lEpn zC%+ae^2$u1NS_AVGhK%D?-iXn5Pjb7dJf@S9Os&~b8D3#HP7PGQ~Nzz%>RX9&r$C} zW~+nCwOc_cSC8vE_&u`6o;L9k-SXbkO2H#og`&=HFbNUe)ZLXVy8HT$SQHGv)g5Y8 zY&AE|fL*$JCK)ag{b$`_u-1h9V+~_hOUql3)88)J9&=JR(!6|J66Ra+?U}K9YYDrR zVe8SGJ9frWT*n3xN*#N6$8EaSGdSxy`)4lZo@9)07gK#+-lJ$eix%gu^OCO{Er^F6 z%&Ke%3GPAY7)&a6-QWy#&4uIXqBB_3BstQ+)GROV&Ff!ozd+$%}mr^4%)NEtWw_wg5KK8`wU*Td0{NTW&O zIkgh+nNsg?T9W^lh-4NSgJ6dKHnlAmd$PUc5LO4J2JWM}TV~f(>SOULMoJ^p#b} zBkL(0Thk^%3g?<;Xs?apIgV@X556_5n$dCxbs*ZEMd4qrh4A^f+qldQ!>szMJ`*G! zDt0=;)vTG&nmc`{;S6x&mAumKrpgcmMFb20Fqlw@_=I{qlCiK|ML-u1Wd?Y}@Ope^Frs zlj*5@GAgOfGg;<|`n|;&iZOXdj?xwjGq3z0$1KV1X!CR>Cc0TNrNcM_1fJb!6c`H!k&-dr@(K69$om%bim^~6k2~1M!X_x8?Y_PJBr-b zi{e{7GxCtXQ`Ok3t}Q{K)cOZJzNDaCw~8acWfFU?z&{yPWPa&j+|bE^khp8>KtvuC z1T4#D#05>(m~5GNv8~lP-L7<~_SXEcN{mGVYqB^MRWr7$Dq0*P9N0)#j*V6%jc0EO z6iX`At5WSM#zY|Y6a1`h(;Q%+781~12Tt$A&5QsVIf<0MF(sj*$hEkISwGk)f#SHV7$D@j{9(a@(n$%!~{ode0ep zPTcC6>_H9|I8^Qe2M5WzwF|!9zOdrAP+bKqyFrPe@H*tjS_+Aj9qnzrm0cWQ@+~Qr zRk7G1yU&4RWIbU{n9W#y0lBfivMPt<9^#i=A}#hT254@z#tu_9 zROKS|RtqYAR+3D>X&eHlZAk5D{p08@qiXSo8Z)QX8O&vKZZ&rj>WaHPo4yT?=Vok} zkeZ5fxA)<5G|+wWydiLjZTj&IxUtY59_exd%Y1p^3&X7T_oJ}{bEX?nXZ>E)EoIoQ z9VnK~3Glk0dOG;1vLymDx1F)ethdrOhsTPIGQP9HFMoxV<(tgU^x$#qaqMZMoa}Y4 z3|2@miJe@E?Qi%z5WGgYS1!=o$hResVz_XmAM9hF->eDdt<2$}zu3n)DEs4I{)f4~ z+{i?^r9cB0{&H_NolCCb$OK-q0TTsV^5 zf62;A2-t?HsSl4si7`FIih_VFe)1xIJ&x>x8^K&C-pQq1@kuM%4(cy#U$)n-@#-DA ziR$mIGVoP5aIpT^jeHy*&&r-$R3^8r=f14rHmP-(-9PS( zLS!d}yjy6UI!{K7RauS~7KW!1fcT)ElYUk_75NK}ySUH@8M`yHI*rqm^U!N{ z1`p0bpAJ%IzE*2aGe~%z+ejd{lt}ccg!jA zcGAVJ%|3>z#qWC6sbi=z2Mgw_SN)%rpj{q(K<)jw!BHJo_xOk^!+86iUkmf z`mxW2u0qt_W^pq}SyP9;YFh_?lCTQ&?F$Fa06~fSb)7lABDGV}$Xt%P)$!>Pbd-*Q z;vbR+xEGx6i;@XQLClZJ#*W&^=e1 zU;Z3RPoY}Mdeqg24b;k%?X>1$DW0Y_bt8r=GA zCRhm`3ZFYvcp3$I#d#}` zRd>WP;L%0szad@v6knMMUOm5h`krlEb}xSi?IaN6U7(6Ovqk3|*t*v>&*3k+`oNek zV_mKFgdV(Pq%a8Yuz=9J(zQ2}6{@`X?MgY1oYVZ!<9yU?c0ctPxc!}tPx2|;mG)YO zOZ@HoJo3C%-Xa3l%ykyU(?hk1Pa~T>D$FhGSxw@9fw}j$?KP{;qK%L|wH87`#|yCs zanJ+JdnvnA3>R79L}fVU1nD*QlCzKj)2q^~omh*-;X2u9yCSq@CDlRPq>P72Zso7c z)}sf_?P~5vs|lvZKrazj-d`v84(nG!BuM&ByxSd*QdP85?e*&HsZ3-cua{SMH%+Xi z(Xw2~&RmD%?!0Lt=%G7}^#U|}OO@|dyyk{ezJw8ZXa@9+ztC5Ix%@_Qd!CSSq6*{n zOvr^pAH8_Oetkf%TyPr7u&km_v&qC3V!TqJgRb2#d2OYIR!dw;)Wy+XS*JY(n!5Y$ zK_smGM-Z?n&k_b)MqOCo3X>61Oy6X!C(13PI~A#i}ZS4qb14Qz_vsq4A5GfSz-M=oE~`6R&R@&sjJGPJQ_y$T5EHYaliq`XTbN zjr%}dCH8R5Y$BF;__^1EY&E5SCIh`?K3-0kV$EIaNc7vec5*MX!l*w#oe`T!_0=SX zCU4^{JCMOfs+~A+xSDQ5u%`EurU?lP=6=J`f5QCEOwK9P9X*FD>hYzOCzRhA+4|_~t`$AB+;E3+a?eFFdAa#*wa=1ZW*Q=C*irwH!vw~4(o!_{bXRP_ zkm#?`qh12du(xTxJ7_VNb!f`prpAuP7=XlK&dsi(!yU$>(p;mX6*#y@5^)?r=(`^t zgw_RbrWE1dvw}(>nH-Lkl|MxY48~D5%f+c(azO2@HhG~sFC zNy2Hjw-;U@s(JZkc|I7=6-Xu3h^1)5O`=++f;xYh=em77E9~Tz<(tt9w+}=8v2`| z|HMx2Z|00C^u(ZFJsB5U536L*&mI;lC{8N#efXnJ@#uc?Wm8{VJS5{0<;>NXW@S*r%^-$*vG_k)t9P&#k6eV}6!}awX`)`Vh9}$r$ zpLm$0Tb$a;{3k<+Df4kV=^5MAULAUM=>tjb(0DFc(y3yPPmk?pu}dyG=?!`ktx&rr z?mZqmJkhW@VK1q5fzH(0whiZTyw#Nn_ruDyk(QRxKa~cD$gk$%wbV>M6+ZBgQRD6g z;wdXM=05AnhQD9SBJQr=qe|&4%8cX7#aZ@bwzk%DWxx6Blps9lss7*jD=~+{Uw%An z%KYs)+78B(dKw0GdO_BY9-usL(YT%5%oWd0cjeAke`IpcpGRKMRqO7R*7)Xfmb{v2 zI8RD;mZ0(-%=n@gkJ}3P;KQl5yFQ+8to|ghOW&kVte5Xj2v`|ORvSWQ12sN}+=XM> zN9Qo5@p_6Ik{bP9x!O17MHsk)!Y?PlGc2!SjN4fjuC#|8v^_)5IErgHwJ(-WTYhcj zUs20h$K{$(iy2E!-qe)X0AFsdQ}xo@x0JVT>bdED%=o6d+uSPb1Aq@+LQG1N$UIL2 z?BTbyq`ND#1FTDlpDKs0no#3Lx^-$xN-Rp5H;A%QL(TF&VEC~Uu%TFD# z*8z!l6?DE*gNj+2?FN75@iyo0e6QH%{n`y?M_R>~Aw;)O0X4k0ZTk7k%vkAHYx`ZI zokOdf4V@AE<89{sFRRSDLgL!4U;vIj)rB|tig6B1xyx7;ndjtjTpVx5zi0dnA@q20 z-~#SNC-F_;E_EoWIpWHV+OhOtA7PjDuB%Rh)X6tiOTu4I@|~%5jRN%l7fvQ6(#etW zbjBc4p=UoVn_MRjeH3l&zpf{`4!oJWU2GMYX&CzX*{E<$FZJ8yyGhFMN{%OFPkqLA zZjO;2zfo?s4eYfZPwE++#I;kuPpPu>U0b z&m^gDLH33H6G`4H0!Z>$d0FD#y*LU>3gH;1CQJW9lIkI{5l&V^erY)f zl|7sly_(Py+bKKbpI{?DY!esHkwjhIP-!nY*8~hFFHrUETN?%*egt--VA6vRy8}#d zUsCZi32rg96#JzC;PoeQzWksPTGtP;KVsmI!YSCPYPsxlMxjkO7Zk7*H4dVUw=HiI zpqXRp&aLQN@wi7BdD2!bmbaa@>jDl^i~?AXT{}-Vw~yJxd}}6lfbtm?u{V@od-?v0 zzA})N9IGPZdM5(Zd>lee`fS#`vL#98UjDkEX~4f zh_PG{KYXST75`DH{|xtd zy#5utaw`;V%8tN);m&Bsr^0|AtIU++{c6S9KA6()D3Kj~OI=-9$HijtQx2D%HF_$}mKvw2J|X_^1GaVwg^mEMox=1y`{t4$a;`So}^y2r42 zGld)Az+KTV(h656Lc?+jgoVd18la-=p~`Mvih3yJi@8eq#ADIFT^lW-rh;baawx(b zIGd|9V;zOwylt zOX#ZZ762|}Ghl549d?!U_SHek>V~Xv-YURZimfh7(Fcf2Is7(Uy`LzAWiMc3_CYUN zI|T-aj@alcCOMx3u}>hcKOaL}Cza<6iiW=sm!dhb5-*kJfowK48Nh_Tr`RpM5|qTq zjh78Nc1ut1Yjl4Z#B6t@qSAgOGWh}Pmju>-=#lK3x=)R>9(`Y~qkxGDSMPL2W&NfO z|J}{T;MGqa#Vi~gOIqWdE0swp8NPWt_q^`82|yohZHS4A!#REN_c(bNky))L@H*Db zj&B%NE5tE>SfnTCs0zsq-E)R+les!n3xLvkR|d%RJ@dr3@~6pp5;1lzeJ!|Tsi7=x zr`z5R3(VVtt&pTSN8CrENQSej@C`ldS{dF^@9b&rX2Qb;)5`^t%FG9vqVadRqt)$p zAg6lssg|-By0VYaO305m34=hRhMX5?8OX!EVJRFzX5BQ^;WNXn1gANQSurhEP!RhK z(Adei@nCb~%+Gl@gYJSxm}&A!D>)+3IH}Sxu!k_)rTKH2WsffLjbVin{O>z7SI&lH ztrR)GLmB~7Nw8pN6=XDGg1U`{tj{KhEzQsZ?Ap`N2azMf{>iF~b7Hd+Ows3ue+Var zf<&Eb5BJsoa36c~O4`at3rE?*bxbeFEk6okqWp>nTTD+Re!?1+A43ano^>|dFv6aU z>bMngR;%5Zhd;XjUN^i{yq$RI>tM2Xu?nuvc;T!BaG6p?{RIKZ%lK{WV&J})isbC$ zEPh5DQrx$fy&T@H`wj@I8kDZQzv`Qf9XzxoPq)qvnrD<+R(dz$&9B3D^kLi#Y3@<1 z6dYr@6=fS`$kZX8s_T$6vh>!6@-vSpyC}N;WZ$DwzQFoAg=D=9olMEfNuTDhKf#du z4=`-!{1Xi2{=Wl5{R~2?oF`s>YxO=|yEi-ZDXn>6MJgdWp)IJ@IhNXdn@N@d*ayH^ zd{ZVt(gXDBNfD5kVr=D2?7>gI{}2n^2iDAtw`$%CudMfcTP#YG7UYNcpXbwV0=x9Y ziTXGn2S#~(B1+(<1DO0Clkf6+?ONg{#Yo)zn~}JV{+p4A@yZ^!vz<+EO-e109O1Ug zeGpx3d&A>{9z-JBNLVBHw$L^2>`MFNrg?(x<*Y$(l!b-5g3`HsWdV1vDhXg9dD{^B zgfyR*y9#(gtR!@o)yp>eEq4r!9bV60Gu0lvb?{9JdblQ`HQN4eeqU-4z@YA|u5?Df z&(Z5@Hd)Q`9MzumJ0l5Jec_GAVvJ*Qsz*&wqt8H~@3;q?Z4TPRJpc8Q`_s0lIw48z zn=uLBGRA9pcjiTTCr-3DRb)qL`87&C{4h{h2%z9xg`)!_>aKLS)20{7#2|aPB(u@R%mGPh?99e{Y1vl zG}T-@X@6xAR{9D3SfaRa;IQ=!Z~~Psp52N;{R1Q=xxJXf+kU;4EE;gVpC3OyD>@Tu z$COPI^;|!>tLAbxcZ!5vWd~(oBlHL0O z^^`fs>kw%@Phno;QX>tGW+AjGbD3G5I3$e;TIOVA)Z@#*;XdkRQmB$AnB+ zXsy8JXL~sR=ACUTDuTXP0xNv9<&>nI11L8%i@Mw9V*%R%x-;1;G%by)@&_&)@QY)w-khaJ8M9cnVq(? zws%(ES`T`lAp%v8IJ=(K`C;so@Tf!eNAEi&*F%@ST|Q+@K5e%zl%%#A1Sw;1$9Brt znaD5w+8QOCoLDP7vCgA5`-10b(F>ZM1wdWbX1o>Cm^}I3>m>c=T60`SaVP8*Wdkn& zryGX$n{ z&)EZ<3KMjfhuR;~kUaD&J&(($4^0iYbvi&2sXtDi08HNxL(YaxwL|ZLD zx*c&{QUW7a?btpi41h4VUr8p4X-8iEC#>e}icWk1$z7z__mi?t>ZLj=%ZRNj?iVe7 zxZB*>7-T%_oS2=sbdhHxRsHQC(==A}rR_$L^ZB5%hRINGqZ>m-} zpzBVD9q<_tSM=}RU&D(i{=#D~k7hSp*%KTk1VU(8-C)b#Yqq;(F|OlwiXhPT7iBik z-spRJzS-#z_ISyl`+%FT%1 z?oq@7lM@R*piB5YWO6$luU2RflV4QR!CFry5ir+b7|)Sv!CerjGqj%xG)gso4n|mm zqfD@e2G!HS%&VgcY3owqbz5=8u{Ifj!{Hck*;uSVQPn*m#Ezbn-MTm=jyqu zveuW;$w@nIKt#^{c1K*tjqB9G+ zR2k0>nyp5@L0bW0A$)R~7J1MGiI=S34D)gZfvkaC?WO!DB{Rj19a&ZdT;29;xa_)1 z$jYvaZqYH&>Y4wkYWd&#fE&?DBqRo(IDt5bB-WafONrfu_$o!ur>ztUfA=(!ygq=9 z>47dn51M3+w_GeWYs?kr!bdV>3T8Y3HZp)$-psEkVl4$?w43lclC!pfp@WE6t5G(( zPOMJ5CQXx~^}W!tp6Nw{1_>$=r}sX`#~!&qvwPx$ItChr{T}4NX4A+c02$ zw7^Vjf{=z0x2$CIDAOOzT7nG9sVdj&l3d^%dbeo7oEC~(-3f7|_Up>7SxV8s^=AGa zkY?j}JHgE+4-%#z3ES^>)#SsJA?AGS)YpdZhNS~;nomw?7?nSfH;Gos*Gaq<AXWqbf5CvqU9m|DwBvXxQ$S#HF9>29SF%<9;p;8aBK*V6%ftx85B$(RbUa(OkU%S~YKt zU{AEu<1yC{8zbn{tDne)8fqae6yLKyjN{QVW6Fbxo@vPJNbA}KK6wU0`2qhT^mB}3km?3GCRkB9WGclPW^c(2gf49FaME+y_ z{(A&{8uVarbEb7}u`jdq`}gbSwu-zcc2a4Y2$Bvl=jXdtkx+f~7dXXCBfkF^I5qx% z7?1BY%tR;(jX4_7vdYW?V}WeR!eyzoa(2*5UjQjB=u87PoAruEl-bYky`l@aTfg5d zT=R^S?!O+Dn*kkPWAm9h<~yHcvw8`j7hV8GqsaY{ho{Lw0ka7P*+dwziU1reInd9r z;NASm35c9#rxG^Syfv;H!Chv@QZaqKzlFQJGhnun};u^?ws! zty3m8H8q_jOLaBy%{uJxx+Eq0eDtl*Bjp}fvgy;Cq4H?gt91+VxG3X7UPCj_IKY@S ztiS7IxMwFtmV_T=Y+-WA{o!Mi)7uekg5+g#Op~h6cgxD$X*jpCQ4i1RwfpzOW1{Ab zJE)O7QNR3jj=QgR)~yWAm0C9jfV0Y^zSpg*Rw8~mZ9f&{7j|lU9%$xaXfnq$_Ou{k z-TU*+rC+^1tFnUhEDUaY#6EnW<)h_V-*C&DPC>|{J@i~5Brt!%rHN*}ik?rWYNoh$ zUB-LAsCL~AIje56kP8gy-I}i$wq+-PZ^8~2_1y@3>?Qj1c;;foo5g;c+Y=m_hzVGAV;5A^2}Q6j~ViSk<&^BLoxmq>ebWh)dl@RxhfE^N5I=hCksF7ljat+6Qb$_dKKRyfbrP!)QLr)`{arv;TS-HJkEy`e@>MPIOJ=G7WT362JS0T{t6b#?+ zej2TSU|HWZVZAn@#uwBOPH_UY+gL}ee-q)zVsq>CIL+C2T&!a3dOVn=3 zV{e03uPh94E51Wbp?6#%>8#sWWx+!O*NsI!HD~pBZ!Cboqvn9#Kgu2GyxHmY{)HaX znoE>q>D8;b@jpMV=El!tG0eF*-TyX^d@ab_5zo|r!|JqV@OSb)UQ4T{7_cQjjVS;w zT+_eT#XIQ6^g5sURkE9)_*V@D`gf8vs!XIEXKjgZzOH<=C>exEO1yqWV42ZVEzOFN zpV;Um!x%Nq7Gcbt8m8`TM#)dR%WD;J3g0t;=thl~LL9{`tzNs&#{=!V_H>a-M9=0_ zPQ7XQQ-DwaG909s@8IvX8kZG`&l!?JSpz_aazhCdzO)rJH4&Y_x`<{#uS-7a=|IKG_uXnS}? z1riCg6E?5DZ`ED?lR1LnNPXZ+-)6ZABCtnfq=pBc2?3XqPD*2~kV1fyO)Q^M};2$#`u|)bAQo%zrCO|D8vN{(}!jo!{z_+o(z<9#sacP{DZ3$#IIVfC~x#-McEe z4m(W3wa>8oK4c>!PR}j*BY9wm-Gp_YKXzv&;B!SsX?eL=Z=8wO_~pW_z3t`lA>$F+ zwa1zrh&7WqGZR7DqF3AXj@`Eb-HCm`+vXreZh^pz(tTar@@pxQ_T2Wt2Vky?e)8a) zEZuZoMNYxOB`;gP8a)$1wOpWVQXCztmuKB_eA~hEYW8}@wqHlFo6fO67N$rdI7uI^ zl7V1)DZtoXG$Xbk=|#*fCzY%?H_h>|x`KU%lX+JBAGm1r7`(2>q116A?#~|eR{~+x zDm!pmjR~aH*!cZt<06W%t^f@YN|qCi8-C%57I}MjO!Lzd(WhQO^fGRBIsrd__LgFi zPA)nsd~MA^%^$UQT()8;UPWY#ZV%9j=I^v3Gg^x#h~GHsx%JM+bDTKtT`{HBqLa_9 zsmG)r4LnQXk4i2AuePaJq?5L;v>QsQtYGQw+Oys;T%mDgs$?I%<#J z0KEvv!x59*-x$#y6W@Gt`@V?)Gf(?r;e60HwcG&*-yXx^pAmQS=Dyf=u!U_M8Q>lB zIKu9_`@wvVE?byt`rmVF2C!^zw@X=>7XqglJ&O-EL`0espS zkMOmtOvk3C-m698({)7ieG>)@_QUM;qM=ocRgA+PE3XcJs&oM$K?dVNWAnrim z$JOml!)X#jf_?ZbyYomvH~xeoBy9TKT1uBG%8p;Vl3N{_Z3;QTVz~AEiNCloFSC=q zyRYBv*;?`SqbZErH*im?u~p#fE|FXtZ6eZh>dO{5z``~4y zDzcliBNAR2yu!s^=2f%wIwGetbb#4BvSX2puWL)-;M?U@xp1TB27dYlWG3d&T{FJ| z9$KNmOM2wg9hv$JN)j)YMx+ zdzPidX<;<~Hlf#~C}9NbpM-|}MO3m*Qa6tDG98%UYKC-@umtd~LiLieWc87aAN$^M z!UdfP{42}L-O$kx#8n^-rkPm)TIttE4$f|ZzN}NMet?27pH8Yv2q1WLq&*fR(Z8)e zrFX(W-uc2JgO~mjWM1AFxIpgquGfdIFF?D)FIMmLYi6U@K0vP>&PyW4!cT%MyZ|Q` z((PYlN6ug+cK(-uVwajaEZ^xgvsOr}_xY&X&d!U9hd6vtog9x43Rh@ex3{6Lqf70t# zF|z%}&yK?C$vuwLDjZeojnt#>bbrYb zEN6hmL=TKO!=7N`T-_X=^f7n%V=M5uRO{b@f?&0{Vc^!uDcpch`sEXTG9mhQ!yo~f zX~#hgtCT3DwVVxXGE?yc41o^O-CNnM@fW=Xh|T}};mm&nVLz*#ItT%xa)pC--40go z2@V_-UyGrn1C-$C!L2q$;J9s-L?}+RMYANo-)o>9rGBcqVkILx)av`<1tDh=b?v(lw@v>NVha#}h-&qrR81#Qe4D?0 zmQtFjH;EYnFKPSUn1W}m>YA*;eUjQW^npdOzkmcz{#WrB|vH$#j@ zg*C_>%?U&oJAW*Mjm!z?ZTuhs4@8W-1JoTT@fJ|8{G+Qe40puJ^_NyzKpiWx`);TY zL&Sw2E6b#TQU3L%!w#&vlw_kN@o*QK?_W6>sZwixZNMC?c+ytQB6<$9xcp)@=&kSj z{u;DNuj?5lnE93R0E^qp z`-i&@1>ALxBto5%CIWat82KN~CiqQTdko)PTNIbu{>HN?RRzASz6_66Tw53W#p-z) zFN+QFaM(=Mgx^RZ00)!!Z}@g?;hOd}_R$de(%-(69v?V$EvSrr?^&_(7iDnM{u4tp ztB@ak6u|u5ue&%44B1YTukT!m`l~muUPu>!W1)oqMtUu5`@;8*tc3mlFV4I+zPoPC z2@>OtBw*HcS#x+Pk6~{-5cyprPyG0AJe>V6_y0@zo&A}JTC7^&ub*eV_xCqv+!lK| z-~&(~p;e|tlV3PUX}Ew~;zZApDP;d&%<=?~40nD2*6cs~D^xgMcD?GpQ1$F3?<(|~ zhEM_pd3)B1>b5i_d#OOT>HbU(LUfob*+Jh=Q}6?R5^Trc$FlFQ z3a;;`?T4l)T<mMY!X#@1@3w}s#-u>%r5^)RT8mTw zm|NN0XB$JH>ZBc%qL4Rs`#GM{$_Kq;7R;_*{+2&MVs@@l+IjQPDH{*8A#O*gojkw= z;?%3jB$GMPF#3hV3f=5rUdY7eAGy!SpNO4@Ega5r;0wxGx$#HhyI|BRQByJ0xaMA4 zI%-HYFX}Sst^kNvcHr?D2Ps8T202}B7=a3B)XiN95DhvAXhuBJs?{jv&K`Ge-iYLV zOMc*tfIE#3h1#hzB=A_}Qf=G*mcfPqvmUV@RDT?K|76V?z~rBWs8kwzilkH>;{u%Im=Qn zQv{~+uS!@gGQ(7-ER7=G5HHtWB$v+fTG6cVM13(I{<3=-Ea;k_8mHGU+{*tUaYM1; zE$}FF&A^Jao83Up_F%G^3W_m$avwmkF0CbxK0(h7us=(gJTq&1)_aptZ9#qp89mea ztqXvlZn7?U)b=j{O>@l& zz%I@@A+7HD_QfN)`x(`}WhzNhz0u~kr;(BhZ=LU_&VG3K{9$l_Z4~B_4+-Vh%xcw4UT}_)=l#p-Ro+BebavWKK1NWS{Ri7V`9TP{8Me(DK?sY zuPrUHpElWu=OfvL8(tdMfM~ZJ+)nrXKGUzXkJ1byH-?S_!7?EDkW;Voov@D9<~k-L zF?u(b=;f}y0D7`vQGBCW+%a_FBhd+;`;$v{j$aoHrJjsDwY@PyWp<|wAAizIrN`)O zz&C)p_c8${pcbxb`AsN#D80*pC$cHwcDvqi{BUBo!WZF+t1RZ1Vpb7Wd>ZYg!zi>~ znx5xyw0!Q!Tw!fC*Ye#qhc{e$VDG$K8!NZrVSTglSAz+i5^<% zmK}Z`3<2b%h}Rj=UhzN1H%j%rP_1}HqLr)_l-&=tyEX10>`u^4m*ZCrXNhVGp$nDU zl8%ZXWywNSok{Q7#3A3Q07xM+x6TdMI#&RxRlOlX*V(U6+nL;;uo^YxU>g?Tbff?* zZY(agZhtPJ>Q8BXoAnP(=r8ztTu1+YWc0I(2lU?WD%m&SV&3g{AXUpPNHdIoZuj_m zKbw9wgD?>~dqt9%nh&?( zDWTKxBNs9}MfC8%0^__4;)zK>dtC6{W8%`T^3q4&qd{Ys@7hh@D58eWB`=u^2Ki;9 zitGZMCxy-c6E1IHE#!7!k778Qs=ITuC7yXWFayIXD|v=@P481@^(tYxT57^^(RxPg z=O^+ifcY@Qy%!RQ)rukuw=ruoYHNzpem7 zrf@Z0`On%WCaBif>pq3+V266yuF9g7{d-TcjBn4B$GJ;o;eVIg{82kVKdJbt3d)Ih zF_}iUAAq+{3$`Nd)qr_YpbxuF9u1Hz(J^-h0){MZxaMu9Uz(iF7H98$vd%Gf;hxdS zPrSZfQRHwCYO*W6__* z*<`JSRH#|`xB6&-sfrE|${Fg)I}xmBu;*(Lz--L>XIKA>t)>+)GeZuUrHpIe#@he^ zfTO0rhKMh^Mu9s@OT%*Fguqzs2jJm&Ozn-xAF;QwZdv-KYy%y1oehZTC&DTtJWq{+ z%T~H&ak2M9XsgozMS8m!fREF<@(u)RQTaCiIYAxEd+M6?zbaY?n1_Xg$Hp*I&lb5W zKLu78oeFe;qLlCx8v-}hO(q8Q>ZP67PgzEH9RfqZ7s*dWTZ^-1092{q0hOmJ+dp46%SEKpAgO{qdI0| z?S&UVyQE|aqs63=ih`Xqd3jp(!1IN)#gvx$A&-pa@5kE?m2Ott3S~YTabbT_ZPvC7 zH15H%n@{j}ulYgVRadR1MT{4&ufD!gJ5M6uyx$N3EyV}e_3xI)K#7>o1BVCeMmc{J z2%hkGX4gq<$^jyIbF)*l=iQ9dR)=A|UV5OHpYP{*k{KYpob9^*Rd%f&(FFc_&o@E+ z(Y;$*wfpFhVeYhof2b^!kMt{tu1(a?dbM}TVZwh4?AwaKeFqS-C;_1fc3l#fAWa?O zH8#ZSk1BHK_S2@>g5=sGh&$ACTM4F9dl^fCqtw6Sc9*>-1ykt3TqOhoWX<_j^s_d; zC*~rOKu9}4I{xgV%y-zy`>k{FCO=Nj|G$ala)%A-d4*V;+)m-z=x7Ec@e-V@g=+#^ zu;v^_jQpPQ1HI$2P(bNSx@T~8 zk&m*SH3{5*VEJ_&;DVN`o(Y$o;2R!X@%-J40BY^l48L<&pvW^^j|~{3`)khecX*#{ z_?9h-CvFUFbs3s30D$?sCjycKa#0Gj@^byhf4AvhtO2l?fBu6{r^hqtQe&~zyXman z=7h4=Ri>Wv|2zxiWSk{0zJhe1VPt`=8am>8ZrGgYU-3R8V7}ODj!XMM73p0}4Dm+_ z(K*Zijt~AQAAk+*^8v`8^`yiGNVu`pw%=Sl>sHmws@E;S!Vbpq%PeY*+v)B!fQ-8F zyGthkC9t`&5#w1!(}57_NAQ7xw17^I5Rf9Q8Du@<>}Io%!)(l|O@?oHW)G-qmaIFe zmQ@Q?zfC)_S_%Zbl#sR(z5tBwq-FqEAAtfJK3j$@`!kqLV@MZWf!$`%>RJH2xV+pV=ToW>$EByWGrP%X@oSUFeB?&$}&QRY-122StgNfFwgJS zdCt>0&wD-B^ZxO^*Y#fS-#6FI%zb}fpcfTU-b#evyJTfab7J^ukcurezOL$Rn*kGT=F z(bi~Zt>2vnM(cgjS+W}G?LN~-KUJUo*_c1C)(Q zZCD(7Q{-So{P>8vLiun|5zum1YCcB>bjSGxFxZzbChl}H$kB2@kUGuMoq{DL6{FT9 z{m_>;v+*108}obw#_0y!6cLPw_oSl5Xk2KBpM2vV8j}@*Tj8HFA#o+< zY?ud(L{5(Mh`{oqPAk+%%q+9_G1rpKjkn~S0-Nctnc=>cQ5%@4s!h-JgLAhxiCp0e zj0h>iT%)ae9K9bvZ8r#Nr&U2VXoE7WG%yFJl<-vB5~$O6&vDiqSc6#TicEE@5_=PP zzCjTL&L(*Q>G4OvvH>;``@#FR@0U*Xn|Z3!F3hJF%AbVtTBMwTm2{BmVB7TosMSOMi(S57IPFC@*Eff`m9TJ2q-i0)k+5=dAB?17ht$0f zj#IZ$DSPJ?0HE>YXf*5KU<$_VRyXvZOG3xd?Wuv<0TU+$u?iDJ2~pUi6$f8A#3=YE zA13}>4cB7^8+14Y|IK!81C9Z>riaxMDb*7ardH&ww)9}Nwal-g+~=~^HQIf`o@VjS8Hz=V85JQ`8IrKrAs#!Kt5Ev!qlpoiWf*-GK)#AmBr=05N7+=2F{4 zLtqZ_j#M{9@NNecP41CCRnzZ}lOo3d%z?xx-V%H|(*OD77x|TcMiQT?E&wC3`rTFi zf(0T&zksxntVn)RJ~Wmv62l)gx+CC2qWkQ-Vf1yA%bSc3oR5DAo!Gzj>9p==a2@Q< z7m6z;1+`RG1tY|Iu7b6{{u})by;L)qQOB=8cX;p2dOQxj%854DBz+45%k;L;#bW*s z&+_k`cK@7>bplv5+%KA_mOp$2qkBR(7Mef)3o|kcOl}?qfI?Bm{yQ!tX+!@AOMjwS zmimV@ihT$zX#R^s2<;>BEt(!a5_m_tOG$Wh48y7Lst(xA^3Ad_ft}j64;e#KgU74< z@lH2?Pysv4H+#$nYHReMn;Ie~bXV)S$kdUo;Bssez3-T=Y0-O3ntMh`m;SrM=J6#!Co`Y4NRLPbG|L$dZ=R%YQx@P^0Cf(XI%1&a<5Hzs+|Y`@ zQSHFjrWt5E00$%|4lMJM7l5G}=yC}o2wX{MO$jZkZ(v}b@VWX$Uu$(5SoP4ki?`1T zc8o9$CZ|GX?{aiRK5S7O_KyPLo&JYxpX1+D0#$DKGER!KBI~L__#^AIJbml0Yt`o@ z#oGxfYJlt=v@r{qe-!=vd6{j-m^oW;cU>rcY62o-SH4sOG11c=Wd|r`;$<<%>4=<- zP_1Lewwl>xZj{Yc%wQWg-V#ai$+-6LWn=BFLu1zZ7-K5uY$a3>@Vr0H)>s~HA?)=| zmW!pXmM2~mPoPWdAldc9R+YuPCwVa^u2-hWXI@gq0?zq;M$CF|-DNnLxYOg&Q9Zx29Rcd+$ohEF-6&4OGKWyyec`iAIlV zmNS1EiH>qLa~`|Lw-*Z!dK1FgTmpmfDa%8r8_R&3GAvYXY;YD6w3Ra|vp70GI&*FS zxtrLxS8Ma^$onXKxiq0SCCuXWaph3lQsxuF@byyr*`>xRu?u{3v+0z7VT z?Y_i}8@7NG+iOxlze-pp3SBkX;Iqqk@=pXwMAU!{jUgcAL`>s?#k%AVf-^znAsb^}XiT_RMFI zNBxnVd*FO(ehz?PqEXvByeCssZslZH;&)ir5%r*N)SkOgC2ye2A=2W+-!4{Kcf=+a zt~|w`X*6UkP^J^q#Y5Zdcln^+s%WWOziGlAeiZ@-9iFDdf z4gHLNxUIa?HMbPBP{Czn)@i<(lC%=>bHGlTicLA8niK+oMJbwjZ=t;zv3;Nwi#p+b zQ;$UDmQGLlO=PD&99O}gh|B^gVHN=n`#8&%E?Y2&k>B_AO_NABhE-iWU2xr3U^t3N zV1tKMQ+E5?zaTegomghw(d_0G^yvm9<|F}VqKnJ&(7JdZnR3D~7KD_O++^h7W=(P- zg%f#$NY*|IAL8(MN4b~5OO6&GLbnw&KzvzIr$22I-=8?e?uH+P>V*UVU7FgE`|ox zXcH4#Fj@p1Kro!;umo_-=a?xc@}fV`_Gp<_o-ZSebCx_XgLj!X4~ETutEkR2BUo38 zT2G%jUFrix?c58N?fYEw6O$cP0?!j)>or~pwtHaiF5wh3#Cg}x*FZCU$yuhuhH8Ir zm62;%V$XNwM*C{5BWJh`EIzvI8~N~yD7;TI74MfXZYkdn-f&czWOGWBEH_FZrFMk@`$#e_A9?(gWgIy^=d8Du{ zCiwK&fsk;C5L3cmk1 zG_q@3{A(KUR$IUBfGF~H2y2i>DaNPZv1KyVDIx_LAv;iyT@f2qmU8TfQG#&uve{^j zqaLjFw-}nifwx=xeBf}CUk(+%4?*;Y^Yeljzh5G}ylOm!ogB>LF?&xKQ4hG}(maG3}Mpn%3@iXbxxnQiI{6d!=lNi}GUH$|PsZ1Er&{ zp2S|D!*tGW@v4-_kN2gwfLc#m+3}V--d(`WfqHh+U8_*FkYCGDo;1g%a2DK(TjO67 zWZLUJf~S?{+o5u&e)?adWL%g1$s$$>U`k$#lmSn}=DJ*zgTm?-Clo>gg%pkG;t2c+ zsD6T7PXdOFFyDHgJg-)5wjOZAH>Qsp-KD`la2#B%tLH5grFZ{FnKE(Ure+cSznCfe zH{&t87uV`NP;AOL-K!nlQEsP<*UYZ<@UPm@Rz{}!OOHuhoyYC=hw2#2-ELGzFEYbs zRloMoLa5udN%Yw}p?eotlVDaErquWR?&91T+cgwn{_C+S`nPBI$9xZ!v4=djP6HOS zN)k=(@ZPG6-#tvjEH22=p(2UAbGt*X0{}Dzxh>IKWITD~+(MD9%Erz%H#RbSh;jg( zk@o7h=%t4s2Alb4NcFfz_I??6gOt}M%4e07&na0bGbke*eKvR>j|gL<2I`Bt+7&M^ zQ~!;YsPLgs?_m{fv*F{#q0OBRR~ufw9-i4I&{(xD4PSv;orX(pZo8p=*MMHMsltQ~ z8_A0oo}ujQmy~ve@B3;=`?4bd*1KWJ8&Yd3WN&(;T@zEt=q`KhzRjpM6I}j0R0S{P z_Y|w4s{ZyNL2aAgP9lejrrqGTBatTR7Q!yg_E|?Fmsf4@Va_^@4KE1{%2tSmA%FK-`@CeESEMkLQ2_I<;l(Vo9aYo=uiEG0KsGXY36`gT9&-a$q zz}aCfwO;8l>PqONX!Pt$Am!8*gRlW}K8E*u5mE3>woae$H*C|De7>>4Gp+m#qusyY z&(#aqqMq05rVHLgupZ9zCnh5Gx0%4p3OdD9dcKB zO(5J@$|*H|xamP(|3O(C$76&X*KFcEW7lR`ZsqaXSr6y~?MQjNh$YBKwzj-NXoBAJ z2q=zsU%TCP_+!(;%?iXmOK&*E-)K3 z;D#lV*fHYZX>8(BYSjRrP@>Gwbz=CiO6x7HNt^r7-JglgJf3gsLdPD4bzOA<4)cEP zAEvA0D!!L%L#@VJ0B#(6k|6^2aKmapDZ3);#QCfBcO`DO#hsf=1qg?psHPTJQ9bM=kyuz3uJi|F7n)<7$TMUo*T{NT?-LOo>4w@bkzRTl5BUXtCYp*S=Gl7p-y(T z!>vFCS&jpZ2fi`PBF~1~fza`L zD{$^0xCrd5AcRvfvgED#Lz><(lqUJtH__^U5BdD(-tGSVYs{aH1q*qm*#vrke}~1` m$jq=<-}%N1018=-pK^xm*)H%2CVRz&%QwPyKBb*2xO)N{4AI|8(dk{Q~MEo%m_SSvkg+%v}_3c@{7;b zy;~uWVw}pPmlF7W-sxS310j$Fb&8)EWK{Ms2*hcn?T)REVV>M>K2E0-%?y_*b;gGO zed^p1&3Q%2S*o`>!dA8=n7OeXa!3UzXj&jU z5;fXkh2}|=mUX!u%4*69%%w>yUViu#y#P#hOX6JcbdW6>|DBH}bs;Vxp;90a1ROt} zX`Gjr*WKIOJJRuTpWNTyUj?2pdIF0jT^%LN4+#yGz&DCVOE`W{dAhJD8Zsatgu!5B zB?YH7WjM(UaGMjxmO{=Yy4L*v4EmxPFP`S@yrB2s=6AmCzD83_B-1(vz0`b1HhSR+Mmw^k{rJt*# zCO)rZjCry-nxlP_TkUCZ(p<^yC0?WjhS5?-RWjsA_*-@1z;BPv!hczE)y&LHT(xqj zyp(7NUAAnDRK&&aG*dz_)f~G)X!*D{Qfa> zUrRGQW=}^JLQ{1`h0GL=S++_Cm!#J!92@^LRqr=Hx4`7`<7C*HF?#SrXQmCEL@Dt; zs8j8Lqp&imsR=7m8-nKh)zQlz3ge+TojlL>Gmjf`GmaOC&W@hqxJ1eSX4pu_z3s`z z$+-tu&32NaPxz=$`cnU#^mm7w3v9{BDR#D!mgY$LfWgMlhtjs<>;!0DS)ZinVMu#m zKq*ch{(gaAJV!6~Q^LEUg*#0|&wDo}94HK{dulTV<3>wF=TmLTkw*K^xm% z>fb5y5=5ov4kQbD2;P0ICgUcVeS4DIL$0L{+-xj{>$X0DqAK&9rJEO8ELc*lsgsCL zPh_vG?mw04X=mC~a*xq9VnfZ*X{<;W1-Y-XQY**9$>QOY+`&U+|AwRF_o7zG*t0D5 zDR^ThH$@wl?5_#w_hl8~p{3&UZ0yr3J!;a=m)q0=qHDrEUQH=qV?=hEZ)Tp zLuxZlzA1c7X)hi}to1YAXcO|b97auRtfI8uUP(^UPEL17OeizORZKXrX~mDxVZPWj zE}tO?Jj|jt#P1ih!wT@Wqq)*HM3XFRcWHX#tz_X13!~pybqU?g+bbPPxn0FO`wvS7 z3KPd@y9K{fQ_F6pHFs?mw)GJ{JpjAsM!0<^EX3nZ(6hw)`;|k%$+~X#^w^~Kbc>Fr zM-%#~B|)&=g0p1fp{_?{|95Fjl4EnTSCIQqMclo>M7D!9f3GhG%zHNEcGjIbjJU2~ zY&%Lr7F$`CB6)d)e3Y>R?k?EgflusGJIp|k<`#@Yc|`oL7Q`-NO@AicF|q;y>4;(7 zqK^jeV3g$xavq$SKsc?^@#kqi%+|?szRR`^Jj&6s^WF!?xCI>U+buC0(y}8TV0gjQdy7Cm~t@E zk6IXQ+gDV+dz5EgSX_#72^cdu0{Y4q zov53F**so!Tk@a)xxH_Vv?#gD__})}F|AtU^-|NnL4INM_r5pW6kWGtx^}_&&0Qnm ze%p~lVeR1~$btIhuJ!{jA9-6gJnL_A^W{7adiyZAqvugXV)-50QPaJQ2+EU=6kmzM zM1=t*&OJ1ojR@J(-Q2JcB%}CtSJ|WB5S{+|M17*?TJB0}R<>&V z_P*DzQ&lq1T&>-LsMMQGr`)W*G&7Ea`H955#I*JUh!2EYg-T_6X><0)-AV6kmY(iV zduB_rd>94wO-U@b9g3}H(bWcDsYTuyo5dXOv+Yz1^)GpsCjaxgMZz*cvhX(@=K$sy z?JV+ji{+a~n_m};WtvrR-U0Yda@y?sLDzilP`kpl%^%xJCpOi_m+kGpcEq#c824_X6hoVc)zii(JzyuT)_+CEZt6zg*#VB$9kY{IjKSDoV27e*jX!k zzm=&z&CNEvdpjOUZqZw{g07i&+zXOjD{!tVU$&XBv%wW5N$%glrr^p_*yR4sCL7<9v9qBYyw=k2; zK=$pAl*NYR6gURt`$8XjAA*J~`^y(WvL|OBXE^YsA+{2OcsqWXCnG+g5$b$Q7ulH} z%JhkdN)wL`*z?gAN6Gg;uyfUvj4Uwhc2Tl+U;gObrg&hP?Pl?7QYF5HGe{^~-?TsI zApxmETYT!xc#}}o@d<)=srjl@hP#!&vDE@>Apj)w-ZaEPw zRCV0hx1VV~+hT#6nRs?#kZyI5eIOM-PCUyA^RXgSRF*e2r3^j|io&EO93C*D67X4@ z?Xev+qHNg&s;^i!8RE}K53CkR?IP?0C0B&4#0uDC@yr^n6UvgK@TX(j5B(`U{|?_HW53H%1UAy_uk_JCPX~nEUZvQit;b@*U^klzSyacN$w!jIayrwcd(36Vk)9I^B)2_>k^}sA zlOR4fB-RNwhJI=4eyfzKI}F+dPfrbKKwPb9(AFoHq+K2bhZmY1)!6LN5R{PQoB!lg zpVB#}MM9f*M`_gVNGOYo?}L)Nn~zt}9| zy*UhJw#q67ywA9*j+V|pfBG=qrWOV%Jzt~-!d=s=|ILrw$|5;09mc_oC(DnE;FAbB zE3DDi6+stnnxwPS`9|&&od>Pix_UZt`R6tjJw7y45p$n&{7s1cD5k)Mv3(9i^fO^3 zhOdoa%Tm?5v2X?ivd)+igT7^Y#D5}( zL4x_wG?ipsu{d=vC>VD`ju{6#NO2J1T{Tp?T={iRFFdgd7)4Ctp@V!`oF{ktMc!L~ zEW#OinBi%}iBtPj5mIX9k)@_r-juL4%1!7gC`+dhxL~N7{9ILuGTXPtDXb%}gJBMR zSk@c~`>Y~;(xX2qks{&y-G-RUcDXjwiud@DSs!l#XRAnonsQl1s2Q6G_UdVdrrIja zxq0YPe0E{?2EyMe)XbIwo7i{#k3#ZM8EQJ>b${igRTI?ogy+E*A)E4Y>xk9V3=82s zqFP^jjC9v^Ay>QXIM#bzjt=z0A!o@+#0Xa$A*;?tqSplS_Fc{eofWJ(#EJt5lJ;G_xu|Mnq)7sBiCb82h|^b)u3+-@Fp zkdFDl7OO2x>zFVn?mg&77B)&VJgNzO9eG@DKALnx?N9Ki{WN66Y53=+GK`{zQp2zomFvCJ4x?jm(kx>|8=AKz2| z_F+=x$~@^=Ts18O`q5J0C3V#)^CD-XQC&~ZtR3$RX^e)4$S+@K`IE*_)(0>!bX9%x z`A4JptIwe;{0+;BgH^fXlcqskdVBF;#H7~l!^g2C4 zh2p9KO)AA&o-4ifb7?T&$Pc*vlV^6D^D(l8S(}9e`Vlg)ODCK?XmSQLVvTlx z>{Vi>UhTHLnHfASNieMCYJuIXVnjy>^cDKZUV`n?$CMaxD|6||YQsDoZhFl4uiUI7 z`IU5MvQKDT!O0W>`(j%BLH1Zr{Vg79Mk_1XMULSliZ{@cVx*>BYJzyn#*|bWX~8uQ zfM@rSlxV5@bzIV1rCmzf{LfboLmW<{w8A3ecL z5=TyaN``jYumf=;kKQG*9zW-RP{q+j+@_4U{|YHXh|H{RINy z`u8^xg>glY9P){_eM8hMN)4vj;^P!iGb0c4(L{Iu`_H|Hpj+C!*HxT*ER7m26YjtZ zdk!*|=GC7X^9)88b@=oA*S&-W?q=I8WLPb< zyB*sbiFfUY0JDHt3u2_PHXfmU&?w{g$ydwsJX?gq5CVyWeKs~wHO>2^xrT;?(U7N3 z1=-lxprzg2-F=28eu4h}c_^Rx=E7UIZV~hGcoDgJmswWG9*k_VoI;`4^zUi_oKjYY z@!2SIVcSG{X@OZ~0&+pYpygl`Sgvx)Z^9+-39_@W_gU703+EBf_dh4Ec3c}IZb|C6 z({bAi0oG};8*J02L8kFl_-qxC(CZC0tj%2Io|A)jYhHR)b#-;(K9=aWx<^~!zHZzg zUCzBP&N{^VurV5r7^$OTw!?BidhKv0c>vDkBwpYcd~SO=vEk2w%uP zD42B5CdMQ1F!khY9fls#VbbI(yy<3o)yU`Yy&; z->~<-k-+Mc<^kBk8IUa!mWrfVRuFX?x>lT&22m+cK3ZwPrgr8P2m9{&+&~X_;1ET# zBV0*31bpASpTG^0Td85swJ%wMEWsQi(>9ga^O5dDGQW1#9`jXfCJudow3Bg)XGyse zgR*X)D#pC&meO7=dYOKH8bSq)a#(wKJjf)k|3hf2g{Fw1)P3=AG{JL#- zuBxSL>_kJ_*CZhSO5~N1@vv9-|d~3Jmlc02ruJbow#*gu(d7jw{i0 z({wZ zuzS7a;q|21d!x&`%$1&*uFh=mYu07%dpdglkR%HisztGduO9av{stngW*=0(U$NO1 z*B$EyU(gIKS)x+QLXaN>SCG%XBcxj(n7fBin|6=po0%4t8Yea-rq7ChS)UK15Lkn^ zCz~eE7}A0#9v$yCvPe?JIIL+dnqP1^n^h-^LqrxcqQXlgR}k_mJS!;LL7scOKmp?W zoRBoEsJVx0hVjo(8_#^m&DAWcuI}dY`B3BF?z&jDs>a5~gX{%rYE|*^@w~yoY&@x| zD)QN%JCOlMq=0+Pnp{aFV*Hjp>_Z$rJY-j?+F(m9DGc|>0v;O;+>FvVv{JUc^IW#d8KH1{(V$0b>{V}Y8^L<(qjfs(g+c` zfusG6L_>k-bAvJb39iFoR0{{6-AC{aA6z<&yK%Uc>DVcl4}s7rZ&T4+O|PvkxJDPP zrh>#>G-tH@SvAW>w{KB&1hiH#;}2l{&|i1AtwSN>=zWZ^<&_#i1?;37o4|L?WOnQo)Z9lxK>r7NZt4?Oywit3qj)P zVg!P`@UMmk7U$oF*Pc6Nc-Uct{D>J1xZH>Giy#mN_6h#*5CRX1MKI=x!G$fJWnRp| z!%@;cH%XFiwcj0|F-3UuI#*4R6+=#JpR&NE*8k%lhtM>6Vs$mIr&(IUYk&(yl~g$$ z!AAh9STT_|AiR=Kbca^KWkY(BbBML{!ifE#Le(vk$wm%Pt)j+@7+qbKWxV0xA_k2% zn2?Yl7FShOk=>5I>+k4rVm4{LYi6}CoR`{w@u#aKiy_#=~-DH9G`s+ ztqLCeWzJt5{Tin9Jn4?=xbObzlkc71k8CAhc%pXCH`O=(H(|S)zB$i5to(i!*LzO2 z?Rg9s04AGtiZ=_a?@4EnFtOzyvr3*59fM-w+aB0#?c!ndPx^J@4{E{e7A{@S(SNq) z>CmAVSoVBm`kY$Xhs@Dumlc__!OrherP(7ORpPyW|NfXqW%mVAG^-lUuhWelhRz-P znl71WlhWn;9+-y=98!Oq&jfWo47|(>xl{)g;lu11ILUZ)-Pznpo7C^$c7}!_szFVq zVPm<%sJRqOM@)e;!YqqU?w@;RDA+47cN7#<=}sOrl1TP7OHz+g|HH6`MXBAl^unp? zXh*}i4qpVNeac{GxG)SBeo2xdt8{tILODkZ0ptsA;mxjGfsWSYIh$vU>Mk%ze_vC(1G{n_zjs9z>WvfkPIN*;L& z`u0xrZGi0&o3%xh?`6Luc2fusy+0$p%B))TMdgPt@ zu26@dmH^x1s$Fr;moa%S^Q^?n+^qRTTQGQ?^tPr{DV%9sdm&RdrTFw6H$CMY$~%2& zf`(i^zi>J~f|S5^Oyx(uM;3Dj(!Hrq672d$xJwO_xAi>orbrIZoDsnK_o8V}bO@k8 zc))GQ+p@y@p!|}wq;(m6!`(E1AuZ)o8PYmKb#0}_F7xPzrkuwL_iwd--J#-^d}+JQ z!E0(>50NshsH&mj-w@Fo8#4tghHw-;IPSQkk1}TkWj|U@(AU^j zpsK9`>bJwxD~+4MzD5LlVbhrP3Sz?`5qOO~5-Ulr!r{L!U0a8d!v_l|xBP#_nR7SX zt3nE@PWNs@j6nxyK-Qryp&?q&yQ@O0+z0^zKGZU-#Y)r_DAk8RMt9KWoLltBOaA7T zez#O2k{pLX`lA7lxU!fWX5{&dW;ObDJ@|S}*COGJBQt`MaIwt&|cZmQ3qGY(Fk@pfu z?CGAIaIQi;8_SU$YH(G9G@Scxz68HISvqE*s#`Bz8U-D;vt}GJW`Ph0z%mCBZ_ffC ztVk@4Q7BZXL?V$&mJYg5sZ{9ZRXPK2-t0!~CR;e#u-}!ZtCi}XZM<)PNvbsytl#f3 zu%oxLDi100N4)H<-Iw4oS;ZN#+T3!fK4Rj{NlW-KuOA`s z_PBxIY5Un;C%p}-5}1pd78OQJV;UnjdSJS|5Dgk#l-M*fq3`cfY;h6F`Yf1iDhkS5 zNnOU;_TzYpeCzmS`}=B~zYFuMI9bRzwc)?Dv*=+%naTFLpN3yD1=f*4+=asCjkTEh z=T{~KF4_(?d3&(?Zbb=R`n!5W)bRdYPGRMVDFM_-l{Q)R%yv56RepE&aL{4NL=lhw zB1<|jEJK7;NG z2i#)rXKEqov58T%P({-yNYmJp!vpy7EJVu-9#gZ3TygmUPOCpB;A{A0@aEziwMqa| zR-a)PzMl@j%D3!DfI<7!hw<(eZQUd+$=Dh1Y1Q-BweSCP8b@XA%-6FerP-ci*cPR8 z)*noCXWIOtFoF-8m?qw1#dATHu>TyivJTJv1q~TEPfEd8O*UJJ+(Y?2)%#jW4Hba{ z_HtRY1?ohdr1aD40mNns`T6s$a+JknCE26D+eXEEAAlxh^b3^%3#a-)??pcoSMM!b zQ={bfh!;pO$d1!K@R+K&fA3I0)SPotAT{=yjWr)gmA*BPm7G~yKK=if08sqHO60*f z*b$N3Lxhqy`&14vMetzx2R&@*RLP*`ujL%_!c8AGX_|~Js|Ro9(fKmR`1(o3*acTPR_VZ!|Mo z6;lp)Kn!-j6~%#E5He4~EeR~}--5p+9gY2Q7t6@l-rZ)j*htJ_y{sleh%&V*kQ>PibKRtaQE$qV;o=hqsyXmd`egP_r@IE7f=P}tk(t2?* zQUw8MbIcJD+!lf9q*E|M-s}CEJ93(|QO{cWKAMR_QNw26ea^c`XZq@GtLVH>^+WLG zG@(Kkg?qQXs?A?3F5@~@z@qeJdi~%IG=%~jD)1Z3opt>}3AWU_LHV_>6$xVgMvH~p$880OM1nUG;_nmSDrk~DB zWX-y+)^XI!T{0elA(eIqh3k4Tp&4qQ{}viMk*SRo#cXxr9$d#4!BxM;1LO z&f&~G{%t3*%cXBYoRX;gYHfM<%#ziA3>iJ`kl;=BY!9r=KV66}wU_E&c9SS0G+ON1 zij0Y>jQ%hEKD!~PaqpP$gBwmn| ze!q$UP;2G}Im>2fYSN0acMeliexzLxthZ^(e3U{W)M)Jh{+F{Rgof|Y7ziGUQ?=tRHDgyc1IEL z>!o8qIYQ^mnWHG(%4Mlk*c~Zo&!_qFa4^eLd@j8xCj8C*0iJIxFer z)3sq8{AO+GQ9>W1rw8V#axcEHy{xQEwxrJvJZOZ#Pd;P$1q+FZH*+GA-N0;8n0S_q zqK%F`!8!AGyNdV1>FDw}&H0?XzMYivh1;Ocb37vQvUwhXMx)ndJ19$V?SM~tJ(6vV z)@n6B;uNAYG%Lq*o+K`#rO+nKaSV2~2)aH*YdI&mY~AeX80b)pG<3Eim<}CZsX!%T z<;k^)pzzY}y}xCE2h)vh6X3+apz0UQx+Hy~QNMiJyL}7p)Ol~CQ%zs#yED8^HJ%kT zDGDg%ojRc;67LneX|hEILR#Fi*lfNj#|FA#=BhOw@@%LV`~+v+7MiiY*Zgf$AtiuD zruY{8%~XjuX7HE(+5|PWGDG778ofj*2-?Hlj3$w?kjgA4G9ORv(vg^U!qH(Zfq&MNbuZ)U$!@R(pg7rJ_Ji+!av`- zzvX7(hFaMKfX3e}<}VaBxN2RT%1;SjtD{XTYcX$WUgwGs9kp7Qrc*Ob;c+^@(-{pp zx#Xvt1G;fk3|Rm7mcMhz*K}sjl^i#`wskrm`3W8{;FMj-D`k z=HQkVCA)PHy=>XCpxD@0>6G_}J##qxjqkt8?^D5J)@b)+8K#fF!T?me)zS3w*_V(Y zZVOn&8$%V+JZHSFv1(rv*bt$zrzQX15em-bm&z3EP+O+VBuy_Z>}W1F+wPyHs^C0+ zUP^`A`@y3W_R~WGW1->^&0oqmsWqmYxS6Cke|SerOVeK!&sdbnh6;Y#ry#BWx55UmfEVAI zj*Rs}t#~D5UWS@Rc_Z(sL>lbli9NWkRL)|tR+3i?rAoU-W|q8c#MusJ+ zj&8S>sI~%Z)-iN*ac&_j*em#ZySyHahM^#oh-pvmj{(<|ji0 zgb7^2Dfv>f`36i+2PnYwbe{dvLGHz^m3z1L2+J(SeN){d9}0u!mwSRd^OyGqL3=+H zV&CC@+9e?MR>)gcn4hmG@;+Z#>T$N$Qd20^iDY_KVhgQ`&GzQ#t~111eTV{)!uN*l zwmwu?#$2da-qr3|p>2lwk#sT^`M?t>Urr*b7V{OT>Spx9AAz-$)y+DsQTceTWMJ`6 zZ|f_r?o#0DW(BU!KzzkjZg`~pP%P2D(qT9Gm{js!?Ot@@%KtX7-W^oK3}5U#0;+}n zBQGH5etuGbu%?~cL&wXiUnb6k=>GHw|B(7Ws|4Xhs9Xj_Y9My{0aZZ;J8O%bdKXhf z6fF`C9Qob@0~=lUt5Q@qg@l%vd0`iP?@^~>v|@7TT*;`(_aTG=`p(a+y6Qrs8Yd}Y zE)A&zJ(Slq#3Y>_Xgn2`UT!%tsh=g@1k@q&x((IXfij2wI?2sX7F4Dir|%^d!RZ-o z72T__EE74FOT(8i$(o+h0}8DB8hnlNGNMhgTWs>zIgC7+3vPJ3M3Ipw_*f%F7OJ3( zyQ?EbPwS5v_H@d-Ho}t?p&nLNC4Yu;s3(q+Q~w>jOg%}TiEb^?_>#2qJCYZ;kz^Gw ztl8B4zVk$%w$aiXr>N!WaODf2RV)TC1qvZ8+CCbl1bp&7vaY379_>p|?eSGV^n3F#`xkK$Nbwj~4nYVuK0$L#37&%XA`*cR`guG)$yy?|i8)p6 ziA?3M%oOA{c%UtfzJLqKeXgYBPAK$W!4V*9-@nH;eT`cBdy9ADJ^sU3U*H$US$&$(9axo^ zzMGnbyje@UDo>e(K!IH9;$>E8*mJKlZT^}AbG&|f!0n+zdD9t{T8gNe! zgmohytcmdhPt|H=A_dlsEh0}O{FZ^U&IaD0=E?u%2>5>rZWoA)N=qK9E0qO!hxgrE6a)5{l6>bT16N-l6M4`Zw zea+|*&@&}+r(_gFW2>F|IEhQKWzb#_CkyZ{*YuqWgA_DQ+{`HTpY0pO9y4Sl*%+yL z_GD>xk15c6nyWG+BO}+RyDMdZArhsAgOYt|G5K6B_uve88&oWaHMVB&m(;+f@01T{Xq}Fy>&TB*n;h=kH=K&;#;u@y`y- zKiCmt^stL*wC7=jaO;fQw+f+PnroiIM_w#OzsLRllkW$?F9HJ^Qx)1x=E7)B7m(ia z)7Zz{);-yWlCUT+)A5DyA=H94B+CZ6ury$>cBy&5@>)c@%QXeeG;;yxQvoVFM_SY6 z|D3~(QDb^YqjbH|Ui6#KscgQXaO4Bqc&hf3Q=RpM+nhHJBM!5YpY)>XpReFJnv&bj z({3gjb~HNtYO-~<5l2oS$#jUG3vbM&7NDl zCU)}pqZ+(`ZY-KH^LG6s_Tte{KrZv93Ax@TOvP!R?;;=Y^aB^7O6Tu?)e1lGR>>!| z>%hX#=V2c9gf+JZc(uu}lY3asr5GQdfv|bEyIaa)CJxX@VIv=26Gq%CgRWrjr_AHG zYkco*JEqiD4{EslN9;pDZheSx7HFi#Fq;W`ag(&ZEe}P)dnCTY*z@hsEv1IBHeth0 z2k)-rq=O3Zj}G5X@RqngtehNVa4F8d2z!$(TU1beyPJa)#lx|;pSS5q-##vF_1U`jU zK0V_R%(M~o+iCsHUpt|3`xJ89=U5hoH35Gm`}+yd+ir{pNyF4>7Jy#qZf$KHNt$TQ zdZFOLDT7i#ZoDo&1?U>o3|%<3JFvh|*DJke&+sT5(b5NMDm90J$OHZ#rZj+AW8F}yeop<;BXwc?%)e(=cQ|M_ z1@|r#7OxhktdtMlCGpO{rjk3w0=a<9>~XZoD_}(1rn#EvWzhFa&KZo2iCy2r_Zaeru`@vp$eG6}kT~NbLcu68z%q``);o!Rk~;m2p?Rv&s%k z_0{M{LDeYzOo1=W`6-U0V!1%P#h2ci$f_{uA%t-^aq$ zDvu~sDlh@?Aw4}kuaz>FKjs>G9&xe6b~85Q_=B^KO7TraSSf#Cnr%5Zv*~x9-`XQc zBr;W5lGfpG;RPKn0%}dol59B8xE_fTM5C?Z(pAQ`|2aIL#5={h@f$SiS}l&m7)m|_ zNB>gGyG|F|hVG87h4re8-t=TdJ^ck70Q3&L{E#(JMFWD$@QP}6-b)3kAds#X_=Yu8 zMpi|mxfwPFyP?0FD^YX`7d>Nr+tIXU^)a>)NdbEVwK;1w5>6qi^6*b8D_!SvGK=uS z33A14Mf3z|$Rie|t9)+#>rbWSN-NexqchGu+JYA-4*31I!u6W~<+Pdx!wSYLVqw*8Mc1AJ*W06hWN z5r5;56k}8fe1r{etscnW4}IGbz?XGab=RYiUmC3aiou>yaEt1Okw9ar$LOF^t1>b& z`V5&_;20R5z8Vz90b8?#aUi#(x*GlL((wVjRiO%4DVwQ$HMUT`&rH#x=r?x~RB1mb zf86=t%>~MIkM@~ghs&l~r$wb6+*4qy`q`<|j1a_S%93*<)HL*wcV(a>jP87#Mw-lN z=sbd0TUkfi<$HajvXe5GUU+&*MTQ?}QF|2<5>lzXF<_z4WQm8d?CurMIH04R1liKR zm8FKe_*`zPaMNuIE(Wb4)(}Rul!(N!wdGBO z^JcWNBfF`(K*{bf6;WI_ef9W(4ZqAtmG6oYOh!5Y;qlB8msjPLe$<(S)lr}5wz&Pl zlc~YbuF^(AFQYWVkBmLtYL2~EnSsBLBA*~5(eEcmsU@ihtRXPE5Jd|fY?uDo{!h-@`-)HHb$4nI6`&Ww((IK=qSk$B24KCN5&7IC9 zpJS!d#h|=iY)e7373LUD2mJj=IM7a8&%g{WAeAqkYVLXFS4V6u85ffYLPM(v%3sZc z3SEEVp{ct--o}7(_<#FUj(7AB>lmk$OmehuL7>kei3J zh4RrVeh7B5-9b2gg2Ml+fv2RT;7&0*JBL66oOtO$Vf`rmM{r;`VU42Na-}@eP17EYZ(WOV{DZ>| zqoh;XKkH)s+tjh#%mHmhckY`axd8jQ{j0fWTD(_w0@596NOayS9ZGQ>CI?=+9lArx z{KXn?4M13S2*?c$Fh(tEw_dHE)HoG9K-bL-XBQB}!LA*7-FKWfJt6D1*#xNHJe9)Q ztL&ihw2VX_d7z)APHZQK8N6Uq60D&HoVw`jw0b|>;2!gx@Q{pb3`h zHK4m3>AXL^yfFd(7gQ@29{Qmf^k|#}m1%hcRzz0ZZqO|vX7y!pRlo8USeJO;u?Lje z^74u6>`ZQ$ayjhem9o^r|!?>r?Agj}B_r(jDzB-d--x-^GR&5WsOJS;2mcZFtYJQ^AzNtNX{_lqKqB zypEQRZ7MSoJ;m83TT6PLg^z{}jm^=lCM?IJ1IcXmu87GYlO+1{$??D=)Z-C4VEn1Z zT-fEc!{hIBrcePMa#Ubo&}oJxJMxj}O2cjkzTxu7A(Ojp@D>4jZIHxbn3(pcp`OqS zHyET`h>DmLqGm+tyw)L!%cNtTN`0RPXo%!5$2)+|KL_@1s5&r}Z=m4~ZnnbcHv^-W z$hU@mn)5`_dFxy!zh5=C@*G9afh2Sem2Z6(zmMlu1%tnJNM|S;8B!P$c-_r{C z*@x6wJ>E*KN`cG$L~+S5%6G6ZX2ImbCO89MnQS^}`fze`vcH;Sr(m!FZ7o}}2`?0h z912B%UhXRcYZg4!V4DXYJuv0rAJ3aq7Ao^WL}2OJF6brCSzhhD>DO9VQ&nAEU4XZD z1}P@En;lWk{$l0uARBO^3bc1SE(=l$#MdazTA1rYwWl%^J_s2Ys%R+P_iZWAL3y*r zu)q&zvHBxPDr896ibyyD>+A?!2sn3%01y~@P~~?DwVN23sJnP(e`6ss!`}%SAY}&3 z0GkIGD`N7~#Z#cYo{riT5!*h>h}v`@%=t3ayfq>ktCVSK+8L|Vnn#(XaRoqj&1YZ{ z%uTY6RNpvarq5-yzBNi{l^w8UnYfYheDf6<#f!tW5qyzGbhW7 zG}e2*2%}YzV~G+I~mjwj&q*AN$rb Aw*UYD diff --git a/media/php_versions.PNG b/media/php_versions.PNG index 98416fe81b028903d03f43422c9c075c7b95481d..b241f3a3f67b8be4056f933a62400e035df7dea4 100644 GIT binary patch literal 12929 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 literal 17235 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 Wpb1Zi;;$iNUocmuIH=4u% diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 2a0ea479..eedd07b1 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -520,7 +520,8 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo fetch_numeric( false ), fetch_datetime( false ), format_decimals( false ), - decimal_places( NO_CHANGE_DECIMAL_PLACES ) + decimal_places( NO_CHANGE_DECIMAL_PLACES ), + use_national_characters(CHARSET_PREFERENCE_NOT_SPECIFIED) { if( client_buffer_max_size < 0 ) { client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT; @@ -720,13 +721,6 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch driver_stmt->buffered_query_limit = driver_dbh->client_buffer_max_size; } - // if the user didn't set anything in the prepare options, then set the query timeout - // to the value set on the connection. - if(( driver_stmt->query_timeout == QUERY_TIMEOUT_INVALID ) && ( driver_dbh->query_timeout != QUERY_TIMEOUT_INVALID )) { - - core_sqlsrv_set_query_timeout( driver_stmt, driver_dbh->query_timeout TSRMLS_CC ); - } - // rewrite named parameters in the query to positional parameters if we aren't letting PDO do the // parameter substitution for us if( stmt->supports_placeholders != PDO_PLACEHOLDER_NONE ) { @@ -1111,6 +1105,27 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout } break; +#if PHP_VERSION_ID >= 70200 + case PDO_ATTR_DEFAULT_STR_PARAM: + { + if (Z_TYPE_P(val) != IS_LONG) { + THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID); + } + + zend_long value = Z_LVAL_P(val); + if (value == PDO_PARAM_STR_NATL) { + driver_dbh->use_national_characters = 1; + } + else if (value == PDO_PARAM_STR_CHAR) { + driver_dbh->use_national_characters = 0; + } + else { + THROW_PDO_ERROR(driver_dbh, PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID); + } + } + break; +#endif + // Not supported case PDO_ATTR_FETCH_TABLE_NAMES: case PDO_ATTR_FETCH_CATALOG_NAMES: @@ -1282,6 +1297,14 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout break; } +#if PHP_VERSION_ID >= 70200 + case PDO_ATTR_DEFAULT_STR_PARAM: + { + ZVAL_LONG(return_value, (driver_dbh->use_national_characters == 0) ? PDO_PARAM_STR_CHAR : PDO_PARAM_STR_NATL); + break; + } +#endif + default: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_INVALID_DBH_ATTR ); @@ -1432,14 +1455,18 @@ char * pdo_sqlsrv_dbh_last_id( _Inout_ pdo_dbh_t *dbh, _In_z_ const char *name, // Return: // 0 for failure, 1 for success. int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const char* unquoted, _In_ size_t unquoted_len, _Outptr_result_buffer_(*quoted_len) char **quoted, _Out_ size_t* quoted_len, - enum pdo_param_type /*paramtype*/ TSRMLS_DC ) + enum pdo_param_type paramtype TSRMLS_DC ) { PDO_RESET_DBH_ERROR; PDO_VALIDATE_CONN; PDO_LOG_DBH_ENTRY; SQLSRV_ENCODING encoding = SQLSRV_ENCODING_CHAR; - + bool use_national_char_set = false; + + pdo_sqlsrv_dbh* driver_dbh = static_cast(dbh->driver_data); + SQLSRV_ASSERT(driver_dbh != NULL, "pdo_sqlsrv_dbh_quote: driver_data object was NULL."); + // 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 @@ -1468,13 +1495,12 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast(stmt->driver_data); SQLSRV_ASSERT(driver_stmt != NULL, "pdo_sqlsrv_dbh_quote: driver_data object was null"); - 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(); + encoding = driver_stmt->encoding(); + if (encoding == SQLSRV_ENCODING_INVALID || encoding == SQLSRV_ENCODING_DEFAULT) { + pdo_sqlsrv_dbh* stmt_driver_dbh = reinterpret_cast(stmt->driver_data); + encoding = stmt_driver_dbh->encoding(); } + // get the placeholder at the current position in driver_stmt->placeholders ht // Normally it's not a good idea to alter the internal pointer in a hashed array // (see pull request 634 on GitHub) but in this case this is for internal use only @@ -1496,6 +1522,16 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const } } + use_national_char_set = (driver_dbh->use_national_characters == 1 || encoding == SQLSRV_ENCODING_UTF8); +#if PHP_VERSION_ID >= 70200 + if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { + use_national_char_set = true; + } + if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { + use_national_char_set = false; + } +#endif + if ( encoding == SQLSRV_ENCODING_BINARY ) { // convert from char* to hex digits using os std::basic_ostringstream os; @@ -1540,7 +1576,7 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const // count the number of quotes needed unsigned int quotes_needed = 2; // the initial start and end quotes of course // include the N proceeding the initial quote if encoding is UTF8 - if ( encoding == SQLSRV_ENCODING_UTF8 ) { + if (use_national_char_set) { quotes_needed = 3; } for ( size_t index = 0; index < unquoted_len; ++index ) { @@ -1554,7 +1590,7 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const unsigned int out_current = 0; // insert N if the encoding is UTF8 - if ( encoding == SQLSRV_ENCODING_UTF8 ) { + if (use_national_char_set) { ( *quoted )[out_current++] = 'N'; } // insert initial quote diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 71c42690..448eee04 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -580,6 +580,11 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt TSRMLS_DC ) query_len = static_cast(stmt->active_query_stringlen); } + // The query timeout setting is inherited from the corresponding connection attribute, but + // the user may have changed the query timeout setting again before this via + // PDOStatement::setAttribute() + driver_stmt->set_query_timeout(); + SQLRETURN execReturn = core_sqlsrv_execute( driver_stmt TSRMLS_CC, query, query_len ); if ( execReturn == SQL_NO_DATA ) { @@ -776,8 +781,12 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, "Invalid column number in pdo_sqlsrv_stmt_get_col_data" ); // set the encoding if the user specified one via bindColumn, otherwise use the statement's encoding - sqlsrv_php_type = driver_stmt->sql_type_to_php_type( static_cast( driver_stmt->current_meta_data[colno]->field_type ), - static_cast( driver_stmt->current_meta_data[colno]->field_size ), true ); + // save the php type for next use + sqlsrv_php_type = driver_stmt->sql_type_to_php_type( + static_cast(driver_stmt->current_meta_data[colno]->field_type), + static_cast(driver_stmt->current_meta_data[colno]->field_size), + true); + driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; // if a column is bound to a type different than the column type, figure out a way to convert it to the // type they want @@ -820,6 +829,9 @@ int pdo_sqlsrv_stmt_get_col_data( _Inout_ pdo_stmt_t *stmt, _In_ int colno, break; } } + + // save the php type for the bound column + driver_stmt->current_meta_data[colno]->sqlsrv_php_type = sqlsrv_php_type; } SQLSRV_PHPTYPE sqlsrv_phptype_out = SQLSRV_PHPTYPE_INVALID; @@ -1271,18 +1283,35 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, driver_stmt, PDO_SQLSRV_ERROR_INVALID_PARAM_DIRECTION, param->paramno + 1 ) { throw pdo::PDOException(); } + // if the parameter is output or input/output, translate the type between the PDO::PARAM_* constant + // and the SQLSRV_PHPTYPE_* constant + // vso 2829: derive the pdo_type for input/output parameter as well + // also check if the user has specified PARAM_STR_NATL or PARAM_STR_CHAR for string params + int pdo_type = param->param_type; if( param->max_value_len > 0 || param->max_value_len == SQLSRV_DEFAULT_SIZE ) { if( param->param_type & PDO_PARAM_INPUT_OUTPUT ) { direction = SQL_PARAM_INPUT_OUTPUT; + pdo_type = param->param_type & ~PDO_PARAM_INPUT_OUTPUT; } else { direction = SQL_PARAM_OUTPUT; } } + + // check if the user has specified the character set to use, take it off but ignore +#if PHP_VERSION_ID >= 70200 + if ((pdo_type & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { + pdo_type = pdo_type & ~PDO_PARAM_STR_NATL; + LOG(SEV_NOTICE, "PHP Extended String type PDO_PARAM_STR_NATL set but is ignored."); + } + if ((pdo_type & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { + pdo_type = pdo_type & ~PDO_PARAM_STR_CHAR; + LOG(SEV_NOTICE, "PHP Extended String type PDO_PARAM_STR_CHAR set but is ignored."); + } +#endif + // if the parameter is output or input/output, translate the type between the PDO::PARAM_* constant // and the SQLSRV_PHPTYPE_* constant - // vso 2829: derive the pdo_type for input/output parameter as well - int pdo_type = (direction == SQL_PARAM_OUTPUT) ? param->param_type : param->param_type & ~PDO_PARAM_INPUT_OUTPUT; SQLSRV_PHPTYPE php_out_type = SQLSRV_PHPTYPE_INVALID; switch (pdo_type) { case PDO_PARAM_BOOL: @@ -1349,13 +1378,17 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, driver_stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param->paramno + 1 ) { throw pdo::PDOException(); } + // the encoding by default is that set on the statement SQLSRV_ENCODING encoding = driver_stmt->encoding(); // if the statement's encoding is the default, then use the one on the connection if( encoding == SQLSRV_ENCODING_DEFAULT ) { encoding = driver_stmt->conn->encoding(); } - // if the user provided an encoding, use it instead + + // Beginning with PHP7.2 the user can specify whether to use PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL + // But this extended type will be ignored in real prepared statements, so the encoding deliberately + // set in the statement or driver options will still take precedence if( !Z_ISUNDEF(param->driver_params) ) { CHECK_CUSTOM_ERROR( Z_TYPE( param->driver_params ) != IS_LONG, driver_stmt, PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM ) { @@ -1378,6 +1411,7 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, break; } } + // and bind the parameter core_sqlsrv_bind_param( driver_stmt, static_cast( param->paramno ), direction, &(param->parameter) , php_out_type, encoding, sql_type, column_size, decimal_digits TSRMLS_CC ); @@ -1503,3 +1537,11 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, return sqlsrv_phptype; } +void pdo_sqlsrv_stmt::set_query_timeout() +{ + if (query_timeout == QUERY_TIMEOUT_INVALID || query_timeout < 0) { + return; + } + + core::SQLSetStmtAttr(this, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast((SQLLEN)query_timeout), SQL_IS_UINTEGER TSRMLS_CC); +} \ No newline at end of file diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 6cfb43ac..cff7add5 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -461,6 +461,10 @@ pdo_error PDO_ERRORS[] = { SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -96, true} }, + { + PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID, + { IMSSP, (SQLCHAR*) "Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.", -97, false} + }, { UINT_MAX, {} } }; diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 6a616da0..2b7269c0 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -139,8 +139,8 @@ class conn_string_parser : private string_parser int discard_trailing_white_spaces( _In_reads_(len) const char* str, _Inout_ int len ); void validate_key( _In_reads_(key_len) const char *key, _Inout_ int key_len TSRMLS_DC); - protected: - void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC); + protected: + void add_key_value_pair( _In_reads_(len) const char* value, _In_ int len TSRMLS_DC); public: conn_string_parser( _In_ sqlsrv_context& ctx, _In_ const char* dsn, _In_ int len, _In_ HashTable* conn_options_ht ); @@ -183,6 +183,7 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn { bool fetch_datetime; bool format_decimals; short decimal_places; + short use_national_characters; pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ); }; @@ -246,6 +247,7 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { fetch_datetime = db->fetch_datetime; format_decimals = db->format_decimals; decimal_places = db->decimal_places; + query_timeout = db->query_timeout; } virtual ~pdo_sqlsrv_stmt( void ); @@ -254,6 +256,9 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { // for PDO, everything is a string, so we return SQLSRV_PHPTYPE_STRING for all SQL types virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); + // driver specific way to set query timeout + virtual void set_query_timeout(); + 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 @@ -382,7 +387,8 @@ enum PDO_ERROR_CODES { PDO_SQLSRV_ERROR_EMULATE_INOUT_UNSUPPORTED, PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, PDO_SQLSRV_ERROR_CE_DIRECT_QUERY_UNSUPPORTED, - PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED + PDO_SQLSRV_ERROR_CE_EMULATE_PREPARE_UNSUPPORTED, + PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID }; extern pdo_error PDO_ERRORS[]; diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index cea5a3dd..10545e42 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -718,14 +718,14 @@ bool core_is_conn_opt_value_escaped( _Inout_ const char* value, _Inout_ size_t v const char *pch = strchr(pstr, '}'); size_t i = 0; - + while (pch != NULL && i < value_len) { i = pch - pstr + 1; - + if (i == value_len || (i < value_len && pstr[i] != '}')) { return false; } - + i++; // skip the brace pch = strchr(pch + 2, '}'); // continue searching } @@ -783,7 +783,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou try { // Since connection options access token and authentication cannot coexist, check if both of them are used. - // If access token is specified, check UID andPWD as well. + // If access token is specified, check UID andPWD as well. // No need to check the keyword Trusted_Connectionbecause it is not among the acceptable options for SQLSRV drivers if (zend_hash_index_exists(options, SQLSRV_CONN_OPTION_ACCESS_TOKEN)) { bool invalidOptions = false; @@ -801,7 +801,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou access_token_used = true; } - // Check if Authentication is ActiveDirectoryMSI + // Check if Authentication is ActiveDirectoryMSI // https://docs.microsoft.com/en-ca/azure/active-directory/managed-identities-azure-resources/overview bool activeDirectoryMSI = false; if (authentication_option_used) { @@ -813,7 +813,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou if (!stricmp(option, AzureADOptions::AZURE_AUTH_AD_MSI)) { activeDirectoryMSI = true; - // There are two types of managed identities: + // There are two types of managed identities: // (1) A system-assigned managed identity: UID must be NULL // (2) A user-assigned managed identity: UID defined but must not be an empty string // In both cases, PWD must be NULL @@ -832,11 +832,11 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou } } } - + // Add the server name common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC ); - // If uid is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, + // If uid is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, // because they are incompatible if (!access_token_used && !activeDirectoryMSI) { if (uid == NULL || strnlen_s(uid) == 0) { @@ -1153,9 +1153,12 @@ void column_encryption_set_func::func( _In_ connection_option const* option, _In convert_to_string( value ); const char* value_str = Z_STRVAL_P( value ); - // Column Encryption is disabled by default unless it is explicitly 'Enabled' + // Column Encryption is disabled by default, but if it is present and not + // explicitly set to disabled or enabled, the ODBC driver will assume the + // user is providing an attestation protocol and URL for enclave support. + // For our purposes we need only set ce_option.enabled to true if not disabled. conn->ce_option.enabled = false; - if ( !stricmp(value_str, "enabled" )) { + if ( stricmp(value_str, "disabled" )) { conn->ce_option.enabled = true; } @@ -1200,7 +1203,7 @@ void ce_akv_str_set_func::func(_In_ connection_option const* option, _In_ zval* char *pValue = static_cast(sqlsrv_malloc(value_len + 1)); memcpy_s(pValue, value_len + 1, value_str, value_len); pValue[value_len] = '\0'; // this makes sure there will be no trailing garbage - + // This will free the existing memory block before assigning the new pointer -- the user might set the value(s) more than once if (option->conn_option_key == SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID) { conn->ce_option.akv_id = pValue; @@ -1262,10 +1265,10 @@ void access_token_set_func::func( _In_ connection_option const* option, _In_ zva } const char* value_str = Z_STRVAL_P( value ); - - // The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from - // an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also - // bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the + + // The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from + // an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also + // bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the // SQL_COPT_SS_ACCESS_TOKEN connection attribute to a pointer to an ACCESSTOKEN structure // // typedef struct AccessToken @@ -1276,30 +1279,30 @@ void access_token_set_func::func( _In_ connection_option const* option, _In_ zva // // NOTE: The ODBC Driver version 13.1 only supports this authentication on Windows. // - // A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte, + // A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte, // similar to a UCS-2 string containing only ASCII characters // // See https://docs.microsoft.com/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token size_t dataSize = 2 * value_len; - - sqlsrv_malloc_auto_ptr accToken; + + sqlsrv_malloc_auto_ptr accToken; accToken = reinterpret_cast(sqlsrv_malloc(sizeof(ACCESSTOKEN) + dataSize)); ACCESSTOKEN *pAccToken = accToken.get(); SQLSRV_ASSERT(pAccToken != NULL, "Something went wrong when trying to allocate memory for the access token."); pAccToken->dataSize = dataSize; - + // Expand access token with padding bytes for (size_t i = 0, j = 0; i < dataSize; i += 2, j++) { pAccToken->data[i] = value_str[j]; pAccToken->data[i+1] = 0; } - + core::SQLSetConnectAttr(conn, SQL_COPT_SS_ACCESS_TOKEN, reinterpret_cast(pAccToken), SQL_IS_POINTER); - - // Save the pointer because SQLDriverConnect() will use it to make connection to the server + + // Save the pointer because SQLDriverConnect() will use it to make connection to the server conn->azure_ad_access_token = pAccToken; accToken.transferred(); } diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index a9e281c4..be6ead07 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -240,6 +240,9 @@ const int SQL_SQLSTATE_BUFSIZE = SQL_SQLSTATE_SIZE + 1; // default value of decimal places (no formatting required) const short NO_CHANGE_DECIMAL_PLACES = -1; +// default value for national character set strings (user did not specify any preference) +const short CHARSET_PREFERENCE_NOT_SPECIFIED = -1; + // buffer size allocated to retrieve data from a PHP stream. This number // was chosen since PHP doesn't return more than 8k at a time even if // the amount requested was more. @@ -1558,6 +1561,8 @@ struct sqlsrv_stmt : public sqlsrv_context { // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ) = 0; + // driver specific way to set query timeout + virtual void set_query_timeout() = 0; }; // *** field metadata struct *** @@ -1571,15 +1576,23 @@ struct field_meta_data { SQLSMALLINT field_scale; SQLSMALLINT field_is_nullable; bool field_is_money_type; + sqlsrv_phptype sqlsrv_php_type; field_meta_data() : field_name_len(0), field_type(0), field_size(0), field_precision(0), field_scale (0), field_is_nullable(0), field_is_money_type(false) { + reset_php_type(); } ~field_meta_data() { } + + void reset_php_type() + { + sqlsrv_php_type.typeinfo.type = SQLSRV_PHPTYPE_INVALID; + sqlsrv_php_type.typeinfo.encoding = SQLSRV_ENCODING_INVALID; + } }; // *** statement constants *** @@ -1616,7 +1629,6 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC, _In_ bool finalize_output_params = true, _In_ bool throw_on_errors = true ); void core_sqlsrv_post_param( _Inout_ sqlsrv_stmt* stmt, _In_ zend_ulong paramno, zval* param_z TSRMLS_DC ); void core_sqlsrv_set_scrollable( _Inout_ sqlsrv_stmt* stmt, _In_ unsigned long cursor_type TSRMLS_DC ); -void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout TSRMLS_DC ); void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z TSRMLS_DC ); void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC ); bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 13bb2e5e..090322a5 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -244,6 +244,12 @@ void sqlsrv_stmt::new_result_set( TSRMLS_D ) // delete sensivity data clean_up_sensitivity_metadata(); + // reset sqlsrv php type in meta data + size_t num_fields = this->current_meta_data.size(); + for (size_t f = 0; f < num_fields; f++) { + this->current_meta_data[f]->reset_php_type(); + } + // create a new result set if( cursor_type == SQLSRV_CURSOR_BUFFERED ) { sqlsrv_malloc_auto_ptr result; @@ -322,6 +328,11 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm } ZEND_HASH_FOREACH_END(); } + // The query timeout setting is inherited from the corresponding connection attribute, but + // the user may override that the query timeout setting using the statement option. + // In any case, set query timeout using the latest value + stmt->set_query_timeout(); + return_stmt = stmt; stmt.transferred(); } @@ -1116,9 +1127,6 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i sqlsrv_phptype sqlsrv_php_type = sqlsrv_php_type_in; - SQLLEN sql_field_type = 0; - SQLLEN sql_field_len = 0; - // Make sure that the statement was executed and not just prepared. CHECK_CUSTOM_ERROR( !stmt->executed, stmt, SQLSRV_ERROR_STATEMENT_NOT_EXECUTED ) { throw core::CoreException(); @@ -1127,37 +1135,47 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i // if the field is to be cached, and this field is being retrieved out of order, cache prior fields so they // may also be retrieved. if( cache_field && (field_index - stmt->last_field_index ) >= 2 ) { - sqlsrv_phptype invalid; - invalid.typeinfo.type = SQLSRV_PHPTYPE_INVALID; - for( int i = stmt->last_field_index + 1; i < field_index; ++i ) { - SQLSRV_ASSERT( reinterpret_cast( zend_hash_index_find_ptr( Z_ARRVAL( stmt->field_cache ), i )) == NULL, "Field already cached." ); - core_sqlsrv_get_field( stmt, i, invalid, prefer_string, field_value, field_len, cache_field, sqlsrv_php_type_out TSRMLS_CC ); - // delete the value returned since we only want it cached, not the actual value - if( field_value ) { - efree( field_value ); - field_value = NULL; - *field_len = 0; - } - } + sqlsrv_phptype invalid; + invalid.typeinfo.type = SQLSRV_PHPTYPE_INVALID; + for( int i = stmt->last_field_index + 1; i < field_index; ++i ) { + SQLSRV_ASSERT( reinterpret_cast( zend_hash_index_find_ptr( Z_ARRVAL( stmt->field_cache ), i )) == NULL, "Field already cached." ); + core_sqlsrv_get_field( stmt, i, invalid, prefer_string, field_value, field_len, cache_field, sqlsrv_php_type_out TSRMLS_CC ); + // delete the value returned since we only want it cached, not the actual value + if( field_value ) { + efree( field_value ); + field_value = NULL; + *field_len = 0; + } + } } // If the php type was not specified set the php type to be the default type. if (sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID) { SQLSRV_ASSERT(stmt->current_meta_data.size() > field_index, "core_sqlsrv_get_field - meta data vector not in sync" ); - sql_field_type = stmt->current_meta_data[field_index]->field_type; - if (stmt->current_meta_data[field_index]->field_precision > 0) { - sql_field_len = stmt->current_meta_data[field_index]->field_precision; + + // Get the corresponding php type from the sql type and then save the result for later + if (stmt->current_meta_data[field_index]->sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID) { + SQLLEN sql_field_type = 0; + SQLLEN sql_field_len = 0; + + sql_field_type = stmt->current_meta_data[field_index]->field_type; + if (stmt->current_meta_data[field_index]->field_precision > 0) { + sql_field_len = stmt->current_meta_data[field_index]->field_precision; + } + else { + sql_field_len = stmt->current_meta_data[field_index]->field_size; + } + sqlsrv_php_type = stmt->sql_type_to_php_type(static_cast(sql_field_type), static_cast(sql_field_len), prefer_string); + stmt->current_meta_data[field_index]->sqlsrv_php_type = sqlsrv_php_type; } else { - sql_field_len = stmt->current_meta_data[field_index]->field_size; + // use the previously saved php type + sqlsrv_php_type = stmt->current_meta_data[field_index]->sqlsrv_php_type; } - - // Get the corresponding php type from the sql type. - sqlsrv_php_type = stmt->sql_type_to_php_type(static_cast(sql_field_type), static_cast(sql_field_len), prefer_string); - } + } // Verify that we have an acceptable type to convert. - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_php_type ), stmt, SQLSRV_ERROR_INVALID_TYPE ) { + CHECK_CUSTOM_ERROR(!is_valid_sqlsrv_phptype(sqlsrv_php_type), stmt, SQLSRV_ERROR_INVALID_TYPE) { throw core::CoreException(); } @@ -1361,7 +1379,7 @@ void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLE } -// Overloaded. Extracts the long value and calls the core_sqlsrv_set_query_timeout +// Extracts the long value and calls the core_sqlsrv_set_query_timeout // which accepts timeout parameter as a long. If the zval is not of type long // than throws error. void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z TSRMLS_DC ) @@ -1375,37 +1393,8 @@ void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* val THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_QUERY_TIMEOUT_VALUE, Z_STRVAL_P( value_z ) ); } - core_sqlsrv_set_query_timeout( stmt, static_cast( Z_LVAL_P( value_z )) TSRMLS_CC ); - } - catch( core::CoreException& ) { - throw; - } -} - -// Overloaded. Accepts the timeout as a long. -void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout TSRMLS_DC ) -{ - try { - - DEBUG_SQLSRV_ASSERT( timeout >= 0 , "core_sqlsrv_set_query_timeout: The value of query timeout cannot be less than 0." ); - - // set the statement attribute - core::SQLSetStmtAttr( stmt, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast( (SQLLEN)timeout ), SQL_IS_UINTEGER TSRMLS_CC ); - - // a query timeout of 0 indicates "no timeout", which means that lock_timeout should also be set to "no timeout" which - // is represented by -1. - int lock_timeout = (( timeout == 0 ) ? -1 : timeout * 1000 /*convert to milliseconds*/ ); - - // set the LOCK_TIMEOUT on the server. - char lock_timeout_sql[32] = {'\0'}; - - int written = snprintf( lock_timeout_sql, sizeof( lock_timeout_sql ), "SET LOCK_TIMEOUT %d", lock_timeout ); - SQLSRV_ASSERT( (written != -1 && written != sizeof( lock_timeout_sql )), - "stmt_option_query_timeout: snprintf failed. Shouldn't ever fail." ); - - core::SQLExecDirect( stmt, lock_timeout_sql TSRMLS_CC ); - - stmt->query_timeout = timeout; + // Save the query timeout setting for processing later + stmt->query_timeout = static_cast(Z_LVAL_P(value_z)); } catch( core::CoreException& ) { throw; diff --git a/source/shared/version.h b/source/shared/version.h index 6bc7e796..c875410d 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -27,7 +27,7 @@ // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 #define SQLVERSION_MINOR 7 -#define SQLVERSION_PATCH 0 +#define SQLVERSION_PATCH 1 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index 3ebb179f..42148b03 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -124,6 +124,9 @@ struct ss_sqlsrv_stmt : public sqlsrv_stmt { // driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ); + // driver specific way to set query timeout + virtual void set_query_timeout(); + bool prepared; // whether the statement has been prepared yet (used for error messages) zend_ulong conn_index; // index into the connection hash that contains this statement structure zval* params_z; // hold parameters passed to sqlsrv_prepare but not used until sqlsrv_execute diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index bbb01190..b67af51a 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -267,6 +267,29 @@ sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _ return ss_phptype; } +void ss_sqlsrv_stmt::set_query_timeout() +{ + if (query_timeout == QUERY_TIMEOUT_INVALID || query_timeout < 0) { + return; + } + + // set the statement attribute + core::SQLSetStmtAttr(this, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast( (SQLLEN)query_timeout ), SQL_IS_UINTEGER TSRMLS_CC ); + + // a query timeout of 0 indicates "no timeout", which means that lock_timeout should also be set to "no timeout" which + // is represented by -1. + int lock_timeout = (( query_timeout == 0 ) ? -1 : query_timeout * 1000 /*convert to milliseconds*/ ); + + // set the LOCK_TIMEOUT on the server. + char lock_timeout_sql[32] = {'\0'}; + + int written = snprintf( lock_timeout_sql, sizeof( lock_timeout_sql ), "SET LOCK_TIMEOUT %d", lock_timeout ); + SQLSRV_ASSERT( (written != -1 && written != sizeof( lock_timeout_sql )), + "stmt_option_query_timeout: snprintf failed. Shouldn't ever fail." ); + + core::SQLExecDirect(this, lock_timeout_sql TSRMLS_CC ); +} + // statement specific parameter proccessing. Uses the generic function specialised to return a statement // resource. #define PROCESS_PARAMS( rsrc, param_spec, calling_func, param_count, ... ) \ diff --git a/test/functional/pdo_sqlsrv/AE_v2_values.inc b/test/functional/pdo_sqlsrv/AE_v2_values.inc new file mode 100644 index 00000000..721295b4 --- /dev/null +++ b/test/functional/pdo_sqlsrv/AE_v2_values.inc @@ -0,0 +1,163 @@ +5', 'fd4$_w@q^@!coe$7', 'abcd', 'ev72#x*fv=u$', '4rfg3sw', 'voi%###i<@@'); +$testValues['nchar'] = array('⽧㘎ⷅ㪋','af㋮ᶄḉㇼ៌ӗඣ','ኁ㵮ഖᅥ㪮ኸ⮊ߒᙵꇕ⯐គꉟफ़⻦ꈔꇼŞ','ꐷꬕ','㐯㩧㖃⺵㴰ڇལᧆ겴ꕕ겑וֹꔄ若㌉ᒵȅ㗉ꗅᡉ','ʭḪぅᾔᎀ㍏겶ꅫၞ㴉ᴳ㜞҂','','בּŬḛʼꃺꌖ㓵ꗛ᧽ഭწ社⾯㬄౧ຸฬ㐯ꋛ㗾'); +$testValues['varchar'] = array('gop093','*#$@@)%*$@!%','cio4*3do*$','zzz$a#l',' ','v#x%n!k&r@p$f^','6$gt?je#~','0x3dK#?'); +$testValues['nvarchar'] = array('ᾁẴ㔮㖖ୱܝ㐗㴴៸ழ᷂ᵄ葉អ㺓節','ӕᏵ൴ꔓὀ⾼','Ὡ','璉Džꖭ갪ụ⺭','Ӿϰᇬ㭡㇑ᵈᔆ⽹hᙎ՞ꦣ㧼ለͭ','Ĕ㬚㔈♠既','ꁈ ݫ','ꍆફⷌ㏽̗ૣܯ¢⽳㌭ゴᔓᅄѓⷉꘊⶮᏏᴗஈԋ≡ㄊହꂈ꓂ꑽრꖾŞ⽉걹ꩰോఫ㒧㒾㑷藍㵀ဲ更ꧥ'); +$testValues['varchar(max)'] = array('Q0H4@4E%v+ 3*Trx#>*r86-&d$VgjZ','AjEvVABur(A&Q@eG,A$3u"xAzl','z#dFd4z', + '9Dvsg9B?7oktB@|OIqy<\K^\e|*7Y&yH31E-<.hQ:)g Jl`MQV>rdOhjG;B4wQ(WR[`l(pELt0FYu._T3+8tns!}Nqrc1%n@|N|ik C@ 03a/ +H9mBq','SSs$Ie*{:D4;S]',' ','<\K^\e|*7Y&yH31E-<.hQ:','@Kg1Z6XTOgbt?CEJ|M^rkR_L4{1?l', '<=', '>=', '<>', '!<', '!>'); + +// Thresholds against which to use the comparison operators +$thresholds = array('integer' => 0, + 'bigint' => 0, + 'smallint' => 1000, + 'tinyint' => 100, + 'bit' => 0, + 'float' => 1.2, + 'real' => -1.2, + 'numeric' => 45.6789, + 'char' => 'rstuv', + 'nchar' => '㊃ᾞਲ㨴꧶ꁚꅍ', + 'varchar' => '6$gt?je#~', + 'nvarchar' => 'ӕᏵ൴ꔓὀ⾼', + 'varchar(max)' => 'hijkl', + 'nvarchar(max)' => 'xᐕᛙᘡ', + 'binary' => 0x44E4A, + 'varbinary' => 0xE4300FF, + 'varbinary(max)' => 0xD3EA762C78F, + 'date' => '2010-01-31', + 'time' => '21:45:45.4545', + 'datetime' => '3125-05-31 05:00:32.4', + 'datetime2' => '2384-12-31 12:40:12.5434323', + 'datetimeoffset' => '1984-09-25 10:40:20.0909111+03:00', + 'smalldatetime' => '1998-06-13 04:00:30', + ); + +// String patterns to test with LIKE +$patterns = array('integer' => array('8', '48', '123'), + 'bigint' => array('000','7', '65536'), + 'smallint' => array('4','768','abc'), + 'tinyint' => array('9','0','25'), + 'bit' => array('0','1','100'), + 'float' => array('14159','.','E+','2.3','308'), + 'real' => array('30','.','e-','2.3','38'), + 'numeric' => array('0','0000','12345','abc','.'), + 'char' => array('w','@','x*fv=u$','e3'), + 'nchar' => array('af㋮','㐯ꋛ㗾','ꦣ㧼ለͭ','123'), + 'varchar' => array(' ','a','#','@@)'), + 'nvarchar' => array('ӕ','Ӿϰᇬ㭡','璉Džꖭ갪ụ⺭','更ꧥ','ꈔꇼŞ'), + 'varchar(max)' => array('A','|*7Y&','4z','@!@','AjE'), + 'nvarchar(max)' => array('t','㧶ᐁቴƯɋ','ᘷ㬡',' ','ꐾɔᡧ㝚'), + 'binary' => array('0x44E4A'), + 'varbinary' => array('0xE4300FF'), + 'varbinary(max)' => array('0xD3EA762C78F'), + 'date' => array('20','%','9-','04'), + 'time' => array('4545','.0','20:','12345',':'), + 'datetime' => array('997','12',':5','9999'), + 'datetime2' => array('3125-05-31 05:','.45','$f#','-29 ','0001'), + 'datetimeoffset' => array('+02','96',' ','5092856',':00'), + 'smalldatetime' => array('00','1999','abc',':','06'), + ); +?> diff --git a/test/functional/pdo_sqlsrv/MsSetup.inc b/test/functional/pdo_sqlsrv/MsSetup.inc index 823f283c..1895f5aa 100644 --- a/test/functional/pdo_sqlsrv/MsSetup.inc +++ b/test/functional/pdo_sqlsrv/MsSetup.inc @@ -49,4 +49,6 @@ $AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPasswo $AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret $AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; ?> \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt new file mode 100644 index 00000000..02d37c20 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_emulate_prepare_natl_char.phpt @@ -0,0 +1,120 @@ +--TEST-- +GitHub issue 1018 - Test emulate prepared statements with the extended string types +--DESCRIPTION-- +This test verifies the extended string types, PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL and +PDO::PARAM_STR_CHAR will affect "emulate prepared" statements. If the parameter encoding is specified, +it also matters. The N'' prefix will be used when either it is PDO::PARAM_STR_NATL or the +parameter encoding is UTF-8. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true); + $stmt = $conn->prepare($sql, $options); + + if ($utf8) { + $stmt->bindParam(':value', $p, $pdoStrParam, 0, PDO::SQLSRV_ENCODING_UTF8); + } else { + $stmt->bindParam(':value', $p, $pdoStrParam); + } + $stmt->execute(); + + $result = $stmt->fetch(PDO::FETCH_NUM); + trace("$testCase: expected $value and returned $result[0]\n"); + if ($result[0] !== $value) { + echo("$testCase: expected $value but returned:\n"); + var_dump($result); + } +} + +try { + $conn = connect(); + + // Test case 1: PDO::PARAM_STR_NATL + $testCase = 'Test case 1: no default but specifies PDO::PARAM_STR_NATL'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase); + + // Test case 2: PDO::PARAM_STR_CHAR + $testCase = 'Test case 2: no default but specifies PDO::PARAM_STR_CHAR'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase); + + // Test case 3: no extended string types + $testCase = 'Test case 3: no default but no extended string types either'; + toEmulatePrepare($conn, PDO::PARAM_STR, $p1, $testCase); + + // Test case 4: no extended string types but specifies UTF 8 encoding + $testCase = 'Test case 4: no default but no extended string types but with UTF-8'; + toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true); + + //////////////////////////////////////////////////////////////////////// + // NEXT tests: set the default string type: PDO::PARAM_STR_CHAR first + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_CHAR); + + // Test case 5: overrides the default PDO::PARAM_STR_CHAR + $testCase = 'Test case 5: overrides the default PDO::PARAM_STR_CHAR'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase); + + // Test case 6: specifies PDO::PARAM_STR_CHAR directly + $testCase = 'Test case 6: specifies PDO::PARAM_STR_CHAR, same as the default'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase); + + // Test case 7: uses the default PDO::PARAM_STR_CHAR without specifying + $testCase = 'Test case 7: no extended string types (uses the default)'; + toEmulatePrepare($conn, PDO::PARAM_STR, $p1, $testCase); + + // Test case 8: uses the default PDO::PARAM_STR_CHAR without specifying but with UTF 8 encoding + $testCase = 'Test case 8: no extended string types (uses the default) but with UTF-8 '; + toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true); + + //////////////////////////////////////////////////////////////////////// + // NEXT tests: set the default string type: PDO::PARAM_STR_NATL + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL); + + // Test case 9: overrides the default PDO::PARAM_STR_NATL + $testCase = 'Test case 9: overrides the default PDO::PARAM_STR_NATL'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase); + + // Test case 10: specifies PDO::PARAM_STR_NATL directly + $testCase = 'Test case 10: specifies PDO::PARAM_STR_NATL, same as the default'; + toEmulatePrepare($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase); + + // Test case 11: uses the default PDO::PARAM_STR_NATL without specifying + $testCase = 'Test case 11: no extended string types (uses the default)'; + toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase); + + // Test case 12: uses the default PDO::PARAM_STR_NATL without specifying but with UTF 8 encoding + $testCase = 'Test case 12: no extended string types (uses the default) but with UTF-8'; + toEmulatePrepare($conn, PDO::PARAM_STR, $p, $testCase, true); + + echo "Done\n"; +} catch (PdoException $e) { + if (isAEConnected()) { + // The Always Encrypted feature does not support emulate prepare for binding parameters + $expected = '*Parameterized statement with attribute PDO::ATTR_EMULATE_PREPARES is not supported in a Column Encryption enabled Connection.'; + if (!fnmatch($expected, $e->getMessage())) { + echo "Unexpected exception caught when connecting with Column Encryption enabled:\n"; + echo $e->getMessage() . PHP_EOL; + } else { + echo "Done\n"; + } + } else { + echo $e->getMessage() . PHP_EOL; + } +} + +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt new file mode 100644 index 00000000..5b39f9f1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_quote_param_str_natl_char.phpt @@ -0,0 +1,93 @@ +--TEST-- +GitHub issue 1018 - Test PDO::quote() with the extended string types +--DESCRIPTION-- +This test verifies the extended string types, PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL and +PDO::PARAM_STR_CHAR will affect how PDO::quote() works. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +query('select 1'); + $error = '*An invalid attribute was designated on the PDOStatement object.'; + $pdoParam = ($isChar) ? PDO::PARAM_STR_CHAR : PDO::PARAM_STR_NATL; + + // This will cause an exception because PDO::ATTR_DEFAULT_STR_PARAM is not a statement attribute + $stmt->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, $pdoParam); + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Unexpected error returned setting PDO::ATTR_DEFAULT_STR_PARAM on statement\n"; + var_dump($e->getMessage()); + } + } +} + +function testErrorCase($attr) +{ + try { + $conn = connect(); + $error = '*Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.'; + + // This will cause an exception because PDO::ATTR_DEFAULT_STR_PARAM expects either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL only + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, $attr); + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Unexpected error returned setting PDO::ATTR_DEFAULT_STR_PARAM\n"; + var_dump($e->getMessage()); + } + } +} + +try { + testErrorCase(true); + testErrorCase('abc'); + testErrorCase(4); + + $conn = connect(); + testErrorCase2($conn, true); + testErrorCase2($conn, false); + + // Start testing quote function + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_CHAR); + + var_dump($conn->quote(null, PDO::PARAM_NULL)); + var_dump($conn->quote('\'', PDO::PARAM_STR)); + var_dump($conn->quote('foo', PDO::PARAM_STR)); + var_dump($conn->quote('foo', PDO::PARAM_STR | PDO::PARAM_STR_CHAR)); + var_dump($conn->quote('über', PDO::PARAM_STR | PDO::PARAM_STR_NATL)); + + var_dump($conn->getAttribute(PDO::ATTR_DEFAULT_STR_PARAM) === PDO::PARAM_STR_CHAR); + $conn->setAttribute(PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL); + var_dump($conn->getAttribute(PDO::ATTR_DEFAULT_STR_PARAM) === PDO::PARAM_STR_NATL); + + var_dump($conn->quote('foo', PDO::PARAM_STR | PDO::PARAM_STR_CHAR)); + var_dump($conn->quote('über', PDO::PARAM_STR)); + var_dump($conn->quote('über', PDO::PARAM_STR | PDO::PARAM_STR_NATL)); + + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +string(2) "''" +string(4) "''''" +string(5) "'foo'" +string(5) "'foo'" +string(8) "N'über'" +bool(true) +bool(true) +string(5) "'foo'" +string(8) "N'über'" +string(8) "N'über'" +Done diff --git a/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt b/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt new file mode 100644 index 00000000..9ee0dcce --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1018_real_prepare_natl_char.phpt @@ -0,0 +1,131 @@ +--TEST-- +GitHub issue 1018 - Test real prepared statements with the extended string types +--DESCRIPTION-- +This test verifies the extended string types, PDO::ATTR_DEFAULT_STR_PARAM, PDO::PARAM_STR_NATL and +PDO::PARAM_STR_CHAR will NOT affect real prepared statements. Unlike emulate prepared statements, +real prepared statements will only be affected by the parameter encoding. If not set, it will use +the statement encoding or the connection one, which is by default UTF-8. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + false); // it's false by default anyway + $stmt = $conn->prepare($sql, $options); + + // Set param encoding only if $encoding is NOT FALSE + if ($encoding !== false) { + $stmt->bindParam(':value', $p, $pdoStrParam, 0, $encoding); + $encOptions = array(PDO::SQLSRV_ATTR_ENCODING => $encoding); + } else { + $stmt->bindParam(':value', $p, $pdoStrParam); + $encOptions = array(); + } + $stmt->execute(); + + // Should also set statement encoding when $encoding is NOT FALSE + // such that data can be fetched with the right encoding + $sql = "SELECT Col1 FROM $tableName WHERE ID = $id"; + $stmt = $conn->prepare($sql, $encOptions); + $stmt->execute(); + + $result = $stmt->fetch(PDO::FETCH_NUM); + trace("$testCase: expected $value and returned $result[0]\n"); + if ($result[0] !== $value) { + echo("$testCase: expected $value but returned:\n"); + var_dump($result); + } +} + +function testUTF8encoding($conn) +{ + global $p, $tableName; + + // Create a NVARCHAR column + $sql = "CREATE TABLE $tableName (ID int identity(1,1), Col1 NVARCHAR(100))"; + $conn->query($sql); + + // The extended string types PDO::PARAM_STR_NATL and PDO::PARAM_STR_CHAR + // will be ignored in the following test cases. Only the statement or + // the connection encoding matters. + + // Test case 1: PDO::PARAM_STR_CHAR + $testCase = 'UTF-8 case 1: no default but specifies PDO::PARAM_STR_CHAR'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p, $testCase, 1); + + // Test case 2: PDO::PARAM_STR_NATL + $testCase = 'UTF-8 case 2: no default but specifies PDO::PARAM_STR_NATL'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p, $testCase, 2); + + // Test case 3: no extended string types + $testCase = 'UTF-8 case 3: no default but no extended string types either'; + insertRead($conn, PDO::PARAM_STR, $p, $testCase, 3); + + // Test case 4: no extended string types but specifies UTF-8 encoding + $testCase = 'UTF-8 case 4: no default but no extended string types but with UTF-8 encoding'; + insertRead($conn, PDO::PARAM_STR, $p, $testCase, 4, PDO::SQLSRV_ENCODING_UTF8); + + dropTable($conn, $tableName); +} + +function testNonUTF8encoding($conn) +{ + global $p, $p1, $tableName; + + // Create a VARCHAR column + $sql = "CREATE TABLE $tableName (ID int identity(1,1), Col1 VARCHAR(100))"; + $conn->query($sql); + + // The extended string types PDO::PARAM_STR_NATL and PDO::PARAM_STR_CHAR + // will be ignored in the following test cases. Only the statement or + // the connection encoding matters. + + // Test case 1: PDO::PARAM_STR_CHAR (expect $p1) + $testCase = 'System case 1: no default but specifies PDO::PARAM_STR_CHAR'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_CHAR, $p1, $testCase, 1); + + // Test case 2: PDO::PARAM_STR_NATL (expect $p1) + $testCase = 'System case 2: no default but specifies PDO::PARAM_STR_NATL'; + insertRead($conn, PDO::PARAM_STR | PDO::PARAM_STR_NATL, $p1, $testCase, 2); + + // Test case 3: no extended string types (expect $p1) + $testCase = 'System case 3: no default but no extended string types either'; + insertRead($conn, PDO::PARAM_STR, $p1, $testCase, 3); + + // Test case 4: no extended string types but specifies UTF-8 encoding (expect $p1) + $testCase = 'System case 4: no default but no extended string types but with UTF-8 encoding'; + insertRead($conn, PDO::PARAM_STR, $p1, $testCase, 4, PDO::SQLSRV_ENCODING_UTF8); + + dropTable($conn, $tableName); +} + +try { + $conn = connect(); + dropTable($conn, $tableName); + + // The connection encoding is by default PDO::SQLSRV_ENCODING_UTF8. For this test + // no change is made to the connection encoding. + testUTF8encoding($conn); + testNonUTF8encoding($conn); + + echo "Done\n"; +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_1027_query_timeout.phpt b/test/functional/pdo_sqlsrv/pdo_1027_query_timeout.phpt new file mode 100644 index 00000000..703ae761 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1027_query_timeout.phpt @@ -0,0 +1,198 @@ +--TEST-- +GitHub issue 1027 - PDO::SQLSRV_ATTR_QUERY_TIMEOUT had no effect on PDO::exec() +--DESCRIPTION-- +This test verifies that setting PDO::SQLSRV_ATTR_QUERY_TIMEOUT correctly should affect PDO::exec() as in the case for PDO::prepare() (as statement attribute or option). +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $timeout); + $sql = 'SELECT 1'; + $stmt = $conn->prepare($sql, $options); + } else { + trace("connection attribute expects error: $invalid\n"); + $conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout); + } + } catch (PDOException $e) { + if (!fnmatch($invalid, $e->getMessage())) { + echo "Unexpected error returned setting invalid $timeout for SQLSRV_ATTR_QUERY_TIMEOUT\n"; + var_dump($e->getMessage()); + } + } +} + +function testErrors($conn) +{ + testTimeoutAttribute($conn, 1.8); + testTimeoutAttribute($conn, 'xyz'); + testTimeoutAttribute($conn, -99, true); + testTimeoutAttribute($conn, 'abc', true); +} + +function checkTimeElapsed($message, $t0, $t1, $expectedDelay) +{ + $elapsed = $t1 - $t0; + $diff = abs($elapsed - $expectedDelay); + $leeway = 1.0; + $missed = ($diff > $leeway); + trace("$message $elapsed secs elapsed\n"); + + if ($missed) { + echo $message; + echo "Expected $expectedDelay but $elapsed secs elapsed\n"; + } +} + +function connectionTest($timeout, $asAttribute) +{ + global $query, $error; + $keyword = ''; + + if ($asAttribute) { + $conn = connect($keyword); + $conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout); + } else { + $options = array(PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $timeout); + $conn = connect($keyword, $options); + } + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // if timeout is 0 it means no timeout + $delay = ($timeout > 0) ? $timeout : _DELAY; + + $result = null; + $t0 = microtime(true); + + try { + $result = $conn->exec($query); + if ($timeout > 0) { + echo "connectionTest $timeout, $asAttribute: "; + echo "this should have timed out!\n"; + } + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Connection test error expected $timeout, $asAttribute:\n"; + var_dump($e->getMessage()); + } + } + + $t1 = microtime(true); + checkTimeElapsed("connectionTest ($timeout, $asAttribute): ", $t0, $t1, $delay); + + return $conn; +} + +function queryTest($conn, $timeout) +{ + global $query, $error; + + // if timeout is 0 it means no timeout + $delay = ($timeout > 0) ? $timeout : _DELAY; + + $t0 = microtime(true); + try { + $conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout); + $stmt = $conn->query($query); + + if ($timeout > 0) { + echo "Query test $timeout: should have timed out!\n"; + } + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Query test error expected $timeout:\n"; + var_dump($e->getMessage()); + } + } + + $t1 = microtime(true); + + checkTimeElapsed("Query test ($timeout): ", $t0, $t1, $delay); + + unset($stmt); +} + +function statementTest($conn, $timeout, $asAttribute) +{ + global $query, $error; + + // if timeout is 0 it means no timeout + $delay = ($timeout > 0) ? $timeout : _DELAY; + + $result = null; + $t0 = microtime(true); + + try { + if ($asAttribute) { + $stmt = $conn->prepare($query); + $stmt->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout); + } else { + $options = array(PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $timeout); + $stmt = $conn->prepare($query, $options); + } + + $result = $stmt->execute(); + + if ($timeout > 0) { + echo "statementTest $timeout: should have timed out!\n"; + } + } catch (PDOException $e) { + if (!fnmatch($error, $e->getMessage())) { + echo "Statement test error expected $timeout, $asAttribute:\n"; + var_dump($e->getMessage()); + } + } + + $t1 = microtime(true); + + checkTimeElapsed("statementTest ($timeout, $asAttribute): ", $t0, $t1, $delay); + + unset($stmt); +} + +try { + $rand = rand(1, 100); + $timeout = $rand % 3; + $asAttribute = $rand % 2; + + $conn = connectionTest($timeout, $asAttribute); + testErrors($conn); + unset($conn); + + $conn = connectionTest(0, !$asAttribute); + queryTest($conn, $timeout); + + for ($i = 0; $i < 2; $i++) { + statementTest($conn, $timeout, $i); + } + unset($conn); + + echo "Done\n"; +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt new file mode 100644 index 00000000..d41175a2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_569_query_varcharmax_ae.phpt @@ -0,0 +1,81 @@ +--TEST-- +GitHub issue #569 - direct query on varchar max fields results in function sequence error (Always Encrypted) +--DESCRIPTION-- +This is similar to pdo_569_query_varcharmax.phpt but is not limited to testing the Always Encrypted feature in Windows only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdoTestTable_569_ae'; + createTable($conn, $tableName, array(new ColumnMeta("int", "id", "IDENTITY"), "c1" => "nvarchar(max)")); + + $input = array(); + + $input[0] = 'some very large string'; + $input[1] = '1234567890.1234'; + $input[2] = 'über über'; + + $numRows = 3; + $tsql = "INSERT INTO $tableName (c1) VALUES (?)"; + + $stmt = $conn->prepare($tsql); + for ($i = 0; $i < $numRows; $i++) { + $stmt->bindParam(1, $input[$i]); + $stmt->execute(); + } + + $tsql = "SELECT id, c1 FROM $tableName ORDER BY id"; + $stmt = $conn->prepare($tsql); + $stmt->execute(); + + // Fetch one row each time with different pdo type and/or encoding + $result = $stmt->fetch(PDO::FETCH_NUM); + if ($result[1] !== $input[0]) { + echo "Expected $input[0] but got: "; + var_dump($result[0]); + } + + $stmt->bindColumn(2, $value, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_SYSTEM); + $result = $stmt->fetch(PDO::FETCH_BOUND); + if (!$result || $value !== $input[1]) { + echo "Expected $input[1] but got: "; + var_dump($result); + } + + $stmt->bindColumn(2, $value, PDO::PARAM_STR); + $result = $stmt->fetch(PDO::FETCH_BOUND); + if (!$result || $value !== $input[2]) { + echo "Expected $input[2] but got: "; + var_dump($value); + } + + // Fetch again but all at once + $stmt->execute(); + $rows = $stmt->fetchall(PDO::FETCH_ASSOC); + for ($i = 0; $i < $numRows; $i++) { + $i = $rows[$i]['id'] - 1; + if ($rows[$i]['c1'] !== $input[$i]) { + echo "Expected $input[$i] but got: "; + var_dump($rows[$i]['c1']); + } + } + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + echo $e->getMessage(); +} + +echo "Done\n"; + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_AE_functions.inc b/test/functional/pdo_sqlsrv/pdo_AE_functions.inc new file mode 100644 index 00000000..393554d9 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_AE_functions.inc @@ -0,0 +1,488 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = $conn->query($query); + $info = $stmt->fetch(); + if ($info['value'] != 1 or $info['value_in_use'] != 1) { + die("Error: enclave computations are not enabled on the server!"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->exec("DBCC FREEPROCCACHE"); + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + foreach ($dataTypes as $type) { + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE. This query can be used to both encrypt plaintext +// columns and to re-encrypt encrypted columns. +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + foreach ($dataTypes as $dataType) { + + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues) +{ + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + foreach ($dataTypes as $type) { + $insertValues[] = $testValues[$type][$v]; + $insertValues[] = $testValues[$type][$v]; + } + + // Insert the data using PDO::prepare() + try { + $stmt = $conn->prepare($insertQuery); + $stmt->execute($insertValues); + } catch (PDOException $error) { + print_r($error); + die("Inserting values in encrypted table failed\n"); + } + } +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $comparison='', $type='') +{ + try { + $nonAEstmt->execute(); + } catch(Exception $error) { + print_r($error); + die("Executing non-AE statement failed!\n"); + } + + try { + $AEstmt->execute(); + } catch(Exception $error) { + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't!"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + $e = $error->errorInfo; + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't!"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Comparison failed for correct attestation when it shouldn't have!\n"); + } + } else { + print_r($error); + die("Unexpected error occurred in compareResults!\n"); + } + + return; + } + + $AEres = $AEstmt->fetchAll(PDO::FETCH_NUM); + $nonAEres = $nonAEstmt->fetchAll(PDO::FETCH_NUM); + $AEcount = count($AEres); + $nonAEcount = count($nonAEres); + + if ($type == 'char' or $type == 'nchar') { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + return; + } elseif ($AEcount > $nonAEcount) { + print_r("Too many AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } elseif ($AEcount < $nonAEcount) { + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } else { + if ($AEcount != 0) { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr[0] != $nonAEres[$i][0]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i][0]." and non-AE result ".$nonAEres[$i][0]."\n"); + } + ++$i; + } + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, $attestation) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the Latin1_General_BIN2 + // collation. If the COLLATE clause is left out, we get different + // results between the encrypted and non-encrypted columns (probably + // because the collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $unicode = dataTypeIsUnicode($type); + $PDOType = getPDOType($type); + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + try { + $AEstmt = $conn->prepare($AEQuery); + $AEstmt->bindParam(1, $thresholds[$type], $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $thresholds[$type], $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $comparison, $type); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Patterns to match against, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types + // without an explicit cast + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + + $patternArray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternArray as $spattern) { + + // Unicode operations with AE require the Latin1_General_BIN2 + // collation. If the COLLATE clause is left out, we get different + // results between the encrypted and non-encrypted columns (probably + // because the collation was only changed in the encryption query). + $unicode = dataTypeIsUnicode($type); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $PDOType = getPDOType($type); + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + try { + $AEstmt = $conn->prepare($AEQuery); + $AEstmt->bindParam(1, $spattern, $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $spattern, $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $pattern, $type); + } + } + } +} + +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0] and $code[1]==$errors[1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function getPDOType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + return PDO::PARAM_INT; + case "bit": + return PDO::PARAM_BOOL; + case "real": + case "float": + case "double": + case "numeric": + case "time": + case "date": + case "datetime2": + case "datetime": + case "datetimeoffset": + case "smalldatetime": + case "money": + case "smallmoney"; + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return PDO::PARAM_STR; + case "binary": + case "varbinary": + case "varbinary(max)": + return PDO::PARAM_LOB; + default: + die("Case is missing for $type type in getPDOType.\n"); + } +} + +?> diff --git a/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt b/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt index cc49693b..2e22a203 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt @@ -48,6 +48,26 @@ $dataTypes = array("char(".SHORT_STRSIZE.")", "varchar(".SHORT_STRSIZE.")", "nva $tableName = "akv_comparison_table"; +// First determine if the server is AE v2 enabled +$isEnclaveEnabled = false; +$connectionOptions = "sqlsrv:Server=$server;Database=$databaseName"; + +$conn = new PDO($connectionOptions, $uid, $pwd); +if (!$conn) { + fatalError("Initial connection failed\n"); +} else { + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = $conn->query($query); + $info = $stmt->fetch(); + if ($info['value'] == 1 and $info['value_in_use'] == 1) { + $isEnclaveEnabled = true; + } + + $conn->query("DBCC FREEPROCCACHE"); +} + +unset($conn); + // Test every combination of the keywords above. // Leave out good credentials to ensure that caching does not influence the // results. The cache timeout can only be changed with SQLSetConnectAttr, so @@ -117,8 +137,11 @@ for ($i = 0; $i < sizeof($columnEncryption); ++$i) { unset($stmt); } else { // The INSERT query succeeded with bad credentials, which - // should only happen when encryption is not enabled. - if (isColEncrypted()) { + // should only happen when 1. encryption is not enabled or + // 2. when ColumnEncryption is set to something other than + // enabled or disabled (i.e. $i == 2), and the server is + // not enclave-enabled + if (!(!isColEncrypted() or ($i == 2 and !$isEnclaveEnabled))) { fatalError("Successful insertion with bad credentials\n"); } } @@ -135,6 +158,7 @@ for ($i = 0; $i < sizeof($columnEncryption); ++$i) { $errors, array('CE258', '0'), array('CE275', '0'), + array('CE400', '0'), array('IMSSP', '-85'), array('IMSSP', '-86'), array('IMSSP', '-87'), @@ -147,6 +171,7 @@ for ($i = 0; $i < sizeof($columnEncryption); ++$i) { $errors, array('CE258', '0'), array('CE275', '0'), + array('CE400', '0'), array('IMSSP', '-85'), array('IMSSP', '-86'), array('IMSSP', '-87'), diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt new file mode 100644 index 00000000..2b603fe1 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt @@ -0,0 +1,93 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to 'enabled', which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with ColumnEncryption set to 'enabled'. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + unset($conn); + + // Reconnect with ColumnEncryption set to 'enabled' + $newAttestation = 'enabled'; + $conn = connect($server, $newAttestation); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'enabled'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'enabled'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $dataTypes, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + + // Query should fail and trigger catch block before getting here + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } catch (PDOException $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt new file mode 100644 index 00000000..59d545d3 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt @@ -0,0 +1,136 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating a plaintext table +each time, then trying to encrypt it with different combinations of enclave-enabled and non-enclave keys +and encryption types. It then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) and compare the result + to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Re-encrypt the table using new key and/or encryption type. +7. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +query("DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating table failed when it shouldn't have!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + if ($count == 0) { + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitDataTypes = array_chunk($dataTypes, 5); + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $encryptionFailed = false; + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($key)) { + die("Encrypting should have failed with key $key and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have!\n"); + } + } + } + } + + if ($encryptionFailed) continue; + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // Try re-encrypting the table + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionFailed = false; + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } + } catch (Exception $error) { + if (!isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have!\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt new file mode 100644 index 00000000..c2fa226e --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt @@ -0,0 +1,60 @@ +--TEST-- +Test various settings for the ColumnEncryption keyword. +--DESCRIPTION-- +For AE v2, the Column Encryption keyword must be set to [protocol]/[attestation URL]. +If [protocol] is wrong, connection should fail; if the URL is wrong, connection +should succeed. This test sets ColumnEncryption to three values: +1. Random nonsense, which is interpreted as an incorrect protocol + so connection should fail. +2. Incorrect protocol with a correct attestation URL, connection should fail. +3. Correct protocol and incorrect URL, connection should succeed. +--SKIPIF-- + +--FILE-- +errorInfo; + checkErrors($e, array('CE400', '0')); +} + +// Test with incorrect protocol and good attestation URL. Connection should fail. +// Insert a rogue 'x' into the protocol part of the attestation. +$comma = strpos($attestation, ','); +$badProtocol = substr_replace($attestation, 'x', $comma, 0); +$options = "sqlsrv:Server=$server;database=$databaseName;ColumnEncryption=$badProtocol"; + +try { + $conn = new PDO($options, $uid, $pwd); + die("Connection should have failed!\n"); +} catch(Exception $error) { + $e = $error->errorInfo; + checkErrors($e, array('CE400', '0')); +} + +// Test with good protocol and incorrect attestation URL. Connection should succeed +// because the URL is only checked when an enclave computation is attempted. +$badURL = substr_replace($attestation, 'x', $comma+1, 0); +$options = "sqlsrv:Server=$server;database=$databaseName;ColumnEncryption=$badURL"; + +try { + $conn = new PDO($options, $uid, $pwd); +} catch(Exception $error) { + print_r($error); + die("Connecting with a bad attestation URL should have succeeded!\n"); +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt new file mode 100644 index 00000000..c962ba3e --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt @@ -0,0 +1,109 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) and compare the result + to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Re-encrypt the table using new key and/or encryption type. +6. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +query("DBCC FREEPROCCACHE"); + + // Create an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitDataTypes = array_chunk($dataTypes, 5); + $encryptionFailed = false; + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have! key = $targetKey and type = $targetType\n"); + } + + continue; + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt new file mode 100644 index 00000000..da6708f2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt @@ -0,0 +1,95 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to the wrong attestation URL, which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with a faulty attestation URL. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + unset($conn); + + // Reconnect with a faulty attestation URL + $comma = strpos($attestation, ','); + $newAttestation = substr_replace($attestation, 'x', $comma+1, 0); + + $conn = connect($server, $newAttestation); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'wrongurl'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'wrongurl'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $dataTypes, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + + // Query should fail and trigger catch block before getting here + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } catch(Exception $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc new file mode 100644 index 00000000..dd4614de --- /dev/null +++ b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc @@ -0,0 +1,36 @@ +$uid, "PWD"=>$pwd, "Driver" => $driver); + +$conn = sqlsrv_connect( $server, $connectionInfo ); +if ($conn === false) { + die( "skip Could not connect during SKIPIF." ); +} + +$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; +$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; +$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; + +if ($msodbcsql_maj < 17) { + die("skip Unsupported ODBC driver version"); +} + +if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { + die("skip Unsupported ODBC driver version"); +} + +// Get SQL Server +$server_info = sqlsrv_server_info($conn); +if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { + die("skip Server is not HGS enabled"); +} +?> diff --git a/test/functional/pdo_sqlsrv/skipif_old_php.inc b/test/functional/pdo_sqlsrv/skipif_old_php.inc new file mode 100644 index 00000000..19f97bc5 --- /dev/null +++ b/test/functional/pdo_sqlsrv/skipif_old_php.inc @@ -0,0 +1,10 @@ + diff --git a/test/functional/setup/AEV2Cert.pfx b/test/functional/setup/AEV2Cert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..4a9fc5bb8ac716acbe38b94c92b5226e410e2a0c GIT binary patch literal 2654 zcmZXUc|6qZ7QlZqeoV%aeI3R!ytHV>nmySSic%cZ_Uf`N8oT@> zeE@$VDr;Y!_G$G1nZ_1(H$7RfGw~K)+A^)La!_n7AW0}{!sKFr6z^?Aq4(5CC434- z{xYqs&-Hp(8`R(LU6vd0ebxpr*W@jvIXJAf@=>0KUi$dNgf%&gWT5^NA6oQ|8QDK_LnyU@_O!>!FZr>yh6hrYeuqg#XSJMU zH73!jn_|UJS!`&c5`*%OX{@K{zlf1wEPG8*W#V$q)afMH-*UtU_^uKB+MAA7aA_Fi zR$lWq(06DsR@g!yT}NW*1w@bS@-B7B?r^`Jw=izTyI##opHrJh(F4k9q}2mgd&1^5 zPWIrZLD@QRCoVm~1|cFlSGr9_FeZI8nq-YEZmMm(3!Qb6c{Qfue^tLK(|Ie1u`Pp2 zcfl#Cy-bf)AsK7m(x^0kFJrd4^=5W}YX&Pcr4mS$KCj}C9OS;g`GsVSqmZw+Opdsk z(tx$4$%pQ{xrr7cvpY+5#`rc=eh?y(9b{bP?kFfa8L?D5*-R%6`(gcysvzdWXNv+` z_vZQLCxm|76YtLDa;Wc#wF^oUOeMTI%ExPVxv8XV1k+BdXdAWb2&sq&K6y(3@_}85 z5D9ko+pl)b2=S-@e%l-5b0u~N$E6e&v8y|(C$0@^X*7-YKWx%k!OL3^b(DN-mb<+g z58xt#e3F7WK*Ep@)G7b{2|Rwv7(?b z*;C5a=h~<_lG3bleAL6oa$^!nK_Zdyc7b#%i}NRH?wJHL`8G zYmEbNL5udMXss51WqDa0Mft|6h{Tj`BlA%U-inAF-s5wbbA)eon zXz0{zesouUl-1%-V%cvUXA+J-S_K1Q z7GiS?R4cC?vQ*UaAgc_{+VVNYbZJ?&ZsrZ~o+qYE4Y^p2N<114`Wzu}jy8Y+ygB3? zpu^D^j-?G~0XR{VS0625*UnCIl;dtg8I|2yiupo|4;*1Ige1BId0LOmkg>gJQ=V>G~g=^#Q z_rrLKDO?{p=sgqyaMa{K5y<~9ewVcz=05WEjbDpDmcm5~U+f5%PU;v)RuXC=-R*aaCkXF<1!4yB)KBd}kSvjXFh zj7<` zKlZtx_wrv9T^Ok#pKZvZW5^4Jp9wb=$C9e|T^VW1Et2kPG3%6!A~-q%cVQ!D`{c?| zS}1J88crg3iP7<7bFqI&@lxLi39h~MJf|iufL~X^D>~sqahzmn#wZ98yk*bWWs{t` zJN7u!)}#TJpNY2lIujWPMv~rm7L>6p%4Ha;uFGWYgXkWdRm|$2i6Jp7Y-ltpqa>?I z!v47tGbdf)Uf0RXjC^9gru)GQP?F_Z`4`VraMS;sxWidb+oG7Qv&JF5l~)J&*M}qN z?VH~}eDgO{_bAlQsOohL5nEP0MOS-tm!1X#ZAJiU|993UeE?OmJW|sf&-R!r^1YYn zoF|Ip6GWbmYSEUNrKxX)u{PRFGDpmakKc=(ep`p^-thkNl_p#d&GU?lRTgh$ryiE| zd{LFdhHQJd>##H1_iI31*ZfQ^Q@cji5z>xw0|+>$eU^Tgqb zT&9@4X0=aial2lAXCWln;vh$o1i$8HWSMUUy8%uM^~r!mLM)pH#G0R7K2>$)F9F+`Ss!d E0&I$_5C8xG literal 0 HcmV?d00001 diff --git a/test/functional/setup/ae_keys.sql b/test/functional/setup/ae_keys.sql index 35c87720..d352a6f8 100644 --- a/test/functional/setup/ae_keys.sql +++ b/test/functional/setup/ae_keys.sql @@ -1,35 +1,98 @@ -/* DROP Column Encryption Key first, Column Master Key cannot be dropped until no encryption depends on it */ -IF EXISTS (SELECT * FROM sys.column_encryption_keys WHERE [name] LIKE '%AEColumnKey%') - +/* DROP Column Encryption Keys first, Column Master Keys cannot be dropped until no CEKs depend on them */ +IF EXISTS (SELECT * FROM sys.column_encryption_keys WHERE [name] LIKE '%AEColumnKey%' OR [name] LIKE '%-win-%') BEGIN DROP COLUMN ENCRYPTION KEY [AEColumnKey] +DROP COLUMN ENCRYPTION KEY [CEK-win-enclave] +DROP COLUMN ENCRYPTION KEY [CEK-win-enclave2] +DROP COLUMN ENCRYPTION KEY [CEK-win-noenclave] +DROP COLUMN ENCRYPTION KEY [CEK-win-noenclave2] END GO -/* Can finally drop Column Master Key after the Encryption Key is dropped */ -IF EXISTS (SELECT * FROM sys.column_master_keys WHERE [name] LIKE '%AEMasterKey%') - +/* Can finally drop Column Master Keys after the Column Encryption Keys are dropped */ +IF EXISTS (SELECT * FROM sys.column_master_keys WHERE [name] LIKE '%AEMasterKey%' OR [name] LIKE '%-win-%') BEGIN DROP COLUMN MASTER KEY [AEMasterKey] +DROP COLUMN MASTER KEY [CMK-win-enclave] +DROP COLUMN MASTER KEY [CMK-win-noenclave] END GO -/* Recreate the Column Master Key */ +/* Create the Column Master Keys */ +/* AKVMasterKey is a non-enclave enabled key for AE v1 testing */ +/* The enclave-enabled master key requires an ENCLAVE_COMPUTATIONS clause */ CREATE COLUMN MASTER KEY [AEMasterKey] WITH ( - KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', - KEY_PATH = N'CurrentUser/my/237F94738E7F5214D8588006C2269DBC6B370816' + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/my/237F94738E7F5214D8588006C2269DBC6B370816' ) GO -/* Create Column Encryption Key using the Column Master Key */ +/* The enclave-enabled master key requires an ENCLAVE_COMPUTATIONS clause */ +CREATE COLUMN MASTER KEY [CMK-win-enclave] +WITH +( + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/My/D9C0572FA54B221D6591C473BAEA53FE61AAC854', + ENCLAVE_COMPUTATIONS (SIGNATURE = 0xA1150DE565E9C132D2AAB8FF8B228EAA8DA804F250B5B422874CB608A3B274DDE523E71B655A3EFC6C3018B632701E9205BAD80C178614E1FE821C6807B0E70BCF11168FC4B202638905C5F016EDBADACA23C696B79772C56825F36EB8C0366B130C91D85362E560C9D2FDD20DCAE99619256045CA2725DEC9E0C115CAEB9EA686CCB0DE0D53D2056C01752B17B634FC6DBB51EA043F607349489722DB8A086CBC876649284A8352822DD22B328E7BA3D671CCDF54CDAAF61DFD6AF2EAAC14E03897324234AB103C45AB48131C1CD19040782359FC920A0AF61BA9842ADFB76C3196CBC6EB9C0A679926ED63E092B7C8643232C97A64C7F918104C210787A56F) +) +GO + +CREATE COLUMN MASTER KEY [CMK-win-noenclave] +WITH +( + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/My/D9C0572FA54B221D6591C473BAEA53FE61AAC854' +) +GO + +/* Now we can create the Column Encryption Keys */ /* ENCRYPTED_VALUE is generated by SSMS and it is always the same if the same Certificate is imported */ CREATE COLUMN ENCRYPTION KEY [AEColumnKey] WITH VALUES ( - COLUMN_MASTER_KEY = [AEMasterKey], - ALGORITHM = 'RSA_OAEP', - ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00320033003700660039003400370033003800650037006600350032003100340064003800350038003800300030003600630032003200360039006400620063003600620033003700300038003100360039DE2397A08F6313E7820D75382D8469BE1C8F3CD47E3240A5A6D6F82D322F6EB1B103C9C47999A69FFB164D37E7891F60FFDB04ADEADEB990BE88AE488CAFB8774442DF909D2EF8BB5961A5C11B85BA7903E0E453B27B49CE0A30D14FF4F412B5737850A4C564B44C744E690E78FAECF007F9005E3E0FB4F8D6C13B016A6393B84BB3F83FEED397C4E003FF8C5BBDDC1F6156349A8B40EDC26398C9A03920DD81B9197BC83A7378F79ECB430A04B4CFDF3878B0219BB629F5B5BF3C2359A7498AD9A6F5D63EF15E060CDB10A65E6BF059C7A32237F0D9E00C8AC632CCDD68230774477D4F2E411A0E4D9B351E8BAA87793E64456370D91D4420B5FD9A252F6D9178AE3DD02E1ED57B7F7008114272419F505CBCEB109715A6C4331DEEB73653990A7140D7F83089B445C59E4858809D139658DC8B2781CB27A749F1CE349DC43238E1FBEAE0155BF2DBFEF6AFD9FD2BD1D14CEF9AC125523FD1120488F24416679A6041184A2719B0FC32B6C393FF64D353A3FA9BC4FA23DFDD999B0771A547B561D72B92A0B2BB8B266BC25191F2A0E2F8D93648F8750308DCD79BE55A2F8D5FBE9285265BEA66173CD5F5F21C22CC933AE2147F46D22BFF329F6A712B3D19A6488DDEB6FDAA5B136B29ADB0BA6B6D1FD6FBA5D6A14F76491CB000FEE4769D5B268A3BF50EA3FBA713040944558EDE99D38A5828E07B05236A4475DA27915E + COLUMN_MASTER_KEY = [AEMasterKey], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00320033003700660039003400370033003800650037006600350032003100340064003800350038003800300030003600630032003200360039006400620063003600620033003700300038003100360039DE2397A08F6313E7820D75382D8469BE1C8F3CD47E3240A5A6D6F82D322F6EB1B103C9C47999A69FFB164D37E7891F60FFDB04ADEADEB990BE88AE488CAFB8774442DF909D2EF8BB5961A5C11B85BA7903E0E453B27B49CE0A30D14FF4F412B5737850A4C564B44C744E690E78FAECF007F9005E3E0FB4F8D6C13B016A6393B84BB3F83FEED397C4E003FF8C5BBDDC1F6156349A8B40EDC26398C9A03920DD81B9197BC83A7378F79ECB430A04B4CFDF3878B0219BB629F5B5BF3C2359A7498AD9A6F5D63EF15E060CDB10A65E6BF059C7A32237F0D9E00C8AC632CCDD68230774477D4F2E411A0E4D9B351E8BAA87793E64456370D91D4420B5FD9A252F6D9178AE3DD02E1ED57B7F7008114272419F505CBCEB109715A6C4331DEEB73653990A7140D7F83089B445C59E4858809D139658DC8B2781CB27A749F1CE349DC43238E1FBEAE0155BF2DBFEF6AFD9FD2BD1D14CEF9AC125523FD1120488F24416679A6041184A2719B0FC32B6C393FF64D353A3FA9BC4FA23DFDD999B0771A547B561D72B92A0B2BB8B266BC25191F2A0E2F8D93648F8750308DCD79BE55A2F8D5FBE9285265BEA66173CD5F5F21C22CC933AE2147F46D22BFF329F6A712B3D19A6488DDEB6FDAA5B136B29ADB0BA6B6D1FD6FBA5D6A14F76491CB000FEE4769D5B268A3BF50EA3FBA713040944558EDE99D38A5828E07B05236A4475DA27915E ) -GO \ No newline at end of file +GO + +/* There are two enclave enabled keys and two non-enclave enabled keys to test the case where a user + tries to reencrypt a table from one enclave enabled key to another enclave enabled key, or from a + non-enclave key to another non-enclave key */ +CREATE COLUMN ENCRYPTION KEY [CEK-win-enclave] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-enclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034007382EDDDE3FFCE076D5715B6BBBD22EA64E665899BEFAAD5B329F218EE30BE9F789EB98717B6FD9E50AE496AC9FEED962B23442D4FD3FBFEC9C9B65F40A3BCEC7CFAC198F4CAEE8A255F67988289EF050F9F75D0287F3DF9A9FDA0C674E48DF2CB13298AAAD039930DD909EEE71682CC8A90202D3F2A1F1037BB20B1954C8B6A11F05D104CA9DAF1561C6B2F9DBB08BCE17244157B751C02FC1730E387F372C31327F2834D19AF626D0B46B152615F05FA2F3566350312CDE6DE1160B3C1D0FD35FAF13891C04711DF184DA501AA51D16BF009EA71A2D28E201804C6F8F9100E90234923B2713EA7988861FBA4E292E5518FFC02CCBD2513EDA871F6E03ECDDD309619557277C10A07906E55BA3F59A6A18834B4CD5185DA4B4574A18B8B1AC53A2C36B033D7A72443F1438E76E37306A1F92AC30BC751F6D7ED1633FEE807440E1D6096C53C5E3E33828C9C59E8761E5BAD341C6D9E2BD1F2B5C3992666620CAA38C4645C154976EF62AE80161A9F7700C96875A72995E1C585918B28F65060F1B8B96417328F6DEDFCA79ED9F01EAB19FF4E3163F9963BA26E9B58031A04320CC73702A6ED438513E0F8ABA1966B53114038CC587050F90D9CD0F9E26CA9749723ABA85CF31F963A5E85E04993B2B2869725E734BE8FCFD30A801825582730B49C00A2058C02D3312D6D8E82078FF4F77C5FF9CE6E9D140F1A4517635AB784 +) +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-enclave2] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-enclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034006B4D40ABF0975AF7C5CA7D1F4345DE437318556F5A2380DCFE4AB792DC3A424EABC80EA24EE850FACD94F04809C8B32674C6FF2D966FA7F9F9E522990E5F5011515BA4B7EF3603619D8A4BF46AA9B769A8A4417462C4B0303F995F04964A2E328A503D87CD1AB85ECFCB8241D0C815540989DC33E58EDCCBAFF0753E196813E3FCCC5A3C9E4277DD528AE276F1F795973A4DF8D1BB3B1F405B5F35A6A583F0BB86BAD7FCADC1FCF6B14B602890109360FAB67D6A27DE542AE87784C40FEB9071AC34C4C40C92A6C153A4A38B6DA3AD48ED39E32D6D161ACE7EFE516B414139A831D878C13FF178649823C4EFDC8E5DB4C02F2147CC76965C01C2F3624EB809FD4F5C2E291056077B1ABEFF1F5001C1F4248704C7C70CF63DA1EBC2FEC4A3DF919BA4F6B465819BC4587599C2E7499CDE62D7C335CE7BBCFC72242A8F41C1B5C94DEB0A9AF49B723759A8CD9751EE70DDEBAFA1957382287F621790543841EBCCA0007BA030CAF29E9FBF8CEB4FEC88673F47B5EC3B5F759BBDD8ED2EAF572711D78286E4294B89FF6EBFEE4968B4596AF3B5C34985F28E886F6C211F385326F10ED62602007589FC494372902FB32B0E3D67A8C64F43A87B06EE9F2CF074EB6F3EC7A431733EDA8745051B7A4AA4C020797A9492E6A3BA643D031E491497BF17539993871085AC249D0AD82203CD442F69D6C686D26F4D17BA46B69D3CB7E395 +) +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-noenclave] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-noenclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00640039006300300035003700320066006100350034006200320032003100640036003500390031006300340037003300620061006500610035003300660065003600310061006100630038003500340042DC7A3AAAD184E01288C0913EFB6FEC6167CD8EA08A5F46ADCCC34D3AA6A1BDDDA15EA3DD219ED8795AB05C0111E48EA35A82ADDF2A206FACBBF4FD73D01C004DF627012D3950FEBCA4BBEDBDF97BA77033728D8873BA81E1C7BDCBE04BB3AA7EB42A1EDDBEF9B1CA9477ADA33F76711FEDF782CA1BD3C0104FDEB9E0D66DFCEC7D3C236906481B44F04457549658635322447742FB00B6D6F36A7CFCC56BB39F7280736BC25FD499F9CBA2F63CE11D53E536FD4A266929E06CF2BDBAF229894A77EDE140323B674ECF28C58C3E0B6C2E9407AD1A26776CB55D68B8286F64787CE5A468CFA27295D6069EFA5D65CD9A04602E861F4504F2611AAE6A8ADE33038A2BECE8BD7CF5B48567C217E324F11935C552FD25FE1FEFB152684BD1B3F8EB70EC9F6439340CE82CD8E74DD5986A6C4F9E8336ED4AC804FAD800A3EA324F78DCE37832035C3DC92782A06150916D01322A80767D1A36D7A8D9BCF6727DCE6AC67A168FA8B8B5032E60DCB178B21A860F2D98BE09DA9BA5DCCBD0D339369FF3C50C7993463372CF5B1DA9FAA12CD16E76F5961C01EADC5804C7F22227E2095BAD0F90A47B6330B1B43407E01DE5B61CEBD542A93797428AD84376E9362EADE6DDD103B9EC96E616A2ECED7D1D665B5B872E77FC024AD92AB4A8335D12D41BDD152790E87590798C1005956F9F92D4DD0C1C9852D147F7CB55B3224DE8EF593F +) +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-noenclave2] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-noenclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034009014CD16FC878CEA2DE91C8C681AE86C7C062D8BD88C4CEE501A89FEAC47356D7181644A350F72B5F6023DA2B9E26C5A2522C08B1910D390068CF26794F4BA7B0298A6676B4DC6DED913E3B077B56224D2E1A3FE4EF33F58FE44CFC3DD67E54FB15BE8E29ABAF8357F378FBEDA3EBF9868A54746074D5E0E798047867E1ABD39AD0645BB8E071C72BFC37C007CBFC58F5690A5253F444E77169B2FE92FD95897A412B2078DA3804A00723D6DF824FCA527208A1DFB377B5BA16B620213F8252E10E7D7A3719A3FBB2F7A8189792B0BCF737236963C7DDCA6366F7B04F127925A1F8DDBB1B5A01D280BD300ECA3B1F31F24C8A0D517AE7BCBC3233A24E83B70A334754098DE373A1C027A4D09BB1D26C930E7501EB02464C519D19CFA0B296238AF11638C2E0688C7599E3DB1714AACF4EBFCEF63E1EE521A8E38E3BEFD4EF4991A15E8DD5CFD94E58E68754F3E90BC117025C01562F6440417A42612BE9C8871A18108CBE3E96DA7E35C45171C03E1DFBB3CA1E35A6D322F2D5B79E2BF2A07F14136DA4A768E08E2A7F1A42E04B717CB6AE3D1A3FA0EACCFC9CEC27DB53761E13DE1F55B410A65FB441D50CF8B2153B64925B1CEBDE062B5CAF4C99C41FED6836327037C46515710F16DC611305A0EBA1943A9BA5CC6889626990879713E9C95BB54D6A8A3C1C05A10AFE142B2487A1F0A07B57841E940CC9816E3F43CAE3CB7 +) +GO diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index 58900526..ead94a30 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -31,6 +31,8 @@ def setupAE(conn_options, dbname): # import self signed certificate inst_command = "certutil -user -p '' -importPFX My PHPcert.pfx NoRoot" executeCommmand(inst_command) + inst_command = "certutil -user -p '' -importPFX My AEV2Cert.pfx NoRoot" + executeCommmand(inst_command) # create Column Master Key and Column Encryption Key script_command = 'sqlcmd -I ' + conn_options + ' -i ae_keys.sql -d ' + dbname executeCommmand(script_command) diff --git a/test/functional/sqlsrv/AE_v2_values.inc b/test/functional/sqlsrv/AE_v2_values.inc new file mode 100644 index 00000000..721295b4 --- /dev/null +++ b/test/functional/sqlsrv/AE_v2_values.inc @@ -0,0 +1,163 @@ +5', 'fd4$_w@q^@!coe$7', 'abcd', 'ev72#x*fv=u$', '4rfg3sw', 'voi%###i<@@'); +$testValues['nchar'] = array('⽧㘎ⷅ㪋','af㋮ᶄḉㇼ៌ӗඣ','ኁ㵮ഖᅥ㪮ኸ⮊ߒᙵꇕ⯐គꉟफ़⻦ꈔꇼŞ','ꐷꬕ','㐯㩧㖃⺵㴰ڇལᧆ겴ꕕ겑וֹꔄ若㌉ᒵȅ㗉ꗅᡉ','ʭḪぅᾔᎀ㍏겶ꅫၞ㴉ᴳ㜞҂','','בּŬḛʼꃺꌖ㓵ꗛ᧽ഭწ社⾯㬄౧ຸฬ㐯ꋛ㗾'); +$testValues['varchar'] = array('gop093','*#$@@)%*$@!%','cio4*3do*$','zzz$a#l',' ','v#x%n!k&r@p$f^','6$gt?je#~','0x3dK#?'); +$testValues['nvarchar'] = array('ᾁẴ㔮㖖ୱܝ㐗㴴៸ழ᷂ᵄ葉អ㺓節','ӕᏵ൴ꔓὀ⾼','Ὡ','璉Džꖭ갪ụ⺭','Ӿϰᇬ㭡㇑ᵈᔆ⽹hᙎ՞ꦣ㧼ለͭ','Ĕ㬚㔈♠既','ꁈ ݫ','ꍆફⷌ㏽̗ૣܯ¢⽳㌭ゴᔓᅄѓⷉꘊⶮᏏᴗஈԋ≡ㄊହꂈ꓂ꑽრꖾŞ⽉걹ꩰോఫ㒧㒾㑷藍㵀ဲ更ꧥ'); +$testValues['varchar(max)'] = array('Q0H4@4E%v+ 3*Trx#>*r86-&d$VgjZ','AjEvVABur(A&Q@eG,A$3u"xAzl','z#dFd4z', + '9Dvsg9B?7oktB@|OIqy<\K^\e|*7Y&yH31E-<.hQ:)g Jl`MQV>rdOhjG;B4wQ(WR[`l(pELt0FYu._T3+8tns!}Nqrc1%n@|N|ik C@ 03a/ +H9mBq','SSs$Ie*{:D4;S]',' ','<\K^\e|*7Y&yH31E-<.hQ:','@Kg1Z6XTOgbt?CEJ|M^rkR_L4{1?l', '<=', '>=', '<>', '!<', '!>'); + +// Thresholds against which to use the comparison operators +$thresholds = array('integer' => 0, + 'bigint' => 0, + 'smallint' => 1000, + 'tinyint' => 100, + 'bit' => 0, + 'float' => 1.2, + 'real' => -1.2, + 'numeric' => 45.6789, + 'char' => 'rstuv', + 'nchar' => '㊃ᾞਲ㨴꧶ꁚꅍ', + 'varchar' => '6$gt?je#~', + 'nvarchar' => 'ӕᏵ൴ꔓὀ⾼', + 'varchar(max)' => 'hijkl', + 'nvarchar(max)' => 'xᐕᛙᘡ', + 'binary' => 0x44E4A, + 'varbinary' => 0xE4300FF, + 'varbinary(max)' => 0xD3EA762C78F, + 'date' => '2010-01-31', + 'time' => '21:45:45.4545', + 'datetime' => '3125-05-31 05:00:32.4', + 'datetime2' => '2384-12-31 12:40:12.5434323', + 'datetimeoffset' => '1984-09-25 10:40:20.0909111+03:00', + 'smalldatetime' => '1998-06-13 04:00:30', + ); + +// String patterns to test with LIKE +$patterns = array('integer' => array('8', '48', '123'), + 'bigint' => array('000','7', '65536'), + 'smallint' => array('4','768','abc'), + 'tinyint' => array('9','0','25'), + 'bit' => array('0','1','100'), + 'float' => array('14159','.','E+','2.3','308'), + 'real' => array('30','.','e-','2.3','38'), + 'numeric' => array('0','0000','12345','abc','.'), + 'char' => array('w','@','x*fv=u$','e3'), + 'nchar' => array('af㋮','㐯ꋛ㗾','ꦣ㧼ለͭ','123'), + 'varchar' => array(' ','a','#','@@)'), + 'nvarchar' => array('ӕ','Ӿϰᇬ㭡','璉Džꖭ갪ụ⺭','更ꧥ','ꈔꇼŞ'), + 'varchar(max)' => array('A','|*7Y&','4z','@!@','AjE'), + 'nvarchar(max)' => array('t','㧶ᐁቴƯɋ','ᘷ㬡',' ','ꐾɔᡧ㝚'), + 'binary' => array('0x44E4A'), + 'varbinary' => array('0xE4300FF'), + 'varbinary(max)' => array('0xD3EA762C78F'), + 'date' => array('20','%','9-','04'), + 'time' => array('4545','.0','20:','12345',':'), + 'datetime' => array('997','12',':5','9999'), + 'datetime2' => array('3125-05-31 05:','.45','$f#','-29 ','0001'), + 'datetimeoffset' => array('+02','96',' ','5092856',':00'), + 'smalldatetime' => array('00','1999','abc',':','06'), + ); +?> diff --git a/test/functional/sqlsrv/MsSetup.inc b/test/functional/sqlsrv/MsSetup.inc index aec2b4bb..8335c13b 100644 --- a/test/functional/sqlsrv/MsSetup.inc +++ b/test/functional/sqlsrv/MsSetup.inc @@ -53,4 +53,6 @@ $AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPasswo $AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret $AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; ?> diff --git a/test/functional/sqlsrv/TC34_PrepAndExec.phpt b/test/functional/sqlsrv/TC34_PrepAndExec.phpt index 3e4f5672..ae9365ca 100644 --- a/test/functional/sqlsrv/TC34_PrepAndExec.phpt +++ b/test/functional/sqlsrv/TC34_PrepAndExec.phpt @@ -7,6 +7,7 @@ Validates that a prepared statement can be successfully executed more than once. PHPT_EXEC=true --SKIPIF-- diff --git a/test/functional/sqlsrv/TC55_StreamScrollable.phpt b/test/functional/sqlsrv/TC55_StreamScrollable.phpt index a59f336c..99e97345 100644 --- a/test/functional/sqlsrv/TC55_StreamScrollable.phpt +++ b/test/functional/sqlsrv/TC55_StreamScrollable.phpt @@ -6,6 +6,7 @@ Verifies the streaming behavior with scrollable resultsets. PHPT_EXEC=true --SKIPIF-- $userName, "PWD"=>$userPassword, "Driver" => $driver); + +$conn = sqlsrv_connect( $server, $connectionInfo ); +if ($conn === false) { + die( "skip Could not connect during SKIPIF." ); +} + +$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; +$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; +$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; + +if ($msodbcsql_maj < 17) { + die("skip Unsupported ODBC driver version"); +} + +if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { + die("skip Unsupported ODBC driver version"); +} + +// Get SQL Server +$server_info = sqlsrv_server_info($conn); +if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { + die("skip Server is not HGS enabled"); +} +?> diff --git a/test/functional/sqlsrv/sqlsrv_AE_functions.inc b/test/functional/sqlsrv/sqlsrv_AE_functions.inc new file mode 100644 index 00000000..f7ef4e89 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_AE_functions.inc @@ -0,0 +1,518 @@ +$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'CharacterSet'=>'UTF-8', + 'ColumnEncryption'=>$attestation_info, + ); + + if ($keystore == 'akv') { + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $security_info = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVPrincipalName, + 'KeyStoreSecret'=>$AKVPassword, + ); + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $security_info = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVClientID, + 'KeyStoreSecret'=>$AKVSecret, + ); + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options = array_merge($options, $security_info); + } + + $conn = sqlsrv_connect($server, $options); + if (!$conn) { + echo "Connection failed\n"; + print_r(sqlsrv_errors()); + } + + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = sqlsrv_query($conn, $query); + $info = sqlsrv_fetch_array($stmt); + if ($info['value'] != 1 or $info['value_in_use'] != 1) { + die("Error: enclave computations are not enabled on the server!"); + } + + // Enable rich computations + sqlsrv_query($conn, "DBCC traceon(127,-1);"); + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues) +{ + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // two copies of each value for the two columns for each data type + foreach ($dataTypes as $type) { + $insertValues[] = $testValues[$type][$v]; + $insertValues[] = $testValues[$type][$v]; + } + + // Insert the data using sqlsrv_prepare() + $stmt = sqlsrv_prepare($conn, $insertQuery, $insertValues); + if ($stmt == false) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at prepare\n"); + } + + if (sqlsrv_execute($stmt) == false) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at execute\n"); + } + } +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $comparison='', $type='') +{ + if (!sqlsrv_execute($nonAEstmt)) { + print_r(sqlsrv_errors()); + die("Executing non-AE statement failed!\n"); + } + + if(!sqlsrv_execute($AEstmt)) { + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption!\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Comparison failed for correct attestation when it shouldn't have!\n"); + } + } else { + print_r(sqlsrv_errors()); + die("Unexpected error occurred in compareResults!\n"); + } + } else { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + if ($type == 'char' or $type == 'nchar') { + return; + } + + while($AEres = sqlsrv_fetch_array($AEstmt, SQLSRV_FETCH_NUMERIC)) { + $nonAEres = sqlsrv_fetch_array($nonAEstmt, SQLSRV_FETCH_NUMERIC); + if (!$nonAEres) { + print_r($AEres); + print_r(sqlsrv_errors()); + print_r("Too many AE results for operation $comparison and data type $type!\n"); + } else { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr != $nonAEres[$i]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i]." and non-AE result ".$nonAEres[$i]."\n"); + print_r(sqlsrv_errors()); + } + ++$i; + } + } + } + + if ($rr = sqlsrv_fetch_array($nonAEstmt)) { + print_r($rr); + print_r(sqlsrv_errors()); + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Thable name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the PHPTYPE to be specified to + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $unicode = dataTypeIsUnicode($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + + $param = array(array($thresholds[$type], SQLSRV_PARAM_IN, $phptype, getSQLType($type, $length))); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $comparison, $type); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation) +{ + // TODO: Pattern matching doesn't work in AE for non-string types + // without an explicit cast + foreach ($dataTypes as $type) { + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + $patternarray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternarray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $slength = $unicode ? iconv_strlen($spattern) : strlen($spattern); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + $sqltype = $unicode ? SQLSRV_SQLTYPE_NCHAR($slength) : SQLSRV_SQLTYPE_CHAR($slength); + + $param = array(array($spattern, SQLSRV_PARAM_IN, $phptype, $sqltype)); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $pattern, $type); + } + } + } +} + +// Check that the expected errors ($codes) is found in the output of sqlsrv_errors() ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0][0] and $code[1]==$errors[0][1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function getSQLType($type, $length) +{ + switch($type) + { + case "bigint": + return SQLSRV_SQLTYPE_BIGINT; + case "integer": + return SQLSRV_SQLTYPE_INT; + case "smallint": + return SQLSRV_SQLTYPE_SMALLINT; + case "tinyint": + return SQLSRV_SQLTYPE_TINYINT; + case "bit": + return SQLSRV_SQLTYPE_BIT; + case "real": + return SQLSRV_SQLTYPE_REAL; + case "float": + case "double": + return SQLSRV_SQLTYPE_FLOAT; + case "numeric": + return SQLSRV_SQLTYPE_NUMERIC(18,0); + case "time": + return SQLSRV_SQLTYPE_TIME; + case "date": + return SQLSRV_SQLTYPE_DATE; + case "datetime": + return SQLSRV_SQLTYPE_DATETIME; + case "datetime2": + return SQLSRV_SQLTYPE_DATETIME2; + case "datetimeoffset": + return SQLSRV_SQLTYPE_DATETIMEOFFSET; + case "smalldatetime": + return SQLSRV_SQLTYPE_SMALLDATETIME; + case "money": + return SQLSRV_SQLTYPE_MONEY; + case "smallmoney": + return SQLSRV_SQLTYPE_SMALLMONEY; + case "xml": + return SQLSRV_SQLTYPE_XML; + case "uniqueidentifier": + return SQLSRV_SQLTYPE_UNIQUEIDENTIFIER; + case "char": + return SQLSRV_SQLTYPE_CHAR($length); + case "varchar": + return SQLSRV_SQLTYPE_VARCHAR($length); + case "varchar(max)": + return SQLSRV_SQLTYPE_VARCHAR('max'); + case "nchar": + return SQLSRV_SQLTYPE_NCHAR($length); + case "nvarchar": + return SQLSRV_SQLTYPE_NVARCHAR($length); + case "nvarchar(max)": + return SQLSRV_SQLTYPE_NVARCHAR('max'); + case "binary": + case "varbinary": + case "varbinary(max)": + // Using a binary type here produces a 'Restricted data type attribute violation' + return SQLSRV_SQLTYPE_BIGINT; + default: + die("Case is missing for $type type in getSQLType.\n"); + } +} + +?> diff --git a/test/functional/sqlsrv/sqlsrv_ae_azure_key_vault_keywords.phpt b/test/functional/sqlsrv/sqlsrv_ae_azure_key_vault_keywords.phpt index 3734e0be..e6f03d27 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_azure_key_vault_keywords.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_azure_key_vault_keywords.phpt @@ -47,6 +47,30 @@ $dataTypes = array("char(".SHORT_STRSIZE.")", "varchar(".SHORT_STRSIZE.")", "nva $tableName = "akv_comparison_table"; +// First determine if the server is AE v2 enabled +$isEnclaveEnabled = false; +$connectionOptions = array("CharacterSet"=>"UTF-8", + "database"=>$databaseName, + "uid"=>$uid, + "pwd"=>$pwd, + "ConnectionPooling"=>0); + +$conn = sqlsrv_connect($server, $connectionOptions); +if (!$conn) { + fatalError("Initial connection failed\n"); +} else { + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = sqlsrv_query($conn, $query); + $info = sqlsrv_fetch_array($stmt); + if ($info['value'] == 1 and $info['value_in_use'] == 1) { + $isEnclaveEnabled = true; + } + + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); +} + +unset($conn); + // Test every combination of the keywords above. // Leave out good credentials to ensure that caching does not influence the // results. The cache timeout can only be changed with SQLSetConnectAttr, so @@ -96,7 +120,8 @@ for ($i = 0; $i < sizeof($columnEncryption); ++$i) { array('IMSSP','-110'), array('IMSSP','-111'), array('IMSSP','-112'), - array('IMSSP','-113') + array('IMSSP','-113'), + array('CE400','0') ); } else { $columns = array(); @@ -148,8 +173,11 @@ for ($i = 0; $i < sizeof($columnEncryption); ++$i) { sqlsrv_free_stmt($stmt); } else { // The INSERT query succeeded with bad credentials, which - // should only happen when encryption is not enabled. - if (AE\isDataEncrypted()) { + // should only happen when 1. encryption is not enabled or + // 2. when ColumnEncryption is set to something other than + // enabled or disabled (i.e. $i == 2), and the server is + // not enclave-enabled + if (!(!AE\isDataEncrypted() or ($i == 2 and !$isEnclaveEnabled))) { fatalError("Successful insertion with bad credentials\n"); } } diff --git a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt index 59e18447..c492a961 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt @@ -15,7 +15,6 @@ function formulateSetupQuery($tableName, &$dataTypes, &$columns, &$insertQuery) { $columns = array(); $queryTypes = "("; - $queryTypesAE = "("; $valuesString = "VALUES ("; $numTypes = sizeof($dataTypes); diff --git a/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt b/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt new file mode 100644 index 00000000..186e9349 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt @@ -0,0 +1,113 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to 'enabled', which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with ColumnEncryption set to 'enabled'. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +2000 characters) + $splitDataTypes = array_chunk($dataTypes, 5); + $encryptionFailed = false; + + foreach ($splitDataTypes as $split) { + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + $encryptionFailed = true; + continue; + } + + continue; + } else { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType!\n"); + } + } + + if ($encryptionFailed) { + continue; + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt b/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt new file mode 100644 index 00000000..b8daaacf --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt @@ -0,0 +1,138 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating a plaintext table +each time, then trying to encrypt it with different combinations of enclave-enabled and non-enclave keys +and encryption types. It then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) and compare the result + to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Re-encrypt the table using new key and/or encryption type. +7. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +2000 characters) + // TODO: This is a known issue, follow up on it. + $splitDataTypes = array_chunk($dataTypes, 5); + foreach ($splitDataTypes as $split) + { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + + $stmt = sqlsrv_query($conn, $alterQuery); + $encryptionFailed = false; + + if(!$stmt) { + if (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have!\n"); + } + } else { + if (!isEnclaveEnabled($key)) { + die("Encrypting should have failed with key $key and encryption type $encryptionType\n"); + } + } + } + } + + if ($encryptionFailed) continue; + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // Try re-encrypting the table + $encryptionFailed = false; + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + + $stmt = sqlsrv_query($conn, $alterQuery); + if(!$stmt) { + if (!isEnclaveEnabled($targetKey)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have!\n"); + } + } else { + if (!isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt b/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt new file mode 100644 index 00000000..d236a2a4 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test various settings for the ColumnEncryption keyword. +--DESCRIPTION-- +For AE v2, the Column Encryption keyword must be set to [protocol]/[attestation URL]. +If [protocol] is wrong, connection should fail; if the URL is wrong, connection +should succeed. This test sets ColumnEncryption to three values: +1. Random nonsense, which is interpreted as an incorrect protocol + so connection should fail. +2. Incorrect protocol with a correct attestation URL, connection should fail. +3. Correct protocol and incorrect URL, connection should succeed. +--SKIPIF-- + +--FILE-- +$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>"xyz", + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); +} else { + die("Connecting with nonsense should have failed!\n"); +} + +// Test with incorrect protocol and good attestation URL. Connection should fail. +// Insert a rogue 'x' into the protocol part of the attestation. +$comma = strpos($attestation, ','); +$badProtocol = substr_replace($attestation, 'x', $comma, 0); +$options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>$badProtocol, + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); +} else { + die("Connecting with a bad attestation protocol should have failed!\n"); +} + +// Test with good protocol and incorrect attestation URL. Connection should succeed +// because the URL is only checked when an enclave computation is attempted. +$badURL = substr_replace($attestation, 'x', $comma+1, 0); +$options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>$badURL, + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + print_r(sqlsrv_errors()); + die("Connecting with a bad attestation URL should have succeeded!\n"); +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt b/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt new file mode 100644 index 00000000..002f61ac --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt @@ -0,0 +1,110 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) and compare the result + to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Re-encrypt the table using new key and/or encryption type. +6. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +2000 characters) + // TODO: This is a known issue, follow up on it. + $splitDataTypes = array_chunk($dataTypes, 5); + $encryptionFailed = false; + + foreach ($splitDataTypes as $split) { + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have! key = $targetKey and type = $targetType\n"); + } + + continue; + } else { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt b/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt new file mode 100644 index 00000000..a42cabaf --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt @@ -0,0 +1,93 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to the wrong attestation URL, which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $targetTypes and $targetKeys to try re-encrypting +the table with different combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with a faulty attestation URL. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/srv_1027_query_timeout.phpt b/test/functional/sqlsrv/srv_1027_query_timeout.phpt new file mode 100644 index 00000000..a22fe3a1 --- /dev/null +++ b/test/functional/sqlsrv/srv_1027_query_timeout.phpt @@ -0,0 +1,120 @@ +--TEST-- +GitHub issue 1027 - timeout option +--DESCRIPTION-- +This test is a variant of the corresponding PDO test, and it verifies that setting the query timeout option should affect sqlsrv_query or sqlsrv_prepare correctly. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $timeout); + $sql = 'SELECT 1'; + + if ($prepare) { + $stmt = sqlsrv_prepare($conn, $sql, null, $options); + } else { + $stmt = sqlsrv_query($conn, $sql, null, $options); + } + + if ($stmt !== false) { + echo "Expect this to fail with timeout option $timeout\n"; + } + if (sqlsrv_errors()[0]['message'] !== $error) { + print_r(sqlsrv_errors()); + } +} + +function testErrors($conn) +{ + testTimeout($conn, 1.8); + testTimeout($conn, 'xyz'); + testTimeout($conn, -99, true); + testTimeout($conn, 'abc', true); +} + +function checkTimeElapsed($message, $t0, $t1, $expectedDelay) +{ + $elapsed = $t1 - $t0; + $diff = abs($elapsed - $expectedDelay); + $leeway = 1.0; + $missed = ($diff > $leeway); + trace("$message $elapsed secs elapsed\n"); + + if ($missed) { + echo $message; + echo "Expected $expectedDelay but $elapsed secs elapsed\n"; + } +} + +function statementTest($conn, $timeout, $prepare) +{ + global $query, $expired; + + $options = array('QueryTimeout' => $timeout); + $stmt = null; + $result = null; + + // if timeout is 0 it means no timeout + $delay = ($timeout > 0) ? $timeout : _DELAY; + + $t0 = microtime(true); + if ($prepare) { + $stmt = sqlsrv_prepare($conn, $query, null, $options); + $result = sqlsrv_execute($stmt); + } else { + $stmt = sqlsrv_query($conn, $query, null, $options); + } + + $t1 = microtime(true); + + if ($timeout > 0) { + if ($prepare && $result !== false) { + echo "Prepared statement should fail with timeout $timeout\n"; + } elseif (!$prepare && $stmt !== false) { + echo "Query should fail with timeout $timeout\n"; + } else { + // check error messages + $errors = sqlsrv_errors(); + if (!fnmatch($expired, $errors[0]['message'])) { + echo "Unexpected error returned ($timeout, $prepare):\n"; + print_r(sqlsrv_errors()); + } + } + } + + checkTimeElapsed("statementTest ($timeout, $prepare): ", $t0, $t1, $delay); +} + +$conn = AE\connect(); + +testErrors($conn); + +$rand = rand(1, 100); +$timeout = $rand % 3; + +for ($i = 0; $i < 2; $i++) { + statementTest($conn, $timeout, $i); +} + +sqlsrv_close($conn); + +echo "Done\n"; + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt b/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt new file mode 100644 index 00000000..4aca5a59 --- /dev/null +++ b/test/functional/sqlsrv/srv_569_query_varcharmax_ae.phpt @@ -0,0 +1,96 @@ +--TEST-- +GitHub issue #569 - sqlsrv_query on varchar max fields results in function sequence error +--DESCRIPTION-- +This is similar to srv_569_query_varcharmax.phpt but is not limited to testing the Always Encrypted feature in Windows only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +$tableName = 'srvTestTable_569_ae'; + +$columns = array(new AE\ColumnMeta('int', 'id', 'identity'), + new AE\ColumnMeta('nvarchar(max)', 'c1')); +AE\createTable($conn, $tableName, $columns); + +$input = array(); + +$input[0] = 'some very large string'; +$input[1] = '1234567890.1234'; +$input[2] = 'über über'; + +$numRows = 3; +$isql = "INSERT INTO $tableName (c1) VALUES (?)"; +for ($i = 0; $i < $numRows; $i++) { + $stmt = sqlsrv_prepare($conn, $isql, array($input[$i])); + $result = sqlsrv_execute($stmt); + if (!$result) { + fatalError("Failed to insert row $i into $tableName"); + } +} + +// Select all from test table +$tsql = "SELECT id, c1 FROM $tableName ORDER BY id"; +$stmt = sqlsrv_prepare($conn, $tsql); +if (!$stmt) { + fatalError("Failed to read from $tableName"); +} +$result = sqlsrv_execute($stmt); +if (!$result) { + fatalError("Failed to select data from $tableName"); +} + +// Fetch each row as an array +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + $i = $row['id'] - 1; + if ($row['c1'] !== $input[$i]) { + echo "Expected $input[$i] but got: "; + var_dump($fieldVal); + } +} + +// Fetch again, one field each time +sqlsrv_execute($stmt); + +$i = 0; +while ($i < $numRows) { + sqlsrv_fetch($stmt); + + switch ($i) { + case 0: + $fieldVal = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + break; + case 1: + $stream = sqlsrv_get_field($stmt, 1); + while (!feof( $stream)) { + $fieldVal = fread($stream, 50); + } + break; + default: + $fieldVal = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STRING('utf-8')); + break; + } + + if ($fieldVal !== $input[$i]) { + echo 'Expected $input[$i] but got: '; + var_dump($fieldVal); + } + + $i++; +} + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/test_ae_keys_setup.phpt b/test/functional/sqlsrv/test_ae_keys_setup.phpt index 53a2c0fa..d2e3850d 100644 --- a/test/functional/sqlsrv/test_ae_keys_setup.phpt +++ b/test/functional/sqlsrv/test_ae_keys_setup.phpt @@ -1,8 +1,8 @@ --TEST-- Test the existence of Windows Always Encrypted keys generated in the database setup --DESCRIPTION-- -This test iterates through the rows of sys.column_master_keys and/or -sys.column_encryption_keys to look for the specific column master key and +This test iterates through the rows of sys.column_master_keys and/or +sys.column_encryption_keys to look for the specific column master key and column encryption key generated in the database setup --SKIPIF-- @@ -44,8 +44,8 @@ if (AE\IsQualified($conn)) { sqlsrv_free_stmt($stmt); } -echo "Test Successfully done.\n"; +echo "Test successfully done.\n"; sqlsrv_close($conn); ?> --EXPECT-- -Test Successfully done. +Test successfully done. diff --git a/test/functional/sqlsrv/test_stream_large_data.phpt b/test/functional/sqlsrv/test_stream_large_data.phpt index 909e3e69..eacf853a 100644 --- a/test/functional/sqlsrv/test_stream_large_data.phpt +++ b/test/functional/sqlsrv/test_stream_large_data.phpt @@ -2,6 +2,7 @@ streaming large amounts of data into a database and getting it out as a string exactly the same. --SKIPIF-- Date: Fri, 31 Jan 2020 14:02:45 -0800 Subject: [PATCH 6/7] 5.8.0 RTW dev to master (#1087) * Fixed the potential error reported by Prefast code analysis * Use SQLSRV_ASSERT for checking NULL ptrs * For these AKV tests check env despite not AE connected * Added the driver option to run functional tests * Fixed connection pooling tests for more than one ODBC drivers * added driver option to pdo isPooled.php * Removed win32 ifdefs re connection resiliency (#802) * Set the driver argument for getDSN to null by default (#798) * Added the driver argument to getDSN * Dropped the driver argument but set to null as default * Removed the AE condition in locale support * Modified the AE condition for locale support * Changed int to SQLLEN to avoid infinite loop (#806) * Version 5.3.0 (#803) * Version 5.3.0 * Fixed the wrong replacements * Added comments block to m4 files * Use dnl for comments * Modified AE fetch phptypes test to insert only one row at a time and loop through php types (#801) * Modified AE fetch phptypes test to insert only one row at a time and loop through php types * Fixed formatting * Streamlined two very similar large column name tests (#807) * Streamlined two very similar large column name tests * Changed the EOL * Updates to change log and readme (#811) * Updates to change log and readme * Dropped support for Ubuntu 17 * Modified as per review comments * Fixed connection resiliency tests for Unix, updated AppVeyor for ODBC 17.2 * Fixed expected output * Fixed output and skipifs * Fixed skipifs and output * Fixed driver name * Updated installation instructions and sample script (#813) * Updated instructions and sample test for 5.3.0 RTW * Fixed sample code to adhere to php coding standard * Fixed cases and spaces * Modified NOTE for UB 18.04 based on review comments * Added 'exit' * Modified change log and readme based on review to PR 811 * Applied review comments * build output to debug appveyor failure * removed debug output * Streamlined two very similar large column name tests (#815) * Streamlined two very similar large column name tests * Added random number of test table names to avoid operand clash issues * Replaced to with for based on review * Changelog updated * changelog updated, test skipif changed to run on unix platforms * Fixed skipif typo * Fixed typo in skipif for pdo * Fixed some output for Travis * Moved error checking inside pdo connres tests * Added links back to changelog * Fixed output for sqlsrv connres tests * Fixed output * Fixed output again * Fixed skipifs for connres * Tweaked per review comments * Changes made to source and tests to support PHP 7.3 (#822) * Changes made to support php 7.3 * Correct use of the smart pointer * Fixed the tests for 7.3 * Some clean up for array_init() * Fixed formattings and clean up * One more fix * Initialising strings with nulls * Removed some spaces * Made array index spacing consistent * Fix for compilation problem * Fix for compilation problem again * Before freeing stmt in destructor check if dbh driver data is NULL (#829) * Issue 434 - set dbh driver data to NULL as well in destructor * Reverted the last change but instead check if dbh driver_data is already freed * Modified the comment * Added driver to the skipif conditions (#831) * Used git clone instead to download source from a branch of a tag (#832) * Modified the error handling to make it more flexible (#833) * Made error handling more flexible * Fixed a minor issue with a test * Enabled Spectre Mitigations (#836) * Incorporated changes in PR 634 to pdo_sqlsrv (#834) * Incorporated changes in PR 634 to pdo_sqlsrv * Reverted the changes because the array is for internal use only * Modified README re user's suggestion (#841) * Modified README re user's suggestion * Moved the if condition to the end as per review * Adding supporting for Azure AD access token (#837) * Adding supporting for Azure AD access token * Added more comments for the AD access token skipif files * Save the pointer to access token struct until after connecting * Clear the access token data before freeing the memory * Added a reference as per review * Feature request - new PDO_STMT_OPTION_FETCHES_DATETIME_TYPE flag for pdo_sqlsrv to return datetime as objects (#842) * Feature request - issue 648 * Fixed constructor for field_cache and added another test * Added tests for FETCH_BOUND * Added a new test for output param * Modified output param test to set attributes differently * Removed a useless helped function in a test * Combined two new tests into one as per review * Uncommented dropTable * Feature request - add ReturnDatesAsStrings option to statement level for sqlsrv (#844) * Added ReturnDatesAsStrings option to the statement level * Added new tests for ReturnDatesAsStrings at statement level * Added more datetime types as per review * Updated version 5.4.0-preview (#846) * Updated version 5.4.0-preview * Replaced 5.3 with 5.4 * Fixed sqlsrv datetime tests to connect with ColumnEncryption variables (#849) * Change log for 5.4.0-preview (#850) * Updated change log for 5.4.0-preview * Updated 5.4.0 preview to add two new feature requests * Modified change log as per review * Modified the wordings * Updated readme, changelog, and install instructions * Clear AKV data after setting the connection attribute or when exception is thrown (#854) * Dev (#820) * Fixed the potential error reported by Prefast code analysis * Use SQLSRV_ASSERT for checking NULL ptrs * For these AKV tests check env despite not AE connected * Added the driver option to run functional tests * Fixed connection pooling tests for more than one ODBC drivers * added driver option to pdo isPooled.php * Removed win32 ifdefs re connection resiliency (#802) * Set the driver argument for getDSN to null by default (#798) * Added the driver argument to getDSN * Dropped the driver argument but set to null as default * Removed the AE condition in locale support * Modified the AE condition for locale support * Changed int to SQLLEN to avoid infinite loop (#806) * Version 5.3.0 (#803) * Version 5.3.0 * Fixed the wrong replacements * Added comments block to m4 files * Use dnl for comments * Modified AE fetch phptypes test to insert only one row at a time and loop through php types (#801) * Modified AE fetch phptypes test to insert only one row at a time and loop through php types * Fixed formatting * Streamlined two very similar large column name tests (#807) * Streamlined two very similar large column name tests * Changed the EOL * Updates to change log and readme (#811) * Updates to change log and readme * Dropped support for Ubuntu 17 * Modified as per review comments * Fixed connection resiliency tests for Unix, updated AppVeyor for ODBC 17.2 * Fixed expected output * Fixed output and skipifs * Fixed skipifs and output * Fixed driver name * Updated installation instructions and sample script (#813) * Updated instructions and sample test for 5.3.0 RTW * Fixed sample code to adhere to php coding standard * Fixed cases and spaces * Modified NOTE for UB 18.04 based on review comments * Added 'exit' * Modified change log and readme based on review to PR 811 * Applied review comments * build output to debug appveyor failure * removed debug output * Streamlined two very similar large column name tests (#815) * Streamlined two very similar large column name tests * Added random number of test table names to avoid operand clash issues * Replaced to with for based on review * Changelog updated * changelog updated, test skipif changed to run on unix platforms * Fixed skipif typo * Fixed typo in skipif for pdo * Fixed some output for Travis * Moved error checking inside pdo connres tests * Added links back to changelog * Fixed output for sqlsrv connres tests * Fixed output * Fixed output again * Clear AKV data after connection or when exception is thrown * Modified tests too to skip some AKV tests without real credentials * Used assignment operator also free the existing memory * Change readme links to https * Change readme links to https Merging this commit to dev * Save meta data for the fetched result set (#855) * Save meta data on fetched result sets * Fixed a compilation error * Optimized some more -- metadata should be available when fetching * Skip conversion for strings of numeric values, integers, floats, decimals etc * Set encoding char for numeric data * Apply review * Added Mojave to macOS instructions (#862) Added Mojave to macOS instructions * Fixed the broken links of Appveyor status badge (#863) * Feature request 415 for sqlsrv (#861) * Modified how to send stream data using SQLPutData and SQLParamData (#865) * Updated instructions to include Ubuntu 18.10 (#869) * Feature request 415 for pdo_sqlsrv (#873) * Skipped some tests when running against Azure (#874) * Modified config files to add the compiler flag, /Qspectre (#878) * Merge the commit from master re survey image link (#880) * Fixed the flaws of decimal tests and added more debugging (#879) * Changed sample code to adhere to PSR standard (#887) * Decimal places for money types only (#886) * Version update for 5.5.0-preview (#889) * Fixed the error in the pdo decimal test (#890) * Removed warning messages while compiling extensions (#892) * Improve performance of Unicode conversions (#891) * Update sqlsrv_statement_format_money_scales.phpt Do not encrypt money / smallmoney fields in the test table * Change log 5.5.0-preview (#895) * updated docs for php 7.3 * Fixed broken links * Added back Ubuntu 18.10 ODBC instruction * Drop tests related to fake passwords (#905) * Initialize output param buffer when allocating extra space (#907) * Enable compiling extensions statically into PHP (#904) * Dropped dbname variable and set QUOTED_IDENTIFIER to ON (#911) * Skipped the non-applicables tests against Azure Data Warehouse (#913) * Support for Managed Identity for Azure resources (#875) * Changed version 5.6.0 (#918) * Initialize hasLoss before passing into Convert function (#919) * Added new tests for setting client buffer size related to issue 228 (#920) * Fixed load order issue in sqlsrv * Added source indexing for symbols (#922) * Modified linux and mac instructions for 5.6.0 RTW (#926) * Change log 5.6.0 (#921) * add Language option on connect * Updated AppVeyor to download ODBC driver 17.3 (#941) * Issue 937 - fixed ASSERT and added new tests (#940) * Changed travis to pull mcr.microsoft.com/mssql/server:2017-latest instead (#943) * Modified money tests to test the accuracies of floats (#944) * Fixed the returned values for PDOStatement::getColumnMeta (#946) * Onboarding to Azure Pipelines (#949) * Fixed the error in Issue 570 (#952) * Added a new status badge on readme (#953) * Added new tests for issue 569 (#951) * Fix issue 955 - errors building sqlsrv alone (#956) * Modified test_largeData for Linux CI (#954) * Issue 937 - fixed ASSERT and added new tests (#940) (cherry picked from commit 12d01c918966f5ff908adb25d5586944949b88be) * Fixed the returned values for PDOStatement::getColumnMeta (#946) (cherry picked from commit 7309fb90b18cd93940f28c6928f152404accb675) * Fix issue 955 - errors building sqlsrv alone (#956) (cherry picked from commit 15f61bd0b4e095978cddb2de9671f3d324fc17c3) * 5.6.1 hotfix * Updated change log * Tests modified for language option for SQL Azure (#963) * Update azure-pipelines.yml for Azure Pipelines [skip ci] (#964) * Added more checks for error conditions (#965) * Removed forward cursor condition * Added row and column count checks * Revert "Update azure-pipelines.yml for Azure Pipelines [skip ci] (#964)" (#969) This reverts commit 7d389e0cffa6840a95ea720673380f9ae0b2b477. * Add new pdo_sqlsrv tests for utf8 encoding errors (#966) * Modified to check if qualified for AE connections (#967) * Fixed test and error message * Minor fixes * Test fixes * Addressed review comments * Fixed test failure * Made Azure AD tests more robust (#973) * Addressed review comments * Issue 970: use quotes for variables (#971) * Added batch query test * Fixed 32 bit test failure * Addressed review comments * Formatting changes * Used different skipif conditions for these two tests that require AE connections (#977) * Simplified insert logic * Modified get column meta method to reference saved metadata (#978) * Revert "Used different skipif conditions for these two tests that require AE connections (#977)" (#980) This reverts commit ee3c85afa863eaff81512ad6399de572ffb943d9. * Fixed failing tests (#981) * Data Classification sensitivity metadata retrieval (#979) * Added more pdo tests to verify different error conditions (#984) * Fixed memory issues with data classification (#985) * Added connection string flag * Removed unix skipif * Fixed test output * Fixed pdo test * Changed flag name * Fixed test output * Updated links and versions (#987) (#988) * Fixed test output (again) * Fixed test output (again) * Fixed test output (again) * Replaced expected test output altogether * Fixed locale issue * Corrected formatting * Replaced EXPECTF with EXPECT * Fixed two failing tests (#991) * Redesigned some tests based on recent test results (#992) * Modified pipelines to connect using sqlcmd inside of the container instead (#995) * Added batch query * Added batch query test for pdo (#997) * Added a new test and modify a non LOB sqlsrv test (#1000) * Two index zval functions are macros in php 7.4 (#1001) * Replaced uint with size_t (#1004) * Check compiler version for php 74 (#1005) * Fixed tests that failed in php 7.4 (#1006) * Improve data caching with datetime objects (#1008) * Fixed for issues found by Semmle (#1011) * Removed unneeded constants * Fixed sqlsrv_free_stmt argument info * Fixed brace escape to avoid buffer overflow * Fixed brace escape and added test * Debugging test failure on Bamboo * Removed debugging output * Debugging test failure on Bamboo * Removed debugging output * Added more test cases * Changed range check to use strchr * Added pdo test * Fixed test and formatting * Addressed various issues with PHP 7.4 beta1 (#1015) * Updated dockerfile to use UB 18.04 and PHP 73 (#1016) * Added survey results (#1017) * Updated ODBC driver 17.4 (#1019) * Modified output.py to take a new argument and travis yml to use include for coveralls (#1020) * Used constants in memory stress tests for easier configuration (#1022) * Removed KSP related scripts and files (#1030) * Updated version to 5.7.0 preview (#1029) * Change log for 5.7.0 (#1028) * Modified how drivers handle query timeout settings (#1037) * Feature request: support extended string types (#1043) * Added the required file to ansi tests (#1047) * Always Encrypted v2 support (#1045) * Change to support ae-v2 * Add support for AE V2 * Added some descriptions and comments * Fixed PDO pattern matching * Updated key generation scripts * Fixed key script * Fixed char/nchar results, fixed formatting issues * Addressed review comments * Updated key scripts * Debugging aev2 keyword failure * Debugging aev2 keyword failure * Debugging aev2 keyword failure * Debugging aev2 keyword failure * Added skipif to ae v2 keyword test * Addressed review comments * Fixed braces and camel caps * Updated test descriptions * Added detail to test descriptions * Tiny change * Modified pdo tests to work with column encryption (#1051) * Saved php types with metadata when fetching (#1049) * Updated survey charts for Nov 2019 (#1057) * Updated all CIs (#1058) * Change log 5.7.1 preview (#1060) * Fix AKV keyword test for AE v2 behaviour (#1061) * Master (#936) 5.6.0 RTW * 5.6.1 hotfix (#959) * Updated links and versions (#987) * Fixed AKV keyword tests for AE v2 * Added comment * Free proc cache before starting test * Fixed comment * Update linux mac instructions for php 7.4 (#1062) * Updated appveyor yml to build 7.3 and 7.4 (#1065) * Fixes suggested by Semmle (#1068) * Fixes suggested by Semmle * Updated azure-pipelines * Added configurable options for setting locales (#1069) #1063 * Fixed the skipif wordings and styles (#1070) * Modified locale tests to work in both linux and mac (#1074) * Include sql_variant type for buffered queries (#1080) * Updated versions and year (#1082) * Change log for version 5.8.0 (#1083) * 5.8.0 rtw docs (#1086) * updated install instructions and changelog * removed md extensions * Addressed review comments * added path * Fixed link Co-authored-by: Jenny Tam Co-authored-by: Gert de Pagter Co-authored-by: Jannes Jeising Co-authored-by: Guillaume Degoulet <34232764+gdegoulet@users.noreply.github.com> --- .travis.yml | 4 +- CHANGELOG.md | 56 ++++ Dockerfile-msphpsql | 13 +- LICENSE | 2 +- Linux-mac-install.md | 231 ++++++++++++---- README.md | 6 +- appveyor.yml | 14 +- azure-pipelines.yml | 57 ++-- source/pdo_sqlsrv/config.m4 | 2 +- source/pdo_sqlsrv/config.w32 | 2 +- source/pdo_sqlsrv/pdo_dbh.cpp | 18 +- source/pdo_sqlsrv/pdo_init.cpp | 18 +- source/pdo_sqlsrv/pdo_parser.cpp | 2 +- source/pdo_sqlsrv/pdo_stmt.cpp | 2 +- source/pdo_sqlsrv/pdo_util.cpp | 2 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 6 +- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 10 +- source/pdo_sqlsrv/template.rc | 2 +- source/shared/FormattedPrint.cpp | 2 +- source/shared/FormattedPrint.h | 2 +- source/shared/StringFunctions.cpp | 2 +- source/shared/StringFunctions.h | 2 +- source/shared/core_conn.cpp | 2 +- source/shared/core_init.cpp | 8 +- source/shared/core_results.cpp | 4 +- source/shared/core_sqlsrv.h | 258 +++++++++--------- source/shared/core_stmt.cpp | 52 ++-- source/shared/core_stream.cpp | 2 +- source/shared/core_util.cpp | 2 +- source/shared/globalization.h | 2 +- source/shared/interlockedatomic.h | 2 +- source/shared/interlockedatomic_gcc.h | 2 +- source/shared/interlockedslist.h | 2 +- source/shared/localization.hpp | 2 +- source/shared/localizationimpl.cpp | 2 +- source/shared/msodbcsql.h | 2 +- source/shared/sal_def.h | 2 +- source/shared/typedefs_for_linux.h | 2 +- source/shared/version.h | 8 +- source/shared/xplat.h | 2 +- source/shared/xplat_intsafe.h | 2 +- source/shared/xplat_winerror.h | 2 +- source/shared/xplat_winnls.h | 2 +- source/sqlsrv/config.m4 | 2 +- source/sqlsrv/config.w32 | 2 +- source/sqlsrv/conn.cpp | 2 +- source/sqlsrv/init.cpp | 21 +- source/sqlsrv/php_sqlsrv.h | 6 +- source/sqlsrv/php_sqlsrv_int.h | 11 +- source/sqlsrv/stmt.cpp | 245 ++++++++--------- source/sqlsrv/template.rc | 2 +- source/sqlsrv/util.cpp | 2 +- .../pdo_sqlsrv/MsCommon_mid-refactor.inc | 18 +- .../pdo_sqlsrv/pdo_1063_locale_configs.phpt | 76 ++++++ .../pdo_sqlsrv/pdo_1063_test_locale.php | 121 ++++++++ ...pdo_1079_sql_variant_buffered_queries.phpt | 59 ++++ .../pdo_ae_azure_key_vault_keywords.phpt | 2 +- .../pdo_fetch_variants_diff_styles.phpt | 13 +- .../pdo_simple_update_variants.phpt | 13 +- test/functional/pdo_sqlsrv/skipif.inc | 6 +- test/functional/pdo_sqlsrv/skipif_azure.inc | 12 +- .../pdo_sqlsrv/skipif_mid-refactor.inc | 34 +-- test/functional/pdo_sqlsrv/skipif_not_akv.inc | 49 ++-- test/functional/pdo_sqlsrv/skipif_unix.inc | 11 +- .../pdo_sqlsrv/skipif_unix_locales.inc | 28 ++ .../pdo_sqlsrv/skipif_versions_old.inc | 21 +- test/functional/sqlsrv/TC34_PrepAndExec.phpt | 3 - test/functional/sqlsrv/TC42_FetchField.phpt | 3 - test/functional/sqlsrv/TC43_FetchData.phpt | 7 +- test/functional/sqlsrv/TC44_FetchArray.phpt | 7 +- test/functional/sqlsrv/TC45_FetchObject.phpt | 3 - .../sqlsrv/TC46_FetchNextResult.phpt | 7 +- .../sqlsrv/TC48_FetchScrollable.phpt | 3 - test/functional/sqlsrv/TC51_StreamRead.phpt | 7 +- .../sqlsrv/TC55_StreamScrollable.phpt | 7 +- test/functional/sqlsrv/skipif_not_akv.inc | 48 ++-- test/functional/sqlsrv/skipif_unix.inc | 6 +- .../functional/sqlsrv/skipif_unix_locales.inc | 28 ++ .../functional/sqlsrv/skipif_versions_old.inc | 33 ++- .../sqlsrv_ae_azure_key_vault_keywords.phpt | 2 +- .../sqlsrv/sqlsrv_param_input_variants.phpt | 3 +- .../sqlsrv/sqlsrv_simple_fetch_variants.phpt | 2 +- .../sqlsrv/sqlsrv_simple_update_variants.phpt | 13 +- .../sqlsrv/srv_1063_locale_configs.phpt | 76 ++++++ .../sqlsrv/srv_1063_test_locale.php | 136 +++++++++ ...srv_1079_sql_variant_buffered_queries.phpt | 62 +++++ .../sqlsrv/test_stream_large_data.phpt | 7 +- 87 files changed, 1421 insertions(+), 615 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdo_1063_locale_configs.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_1063_test_locale.php create mode 100644 test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt create mode 100644 test/functional/pdo_sqlsrv/skipif_unix_locales.inc create mode 100644 test/functional/sqlsrv/skipif_unix_locales.inc create mode 100644 test/functional/sqlsrv/srv_1063_locale_configs.phpt create mode 100644 test/functional/sqlsrv/srv_1063_test_locale.php create mode 100644 test/functional/sqlsrv/srv_1079_sql_variant_buffered_queries.phpt diff --git a/.travis.yml b/.travis.yml index e368b84e..86cc31ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,10 @@ env: - TEST_PHP_SQL_PWD=Password123 before_install: - - docker pull mcr.microsoft.com/mssql/server:2019-GA-ubuntu-16.04 + - docker pull mcr.microsoft.com/mssql/server:2019-latest install: - - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-CTP3.2-ubuntu + - docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=Password123' -p 1433:1433 --name=$TEST_PHP_SQL_SERVER -d mcr.microsoft.com/mssql/server:2019-latest - docker build --build-arg PHPSQLDIR=$PHPSQLDIR -t msphpsql-dev -f Dockerfile-msphpsql . before_script: diff --git a/CHANGELOG.md b/CHANGELOG.md index c18663be..9eab1ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,62 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.8.0 - 2020-01-31 +Updated PECL release packages. Here is the list of updates: + +### Added +- Support for PHP 7.4 +- Support for [Microsoft ODBC Driver 17.5]( +https://docs.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver15) on all platforms +- Support for Debian 10 and Red Hat 8 - require MS ODBC Driver 17.4+ +- Support for macOS Catalina, Alpine Linux 3.11 (experimental), and Ubuntu 19.10 - require ODBC Driver 17.5+ +- Feature Request [#929](https://github.com/microsoft/msphpsql/issues/929) - new [Language option](https://github.com/microsoft/msphpsql/wiki/Features#language) - Pull Request [#930](https://github.com/microsoft/msphpsql/pull/930) +- [Data Classification Sensitivity Metadata Retrieval](https://github.com/microsoft/msphpsql/wiki/Features#data-classification-sensitivity-metadata) - requires ODBC Driver 17.4.2+ and [SQL Server 2019](https://www.microsoft.com/sql-server/sql-server-2019) +- Feature Request [#1018](https://github.com/microsoft/msphpsql/issues/1018) - support for [PHP extended string types](https://github.com/microsoft/msphpsql/wiki/Features#natlTypes) - Pull Request [#1043](https://github.com/microsoft/msphpsql/pull/1043) +- [Always Encrypted with secure enclaves](https://github.com/microsoft/msphpsql/wiki/Features#alwaysencryptedV2) - requires ODBC Driver 17.4+ and [SQL Server 2019](https://www.microsoft.com/sql-server/sql-server-2019) +- Feature Request [#1063](https://github.com/microsoft/msphpsql/issues/1063) - add configurable options for locale settings in Linux and macOS - Pull Request [#1069](https://github.com/microsoft/msphpsql/pull/1069) + +### Removed +- Dropped support for [PHP 7.1](https://www.php.net/supported-versions.php) +- Dropped support for SQL Server 2008 R2, macOS Sierra, Ubuntu 18.10 and Ubuntu 19.04. + +### Fixed +- Issue [#570](https://github.com/microsoft/msphpsql/issues/570) - Fixed fetching varbinary data using client buffer with sqlsrv +- Pull Request [#972](https://github.com/microsoft/msphpsql/pull/972) - Removed redundant calls to retrieve the number of columns or rows in the current query result set +- Pull Request [#978](https://github.com/microsoft/msphpsql/pull/978) - PDO_SQLSRV implementation of PDO::getColumnMeta now references cached metadata rather than making an ODBC call every time +- Pull Request [#979](https://github.com/microsoft/msphpsql/pull/979) - Added support for Data Classification Sensitivity metadata retrieval +- Pull Request [#985](https://github.com/microsoft/msphpsql/pull/985) - Fixed memory issues with Data Classification data structures +- Issue [#432](https://github.com/microsoft/msphpsql/issues/432) - Having any invalid UTF-8 name in the connection string will no longer invoke misleading error messages +- Issue [#909](https://github.com/microsoft/msphpsql/issues/909) - Fixed potential exception with locale issues in macOS +- Pull Request [#992](https://github.com/microsoft/msphpsql/pull/992) - Produced the correct error when requesting Data Classification metadata with ODBC drivers prior to 17 +- Pull Request [#1001](https://github.com/microsoft/msphpsql/pull/1001) - Fixed compilation issue with PHP 7.4 alpha +- Pull Request [#1004](https://github.com/microsoft/msphpsql/pull/1004) - Fixed another compilation issue with PHP 7.4 alpha +- Pull Request [#1008](https://github.com/microsoft/msphpsql/pull/1008) - Improved data caching when fetching datetime objects +- Pull Request [#1011](https://github.com/microsoft/msphpsql/pull/1011) - Fixed a potential buffer overflow when parsing for escaped braces in the connection string +- Pull Request [#1015](https://github.com/microsoft/msphpsql/pull/1015) - Fixed compilation issues and addressed various memory leaks detected by PHP 7.4 beta 1 +- Issue [#1027](https://github.com/microsoft/msphpsql/issues/1027) - Fixed how drivers handle query timeout settings +- Pull Request [#1049](https://github.com/microsoft/msphpsql/pull/1049) - Performance improvement for fetching from tables with many columns - cached the derived php types with column metadata to streamline data retrieval +- Pull Request [#1068](https://github.com/microsoft/msphpsql/pull/1068) - Some cosmetic changes to source code as per suggestions from a static analysis tool +- Issue [#1079](https://github.com/microsoft/msphpsql/issues/1079) - Support sql_variant types when using client buffers + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - Issue [#1050](https://github.com/microsoft/msphpsql/issues/1050) - With Always Encrypted enabled, insertion requires the column list for any tables with identity columns + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) +- Alpine Linux support is currently experimental. More robust support will be added in future releases + +### Known Issues +- In Alpine Linux, the Client-Side Cursors feature may cause an access violation if both sqlsrv and pdo_sqlsrv are enabled. Either enable only sqlsrv or pdo_sqlsrv, or build PHP from source by compiling the drivers statically. +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) + ## 5.7.1-preview - 2019-12-03 Updated PECL release packages. Here is the list of updates: diff --git a/Dockerfile-msphpsql b/Dockerfile-msphpsql index 4960b584..31eba77a 100644 --- a/Dockerfile-msphpsql +++ b/Dockerfile-msphpsql @@ -21,6 +21,7 @@ RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && \ make \ php7.3 \ php7.3-dev \ + php7.3-intl \ python-pip \ re2c \ unixodbc-dev \ @@ -39,12 +40,14 @@ ENV TEST_PHP_SQL_PWD Password123 # update PATH after ODBC driver and tools are installed ENV PATH="/opt/mssql-tools/bin:${PATH}" -# add locale iso-8859-1 +# add locales for testing RUN sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen -RUN locale-gen en_US +RUN sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/g' /etc/locale.gen +RUN sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/g' /etc/locale.gen +RUN locale-gen # set locale to utf-8 -RUN locale-gen en_US.UTF-8 +# RUN locale-gen en_US.UTF-8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8' # install coveralls (upgrade both pip and requests first) @@ -65,6 +68,10 @@ RUN /bin/bash -c "./packagize.sh" RUN echo "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.3/mods-available/sqlsrv.ini RUN echo "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.3/mods-available/pdo_sqlsrv.ini +# create a writable ini file for testing locales +RUN echo '' > `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/99-overrides.ini +RUN chmod 666 `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/99-overrides.ini + WORKDIR $PHPSQLDIR/source/sqlsrv RUN /usr/bin/phpize && ./configure LDFLAGS="-lgcov" CXXFLAGS="-O0 --coverage" && make && make install diff --git a/LICENSE b/LICENSE index 6fa366d5..a58c37db 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright(c) 2019 Microsoft Corporation +Copyright(c) 2020 Microsoft Corporation All rights reserved. MIT License diff --git a/Linux-mac-install.md b/Linux-mac-install.md index fb317515..b3decb93 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,23 +1,27 @@ # Linux and macOS Installation Tutorial for the Microsoft Drivers for PHP for SQL Server -The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, Apache, and the Microsoft Drivers for PHP for SQL Server on Ubuntu, RedHat, Debian, Suse, and macOS. These instructions advise installing the drivers using PECL, but you may also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver##loading-the-driver-at-php-startup). +The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 19.10, RedHat 7 and 8, Debian 8, 9, and 10, Suse 12 and 15, Alpine 3.11 (experimental), and macOS 10.13, 10.14, and 10.15. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver#loading-the-driver-at-php-startup). -These instructions install PHP 7.4 by default. Note that some supported Linux distros default to PHP 7.1 or earlier, which the PHP drivers for SQL Server no longer support. When installing PHP 7.2 or above, please read the notes at the beginning of each section below. +These instructions install PHP 7.4 by default. Note that some supported Linux distros default to PHP 7.1 or earlier, which is not supported for the latest version of the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.2 or 7.3 instead. + +Also included are instructions for installing the PHP FastCGI Process Manager, PHP-FPM, on Ubuntu. This is needed if using the nginx web server instead of Apache. ## Contents of this page: -- [Installing the drivers on Ubuntu 16.04, 18.04, and 19.04](#installing-the-drivers-on-ubuntu-1604-1804-and-1904) +- [Installing the drivers on Ubuntu 16.04, 18.04, and 19.10](#installing-the-drivers-on-ubuntu-1604-1804-and-1910) +- [Installing the drivers with PHP-FPM on Ubuntu](#installing-the-drivers-with-php-fpm-on-ubuntu) - [Installing the drivers on Red Hat 7 and 8](#installing-the-drivers-on-red-hat-7-and-8) -- [Installing the drivers on Debian 8, 9 and 10](#installing-the-drivers-on-debian-8-9-and-10) +- [Installing the drivers on Debian 8, 9, and 10](#installing-the-drivers-on-debian-8-9-and-10) - [Installing the drivers on Suse 12 and 15](#installing-the-drivers-on-suse-12-and-15) -- [Installing the drivers on macOS Sierra, High Sierra, Mojave, and Catalina](#installing-the-drivers-on-macos-sierra-high-sierra-mojave-and-catalina) +- [Installing the drivers on Alpine 3.11](#installing-the-drivers-on-alpine-311) +- [Installing the drivers on macOS High Sierra, Mojave, and Catalina](#installing-the-drivers-on-macos-high-sierra-mojave-and-catalina) -## Installing the drivers on Ubuntu 16.04, 18.04, and 19.04 +## Installing the drivers on Ubuntu 16.04, 18.04, and 19.10 > [!NOTE] -> To install PHP 7.3 or 7.2, replace 7.4 with 7.3 or 7.2 in the following commands. +> To install PHP 7.2 or 7.3, replace 7.4 with 7.2 or 7.3 in the following commands. ### Step 1. Install PHP -``` +```bash sudo su add-apt-repository ppa:ondrej/php -y apt-get update @@ -28,15 +32,16 @@ Install the ODBC driver for Ubuntu by following the instructions on the [Linux a ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv-5.7.1preview -sudo pecl install pdo_sqlsrv-5.7.1preview +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv sudo su printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini exit sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv ``` -If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + +If there is only one PHP version in the system, then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. ### Step 4. Install Apache and configure driver loading ``` @@ -53,53 +58,120 @@ sudo service apache2 restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Red Hat 7 and 8 +## Installing the drivers with PHP-FPM on Ubuntu > [!NOTE] -> To install PHP 7.3 or 7.2, replace remi-php74 with remi-php73 or remi-php72 respectively in the following commands. +> To install PHP 7.2 or 7.3, replace 7.4 with 7.2 or 7.3 in the following commands. + +### Step 1. Install PHP +```bash +sudo su +add-apt-repository ppa:ondrej/php -y +apt-get update +apt-get install php7.4 php7.4-dev php7.4-xml php7.4-fpm -y --allow-unauthenticated +``` +Verify the status of the PHP-FPM service by running +``` +systemctl status php7.4-fpm +``` +### Step 2. Install prerequisites +Install the ODBC driver for Ubuntu by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). + +### Step 3. Install the PHP drivers for Microsoft SQL Server +``` +sudo pecl config-set php_ini /etc/php/7.3/fpm/php.ini +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv +sudo su +printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini +printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini +exit +sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv +``` +If there is only one PHP version in the system, then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + +Verify that `sqlsrv.ini` and `pdo_sqlsrv.ini` are located in `/etc/php/7.4/fpm/conf.d/`: +``` +ls /etc/php/7.4/fpm/conf.d/*sqlsrv.ini +``` +Restart the PHP-FPM service: +``` +sudo systemctl restart php7.4-fpm +``` + +### Step 4. Install and configure nginx +``` +sudo apt-get update +sudo apt-get install nginx +sudo systemctl status nginx +``` +To configure nginx, you must edit the `/etc/nginx/sites-available/default` file. Add `index.php` to the list below the section that says `# Add index.php to the list if you are using PHP`: +``` +# Add index.php to the list if you are using PHP +index index.html index.htm index.nginx-debian.html index.php; +``` +Next, modify the section following `# pass PHP scripts to FastCGI server` as follows: +``` +# pass PHP scripts to FastCGI server +# +location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php7.4-fpm.sock; +} +``` +### Step 5. Restart nginx and test the sample script +``` +sudo systemctl restart nginx.service +``` +To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. + +## Installing the drivers on Red Hat 7 and 8 ### Step 1. Install PHP +To install PHP on Red Hat 7, run the following: +> [!NOTE] +> To install PHP 7.2 or 7.3, replace remi-php74 with remi-php72 or remi-php73 respectively in the following commands. ``` sudo su -wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -wget https://rpms.remirepo.net/enterprise/remi-release-7.rpm -rpm -Uvh remi-release-7.rpm epel-release-latest-7.noarch.rpm +yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm subscription-manager repos --enable=rhel-7-server-optional-rpms yum install yum-utils yum-config-manager --enable remi-php74 yum update yum install php php-pdo php-xml php-pear php-devel re2c gcc-c++ gcc ``` -### Step 2. Install prerequisites -Install the ODBC driver for Red Hat 7 and 8 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). -In some versions of Red Hat 7, compiling the PHP drivers with PECL and PHP 7.2 requires a more recent GCC than the default: +To install PHP on Red Hat 8, run the following: +> [!NOTE] +> To install PHP 7.2 or 7.3, replace remi-7.4 with remi-7.2 or remi-7.3 respectively in the following commands. ``` -sudo yum-config-manager --enable rhel-server-rhscl-7-rpms -sudo yum install devtoolset-7 -scl enable devtoolset-7 bash +sudo su +dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm +dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm +dnf install yum-utils +dnf module reset php +dnf module install php:remi-7.4 +subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms +dnf update +dnf install php-pdo php-pear php-devel ``` + +### Step 2. Install prerequisites +Install the ODBC driver for Red Hat 7 or 8 by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). + ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv-5.7.1preview -sudo pecl install pdo_sqlsrv-5.7.1preview +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/30-pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/20-sqlsrv.ini exit ``` -An issue in PECL may prevent correct installation of the latest version of the drivers even if you have upgraded GCC. To install, download the packages and compile manually (similar steps for pdo_sqlsrv): -``` -pecl download sqlsrv-5.7.1preview -tar xvzf sqlsrv-5.7.1preview.tgz -cd sqlsrv-5.7.1preview/ -phpize -./configure --with-php-config=/usr/bin/php-config -make -sudo make install -``` -You can alternatively download the prebuilt binaries from the [Github project page](https://github.com/Microsoft/msphpsql/releases), or install from the Remi repo: + +You can alternatively install from the Remi repo: ``` sudo yum install php-sqlsrv ``` @@ -117,10 +189,10 @@ sudo apachectl restart ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on Debian 8, 9 and 10 +## Installing the drivers on Debian 8, 9, and 10 > [!NOTE] -> To install PHP 7.3 or 7.2, replace 7.4 in the following commands with 7.3 or 7.2. +> To install PHP 7.2 or 7.3, replace 7.4 in the following commands with 7.2 or 7.3. ### Step 1. Install PHP ``` @@ -129,7 +201,7 @@ apt-get install curl apt-transport-https wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list apt-get update -apt-get install -y php7.4 php7.4-dev php7.4-xml +apt-get install -y php7.4 php7.4-dev php7.4-xml php7.4-intl ``` ### Step 2. Install prerequisites Install the ODBC driver for Debian by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). @@ -140,18 +212,20 @@ sudo su sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/g' /etc/locale.gen locale-gen ``` +You may need to add `/usr/sbin` to your `$PATH`, as the `locale-gen` executable is located there. ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv-5.7.1preview -sudo pecl install pdo_sqlsrv-5.7.1preview +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv sudo su printf "; priority=20\nextension=sqlsrv.so\n" > /etc/php/7.4/mods-available/sqlsrv.ini printf "; priority=30\nextension=pdo_sqlsrv.so\n" > /etc/php/7.4/mods-available/pdo_sqlsrv.ini exit sudo phpenmod -v 7.4 sqlsrv pdo_sqlsrv ``` -If there is only one PHP version in the system then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. + +If there is only one PHP version in the system, then the last step can be simplified to `phpenmod sqlsrv pdo_sqlsrv`. As with `locale-gen`, `phpenmod` is located in `/usr/sbin` so you may need to add this directory to your `$PATH`. ### Step 4. Install Apache and configure driver loading ``` @@ -170,21 +244,21 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Suse 12 and 15 > [!NOTE] -> In the following instructions, replace with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. +> In the following instructions, replace `` with your version of Suse - if you are using Suse Enterprise Linux 15, it will be SLE_15 or SLE_15_SP1. For Suse 12, use SLE_12_SP4 (or above if applicable). Not all versions of PHP are available for all versions of Suse Linux - please refer to `http://download.opensuse.org/repositories/devel:/languages:/php` to see which versions of Suse have the default version PHP available, or to `http://download.opensuse.org/repositories/devel:/languages:/php:/` to see which other versions of PHP are available for which versions of Suse. > [!NOTE] > Packages for PHP 7.4 are not available for Suse 12. -> To install PHP 7.3, replace the repository URL below with the following URL: - `https://download.opensuse.org/repositories/devel:/languages:/php:/php73//devel:languages:php:php73.repo`. > To install PHP 7.2, replace the repository URL below with the following URL: `https://download.opensuse.org/repositories/devel:/languages:/php:/php72//devel:languages:php:php72.repo`. +> To install PHP 7.3, replace the repository URL below with the following URL: + `https://download.opensuse.org/repositories/devel:/languages:/php:/php73//devel:languages:php:php73.repo`. ### Step 1. Install PHP ``` sudo su zypper -n ar -f https://download.opensuse.org/repositories/devel:languages:php//devel:languages:php.repo zypper --gpg-auto-import-keys refresh -zypper -n install php7 php7-pear php7-devel php7-openssl +zypper -n install php7 php7-devel php7-openssl ``` ### Step 2. Install prerequisites Install the ODBC driver for Suse by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). @@ -194,8 +268,8 @@ Install the ODBC driver for Suse by following the instructions on the [Linux and > If you get an error message saying `Connection to 'pecl.php.net:443' failed: Unable to find the socket transport "ssl"`, edit the pecl script at /usr/bin/pecl and remove the `-n` switch in the last line. This switch prevents PECL from loading ini files when PHP is called, which prevents the OpenSSL extension from loading. ``` -sudo pecl install sqlsrv-5.7.1preview -sudo pecl install pdo_sqlsrv-5.7.1preview +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/sqlsrv.ini @@ -216,7 +290,52 @@ sudo systemctl restart apache2 ``` To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. -## Installing the drivers on macOS Sierra, High Sierra, Mojave, and Catalina +## Installing the drivers on Alpine 3.11 + +> [!NOTE] +> Alpine support is experimental. + +> [!NOTE] +> The default version of PHP is 7.3. Alternate versions of PHP are not available from other repositories for Alpine 3.11. You can instead compile PHP from source. + +### Step 1. Install PHP +PHP packages for Alpine are found in the `edge/community` repository. Add the following line to `/etc/apt/repositories`, replacing `` with the URL of an Alpine repository mirror: +``` +http:///alpine/edge/community +``` +Then run: +``` +sudo su +apk update +apk add php7 php7-dev php7-pear php7-pdo php7-openssl autoconf make g++ +``` +### Step 2. Install prerequisites +Install the ODBC driver for Alpine by following the instructions on the [Linux and macOS installation page](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server). + +### Step 3. Install the PHP drivers for Microsoft SQL Server +``` +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv +sudo su +echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/10_pdo_sqlsrv.ini +echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/00_sqlsrv.ini +``` +You may need to define a locale: +``` +export LC_ALL=C +``` +### Step 4. Install Apache and configure driver loading +``` +sudo apk add php7-apache2 apache2 +``` +### Step 5. Restart Apache and test the sample script +``` +sudo rc-service apache2 restart +``` +To test your installation, see [Testing your installation](#testing-your-installation) at the end of this document. + + +## Installing the drivers on macOS High Sierra, Mojave, and Catalina If you do not already have it, install brew as follows: ``` @@ -224,7 +343,7 @@ If you do not already have it, install brew as follows: ``` > [!NOTE] -> To install PHP 7.3 or 7.2, replace php@7.4 with php@7.3 or php@7.2 respectively in the following commands. +> To install PHP 7.2 or 7.3, replace php@7.4 with php@7.2 or php@7.3 respectively in the following commands. ### Step 1. Install PHP @@ -248,18 +367,18 @@ brew install autoconf automake libtool ### Step 3. Install the PHP drivers for Microsoft SQL Server ``` -sudo pecl install sqlsrv-5.7.1preview -sudo pecl install pdo_sqlsrv-5.7.1preview +sudo pecl install sqlsrv +sudo pecl install pdo_sqlsrv ``` ### Step 4. Install Apache and configure driver loading ``` brew install apache2 ``` -To find the Apache configuration file for your Apache installation, run +To find the Apache configuration file, `httpd.conf`, for your Apache installation, run ``` -apachectl -V | grep SERVER_CONFIG_FILE +/usr/local/bin/apachectl -V | grep SERVER_CONFIG_FILE ``` -and substitute the path for `httpd.conf` in the following commands: +The following commands append the required configuration to `httpd.conf`. Be sure to substitute the path returned by the preceding command in place of `/usr/local/etc/httpd/httpd.conf`: ``` echo "LoadModule php7_module /usr/local/opt/php@7.4/lib/httpd/modules/libphp7.so" >> /usr/local/etc/httpd/httpd.conf (echo ""; echo "SetHandler application/x-httpd-php"; echo "";) >> /usr/local/etc/httpd/httpd.conf @@ -272,7 +391,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Testing Your Installation -To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. +To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, `/var/www/localhost/htdocs` on Alpine, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. On Alpine 3.11, you may also need to specify the **CharacterSet** as 'UTF-8' in the `$connectionOptions` array. ``` ``` -Point your browser to https://localhost/testsql.php (https://localhost:8080/testsql.php on macOS). You should now be able to connect to your SQL Server/Azure SQL database. \ No newline at end of file +Point your browser to https://localhost/testsql.php (https://localhost:8080/testsql.php on macOS). You should now be able to connect to your SQL Server/Azure SQL database. diff --git a/README.md b/README.md index c89e8a24..41feaf15 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Welcome to the Microsoft Drivers for PHP for Microsoft SQL Server** -The Microsoft Drivers for PHP for Microsoft SQL Server are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2008 R2 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server][odbcdoc] to handle the low-level communication with SQL Server. +The Microsoft Drivers for PHP for Microsoft SQL Server are PHP extensions that allow for the reading and writing of SQL Server data from within PHP scripts. The SQLSRV extension provides a procedural interface while the PDO_SQLSRV extension implements PHP Data Objects (PDO) for accessing data in all editions of SQL Server 2012 and later (including Azure SQL DB). These drivers rely on the [Microsoft ODBC Driver for SQL Server][odbcdoc] to handle the low-level communication with SQL Server. -This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.1+ with improvements on both drivers and some limitations. Upcoming [releases][releases] will contain additional functionalities, bug fixes, and more. +This release contains the SQLSRV and PDO_SQLSRV drivers for PHP 7.2+ with improvements on both drivers and some limitations. Upcoming [releases][releases] will contain additional functionalities, bug fixes, and more. ## Take our survey @@ -51,7 +51,7 @@ On the client machine: - [Microsoft ODBC Driver 17, Microsoft ODBC Driver 13, or Microsoft ODBC Driver 11][odbcdoc] - If using a Web server such as Internet Information Services (IIS) or Apache, it must be configured to run PHP -On the server side, Microsoft SQL Server 2008 R2 and above on Windows are supported, as are Microsoft SQL Server 2016 and above on Linux. +On the server side, Microsoft SQL Server 2012 and above on Windows are supported, as are Microsoft SQL Server 2016 and above on Linux. ## Building and Installing the Drivers on Windows diff --git a/appveyor.yml b/appveyor.yml index ef9870ef..a473ab3f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,22 +24,22 @@ environment: SQL_INSTANCE: SQL2017 PHP_VC: 15 PHP_MAJOR_VER: 7.3 - PHP_MINOR_VER: 11 + PHP_MINOR_VER: latest PHP_EXE_PATH: x64\Release_TS THREAD: ts platform: x64 - - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 BUILD_PLATFORM: x86 TEST_PHP_SQL_SERVER: (local)\SQL2016 SQL_INSTANCE: SQL2016 - PHP_VC: 14 - PHP_MAJOR_VER: 7.1 + PHP_VC: 15 + PHP_MAJOR_VER: 7.4 PHP_MINOR_VER: latest PHP_EXE_PATH: Release THREAD: nts platform: x86 -# PHP_MAJOR_VER is PHP major version to build (7.2, 7.3) +# PHP_MAJOR_VER is PHP major version to build (7.4, 7.3) # PHP_MINOR_VER is PHP point release number (or latest for latest release) # PHP_VC is the Visual C++ version # PHP_EXE_PATH is the relative path from php src folder to php executable @@ -75,9 +75,9 @@ install: - ps: | $client = New-Object Net.WebClient; $client.Headers.Add("user-agent", "appveyor-ci-build2"); - $client.DownloadFile("http://windows.php.net/downloads/releases/sha1sum.txt", "c:\projects\sha1sum.txt"); + $client.DownloadFile("http://windows.php.net/downloads/releases/sha256sum.txt", "c:\projects\sha256sum.txt"); If ($env:PHP_MINOR_VER -Match "latest") { - $env:PHP_VERSION=type c:\projects\sha1sum.txt | where { $_ -match "php-($env:PHP_MAJOR_VER\.\d+)-src" } | foreach { $matches[1] } ; + $env:PHP_VERSION=type c:\projects\sha256sum.txt | where { $_ -match "php-($env:PHP_MAJOR_VER\.\d+)-src" } | foreach { $matches[1] } ; } Else { $env:PHP_VERSION=$env:PHP_MAJOR_VER + '.' + $env:PHP_MINOR_VER; } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 23b9d72c..68c85525 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,7 +2,7 @@ # https://aka.ms/yaml variables: - phpVersion: 7.3 + phpVersion: 7.4 server: 'localhost,1433' host: 'sql1' sqlsrv_db: 'sqlsrv_testdb' @@ -59,6 +59,8 @@ jobs: displayName: 'Build and install drivers' - job: Linux + variables: + phpver: 7.3 pool: vmImage: 'ubuntu-18.04' steps: @@ -70,15 +72,17 @@ jobs: inputs: versionSpec: '3.6' architecture: 'x64' - + - script: | - sudo update-alternatives --set php /usr/bin/php$(phpVersion) - sudo update-alternatives --set phar /usr/bin/phar$(phpVersion) - sudo update-alternatives --set phpdbg /usr/bin/phpdbg$(phpVersion) - sudo update-alternatives --set php-cgi /usr/bin/php-cgi$(phpVersion) - sudo update-alternatives --set phar.phar /usr/bin/phar.phar$(phpVersion) + sudo update-alternatives --set php /usr/bin/php$(phpver) + sudo update-alternatives --set phpize /usr/bin/phpize$(phpver) + sudo update-alternatives --set phar /usr/bin/phar$(phpver) + sudo update-alternatives --set phpdbg /usr/bin/phpdbg$(phpver) + sudo update-alternatives --set php-cgi /usr/bin/php-cgi$(phpver) + sudo update-alternatives --set phar.phar /usr/bin/phar.phar$(phpver) + sudo update-alternatives --set php-config /usr/bin/php-config$(phpver) php -version - displayName: 'Use PHP version $(phpVersion)' + displayName: 'Use PHP version $(phpver)' - script: | echo install ODBC and dependencies @@ -107,15 +111,16 @@ jobs: - script: | sudo sed -i 's/# en_US ISO-8859-1/en_US ISO-8859-1/g' /etc/locale.gen - sudo locale-gen en_US - sudo locale-gen en_US.UTF-8 - export LANG='en_US.UTF-8' - export LANGUAGE='en_US:en' + sudo sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/g' /etc/locale.gen + sudo sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/g' /etc/locale.gen + sudo locale-gen + export LANG='en_US.UTF-8' + export LANGUAGE='en_US:en' export LC_ALL='en_US.UTF-8' displayName: 'Generate locales for testing' - script: | - echo setting env variables + echo setting env variables export TEST_PHP_SQL_SERVER='$(server)' export TEST_PHP_SQL_UID='$(uid)' export TEST_PHP_SQL_PWD='$(pwd)' @@ -127,12 +132,13 @@ jobs: - script: | echo ready to build extensions + sudo apt-get install -y php$(phpver)-intl cd $(Build.SourcesDirectory)/source chmod a+x packagize.sh ./packagize.sh dest=`php --ini | grep "Scan for additional .ini files" | sudo sed -e "s|.*:\s*||"`/ - + cd $(Build.SourcesDirectory)/source/sqlsrv ls -al phpize && ./configure && make && sudo make install @@ -141,7 +147,7 @@ jobs: echo copying sqlsrv to $dest sudo cp 20-sqlsrv.ini $dest - + cd $(Build.SourcesDirectory)/source/pdo_sqlsrv ls -al phpize && ./configure && make && sudo make install @@ -151,6 +157,9 @@ jobs: echo copying pdo_sqlsrv to $dest sudo cp 30-pdo_sqlsrv.ini $dest + sudo touch $dest/99-overrides.ini + sudo chmod 666 $dest/99-overrides.ini + php --ri sqlsrv php --ri pdo_sqlsrv displayName: 'Build and install drivers' @@ -160,8 +169,9 @@ jobs: sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc sed -i -e 's/TARGET_DATABASE/'"$(sqlsrv_db)"'/g' MsSetup.inc sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc - sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + export LC_ALL='en_US.UTF-8' php run-tests.php -P ./*.phpt 2>&1 | tee ../sqlsrv.log displayName: 'Run sqlsrv functional tests' @@ -170,8 +180,9 @@ jobs: sed -i -e 's/TARGET_SERVER/'"$(server)"'/g' MsSetup.inc sed -i -e 's/TARGET_DATABASE/'"$(pdo_sqlsrv_db)"'/g' MsSetup.inc sed -i -e 's/TARGET_USERNAME/'"$(uid)"'/g' MsSetup.inc - sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + sed -i -e 's/TARGET_PASSWORD/'"$(pwd)"'/g' MsSetup.inc + export LC_ALL='en_US.UTF-8' php run-tests.php -P ./*.phpt 2>&1 | tee ../pdo_sqlsrv.log displayName: 'Run pdo_sqlsrv functional tests' @@ -194,7 +205,7 @@ jobs: docker stop $(host) docker rm $(host) displayName: 'Stop SQL Server for Linux' - condition: always() + condition: always() - job: Windows pool: @@ -249,7 +260,7 @@ jobs: - script: msiexec /i "msodbcsql_13.1.msi" /q IACCEPTMSODBCSQLLICENSETERMS=YES ADDLOCAL=ALL condition: false - # FOR SOME REASON the set up did not set the PATH + # FOR SOME REASON the set up did not set the PATH - script: | msiexec /i "MsSqlCmdLnUtils.msi" /qn IACCEPTMSSQLCMDLNUTILSLICENSETERMS=YES displayName: 'Install SQL command line utilities version 15' @@ -260,7 +271,7 @@ jobs: $client.Headers.Add("user-agent", "azure pipeline build") $client.DownloadFile("https://windows.php.net/downloads/releases/sha256sum.txt", "sha256sum.txt") $env:VERSION=type sha256sum.txt | where { $_ -match "php-($(phpVersion)\.\d+)-src" } | foreach { $matches[1] } - Write-Host "Latest PHP $(phpVersion) is ${env:VERSION}" + Write-Host "Latest PHP $(phpVersion) is ${env:VERSION}" cd $(Build.SourcesDirectory)/buildscripts/ python builddrivers.py --PHPVER=${env:VERSION} --DRIVER=sqlsrv --ARCH=x64 --THREAD=nts --SOURCE=$(Build.SourcesDirectory)/source --TESTING --NO_RENAME dir *sqlsrv*.dll @@ -270,7 +281,7 @@ jobs: dir *sqlsrv*.dll cp *sqlsrv*.dll C:\tools\php\ext\ displayName: 'Build drivers (separately) for the latest version of PHP $(phpVersion)' - + - script: | echo extension=php_sqlsrv.dll >> C:\tools\php\php.ini echo extension=php_pdo_sqlsrv.dll >> C:\tools\php\php.ini @@ -285,7 +296,7 @@ jobs: displayName: 'Run SQL Server for Windows Server' condition: false - - script: | + - script: | set path=C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;%path% sqlcmd -S $(host) -U $(uid) -P $(pwd) -Q "SELECT @@Version" set TEST_PHP_SQL_SERVER=$(host) @@ -309,4 +320,4 @@ jobs: docker stop sqlcontainer docker rm sqlcontainer displayName: 'Stop SQL Server for Windows Server' - condition: false \ No newline at end of file + condition: false diff --git a/source/pdo_sqlsrv/config.m4 b/source/pdo_sqlsrv/config.m4 index 75961ad7..92bfc414 100644 --- a/source/pdo_sqlsrv/config.m4 +++ b/source/pdo_sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.7 for PHP for SQL Server +dnl Microsoft Drivers 5.8 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/pdo_sqlsrv/config.w32 b/source/pdo_sqlsrv/config.w32 index 1d37593b..a82fbb71 100644 --- a/source/pdo_sqlsrv/config.w32 +++ b/source/pdo_sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index eedd07b1..d950ac3b 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDO object for PDO_SQLSRV // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -1105,7 +1105,7 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout } break; -#if PHP_VERSION_ID >= 70200 +#if PHP_VERSION_ID >= 70200 case PDO_ATTR_DEFAULT_STR_PARAM: { if (Z_TYPE_P(val) != IS_LONG) { @@ -1297,7 +1297,7 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout break; } -#if PHP_VERSION_ID >= 70200 +#if PHP_VERSION_ID >= 70200 case PDO_ATTR_DEFAULT_STR_PARAM: { ZVAL_LONG(return_value, (driver_dbh->use_national_characters == 0) ? PDO_PARAM_STR_CHAR : PDO_PARAM_STR_NATL); @@ -1523,12 +1523,12 @@ int pdo_sqlsrv_dbh_quote( _Inout_ pdo_dbh_t* dbh, _In_reads_(unquoted_len) const } use_national_char_set = (driver_dbh->use_national_characters == 1 || encoding == SQLSRV_ENCODING_UTF8); -#if PHP_VERSION_ID >= 70200 - if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { - use_national_char_set = true; - } - if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { - use_national_char_set = false; +#if PHP_VERSION_ID >= 70200 + if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { + use_national_char_set = true; + } + if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { + use_national_char_set = false; } #endif diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index 8f374093..4f1a8845 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -3,7 +3,7 @@ // // Contents: initialization routines for PDO_SQLSRV // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -220,6 +220,22 @@ PHP_RINIT_FUNCTION(pdo_sqlsrv) ZEND_TSRMLS_CACHE_UPDATE(); #endif +#ifndef _WIN32 + // if necessary, set locale from the environment for ODBC, which MUST be done before any connection + int set_locale = PDO_SQLSRV_G(set_locale_info); + if (set_locale == 2) { + setlocale(LC_ALL, ""); + LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_ALL"); + } + else if (set_locale == 1) { + setlocale(LC_CTYPE, ""); + LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_CTYPE"); + } + else { + LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale NONE"); + } +#endif + LOG( SEV_NOTICE, "pdo_sqlsrv: entering rinit" ); return SUCCESS; diff --git a/source/pdo_sqlsrv/pdo_parser.cpp b/source/pdo_sqlsrv/pdo_parser.cpp index 6a96ace2..48b591be 100644 --- a/source/pdo_sqlsrv/pdo_parser.cpp +++ b/source/pdo_sqlsrv/pdo_parser.cpp @@ -5,7 +5,7 @@ // // Copyright Microsoft Corporation // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 448eee04..65f5a7da 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Implements the PDOStatement object for the PDO_SQLSRV // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index cff7add5..56048330 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -3,7 +3,7 @@ // // Contents: Utility functions used by both connection or statement functions // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index a6403219..ab1aaf26 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -6,7 +6,7 @@ // // Contents: Declarations for the extension // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -32,6 +32,10 @@ ZEND_BEGIN_MODULE_GLOBALS(pdo_sqlsrv) unsigned int log_severity; zend_long client_buffer_max_size; +#ifndef _WIN32 +zend_long set_locale_info; +#endif + ZEND_END_MODULE_GLOBALS(pdo_sqlsrv) ZEND_EXTERN_MODULE_GLOBALS(pdo_sqlsrv); diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 2b7269c0..2934e11f 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -6,7 +6,7 @@ // // Contents: Internal declarations for the extension // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -55,11 +55,19 @@ extern HMODULE g_sqlsrv_hmodule; #define INI_PDO_SQLSRV_LOG "log_severity" #define INI_PREFIX "pdo_sqlsrv." +#ifndef _WIN32 +#define INI_PDO_SET_LOCALE_INFO "set_locale_info" +#endif + PHP_INI_BEGIN() STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, log_severity, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE , INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, client_buffer_max_size, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) +#ifndef _WIN32 + STD_PHP_INI_ENTRY(INI_PREFIX INI_PDO_SET_LOCALE_INFO, "2", PHP_INI_ALL, OnUpdateLong, set_locale_info, + zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals) +#endif PHP_INI_END() diff --git a/source/pdo_sqlsrv/template.rc b/source/pdo_sqlsrv/template.rc index 2db6a2e9..350a2f82 100644 --- a/source/pdo_sqlsrv/template.rc +++ b/source/pdo_sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.cpp b/source/shared/FormattedPrint.cpp index 185489d7..dd076078 100644 --- a/source/shared/FormattedPrint.cpp +++ b/source/shared/FormattedPrint.cpp @@ -6,7 +6,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/FormattedPrint.h b/source/shared/FormattedPrint.h index 32f65ebb..79b824b4 100644 --- a/source/shared/FormattedPrint.h +++ b/source/shared/FormattedPrint.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.cpp b/source/shared/StringFunctions.cpp index ac0a8239..7426644f 100644 --- a/source/shared/StringFunctions.cpp +++ b/source/shared/StringFunctions.cpp @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/StringFunctions.h b/source/shared/StringFunctions.h index 0eb011c3..7593a534 100644 --- a/source/shared/StringFunctions.h +++ b/source/shared/StringFunctions.h @@ -3,7 +3,7 @@ // // Contents: Contains functions for handling UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 10545e42..c328a9d5 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use connection handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_init.cpp b/source/shared/core_init.cpp index 63d08ce9..1d483078 100644 --- a/source/shared/core_init.cpp +++ b/source/shared/core_init.cpp @@ -3,7 +3,7 @@ // // Contents: common initialization routines shared by PDO and sqlsrv // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -39,12 +39,6 @@ void core_sqlsrv_minit( _Outptr_ sqlsrv_context** henv_cp, _Inout_ sqlsrv_contex SQLSRV_STATIC_ASSERT( sizeof( sqlsrv_sqltype ) == sizeof( zend_long ) ); SQLSRV_STATIC_ASSERT( sizeof( sqlsrv_phptype ) == sizeof( zend_long )); -#ifndef _WIN32 - // set locale from environment - // this is necessary for ODBC and MUST be done before connection - setlocale(LC_ALL, ""); -#endif - *henv_cp = *henv_ncp = SQL_NULL_HANDLE; // initialize return values to NULL try { diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index 1fbe005f..0b9c8fd9 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -3,7 +3,7 @@ // // Contents: Result sets // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -508,6 +508,7 @@ sqlsrv_buffered_result_set::sqlsrv_buffered_result_set( _Inout_ sqlsrv_stmt* stm break; case SQL_CHAR: case SQL_VARCHAR: + case SQL_SS_VARIANT: if ( meta[i].length == sqlsrv_buffered_result_set::meta_data::SIZE_UNKNOWN ) { offset += sizeof( void* ); } @@ -610,6 +611,7 @@ sqlsrv_buffered_result_set::sqlsrv_buffered_result_set( _Inout_ sqlsrv_stmt* stm case SQL_CHAR: case SQL_VARCHAR: + case SQL_SS_VARIANT: case SQL_LONGVARCHAR: // If encoding is set to UTF-8, the following types are not necessarily column size. // We need to call SQLGetData with c_type SQL_C_WCHAR and set the size accordingly. diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index be6ead07..ae27400e 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -6,17 +6,17 @@ // // Contents: Core routines and constants shared by the Microsoft Drivers for PHP for SQL Server // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), -// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, // and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- @@ -28,7 +28,7 @@ #undef SQL_WCHART_CONVERT #endif #ifndef _WCHART_DEFINED -#define _WCHART_DEFINED +#define _WCHART_DEFINED #endif #include "php.h" @@ -54,8 +54,6 @@ #define PHP_SQLSRV_API #endif -// #define MultiByteToWideChar SystemLocale::ToUtf16 - #define stricmp strcasecmp #define strnicmp strncasecmp #define strnlen_s(s) strnlen_s(s, INT_MAX) @@ -103,10 +101,10 @@ extern "C" { #endif #if _MSC_VER >= 1400 -// typedef and macro to prevent a conflict between php.h and ws2tcpip.h. -// php.h defines this constant as unsigned int which causes a compile error +// typedef and macro to prevent a conflict between php.h and ws2tcpip.h. +// php.h defines this constant as unsigned int which causes a compile error // in ws2tcpip.h. Fortunately php.h allows an override by defining -// HAVE_SOCKLEN_T. Since ws2tcpip.h isn't included until later, we define +// HAVE_SOCKLEN_T. Since ws2tcpip.h isn't included until later, we define // socklen_t here and override the php.h version. typedef int socklen_t; #define HAVE_SOCKLEN_T @@ -142,7 +140,7 @@ OACR_WARNING_POP #define WC_ERR_INVALID_CHARS 0x00000080 // error for invalid chars #endif -// PHP defines inline as __forceinline, which in debug mode causes a warning to be emitted when +// PHP defines inline as __forceinline, which in debug mode causes a warning to be emitted when // we use std::copy, which causes compilation to fail since we compile with warnings as errors. #if defined(ZEND_DEBUG) && defined(inline) #undef inline @@ -190,7 +188,7 @@ const long ACTIVE_NUM_ROWS_INVALID = -99; const int SQL_SERVER_2005_DEFAULT_DATETIME_PRECISION = 23; const int SQL_SERVER_2005_DEFAULT_DATETIME_SCALE = 3; const int SQL_SERVER_2008_DEFAULT_DATETIME_PRECISION = 34; -const int SQL_SERVER_2008_DEFAULT_DATETIME_SCALE = 7; +const int SQL_SERVER_2008_DEFAULT_DATETIME_SCALE = 7; namespace AzureADOptions { const char AZURE_AUTH_SQL_PASSWORD[] = "SqlPassword"; @@ -215,7 +213,7 @@ enum SQLSRV_PHPTYPE { }; // encodings supported by this extension. These basically translate into the use of SQL_C_CHAR or SQL_C_BINARY when getting -// information as a string or a stream. +// information as a string or a stream. enum SQLSRV_ENCODING { SQLSRV_ENCODING_INVALID, // unknown or invalid encoding. Used to initialize variables. SQLSRV_ENCODING_DEFAULT, // use what is the connection's default for a statement, use system if a connection @@ -263,7 +261,7 @@ union sqlsrv_sqltype { // SQLSRV PHP types (as opposed to the Zend PHP type constants). Contains the type (see SQLSRV_PHPTYPE) -// and the encoding for strings and streams (see SQLSRV_ENCODING) +// and the encoding for strings and streams (see SQLSRV_ENCODING) union sqlsrv_phptype { @@ -325,8 +323,8 @@ void die( _In_opt_ const char* msg, ... ); #pragma push_macro( "max" ) #undef max -// new memory allocation/free debugging facilities to help us verify that all allocations are being -// released in a timely manner and not just at the end of the script. +// new memory allocation/free debugging facilities to help us verify that all allocations are being +// released in a timely manner and not just at the end of the script. // Zend has memory logging and checking, but it can generate a lot of noise for just one extension. // It's meant for internal use but might be useful for people adding features to our extension. // To use it, uncomment the #define below and compile in Debug NTS. All allocations and releases @@ -435,7 +433,7 @@ struct remove_const { // this allows us to use STL classes that still work with Zend objects template struct sqlsrv_allocator { - + // typedefs used by the STL classes typedef T value_type; typedef value_type* pointer; @@ -459,8 +457,8 @@ struct sqlsrv_allocator { // address (doesn't work if the class defines operator&) inline pointer address( _In_ reference r ) - { - return &r; + { + return &r; } inline const_pointer address( _In_ const_reference r ) @@ -469,20 +467,20 @@ struct sqlsrv_allocator { } // memory allocation/deallocation - inline pointer allocate( _In_ size_type cnt, + inline pointer allocate( _In_ size_type cnt, typename std::allocator::const_pointer = 0 ) { - return reinterpret_cast( sqlsrv_malloc(cnt, sizeof (T), 0)); + return reinterpret_cast( sqlsrv_malloc(cnt, sizeof (T), 0)); } - inline void deallocate( _Inout_ pointer p, size_type ) - { - sqlsrv_free(p); + inline void deallocate( _Inout_ pointer p, size_type ) + { + sqlsrv_free(p); } // size - inline size_type max_size( void ) const - { + inline size_type max_size( void ) const + { return std::numeric_limits::max() / sizeof(T); } @@ -507,11 +505,11 @@ struct sqlsrv_allocator { { return !operator==(a); } -}; +}; -// base class for auto_ptrs that we define below. It provides common operators and functions -// used by all the classes. +// base class for auto_ptrs that we define below. It provides common operators and functions +// used by all the classes. template class sqlsrv_auto_ptr { @@ -576,8 +574,8 @@ public: return _ptr[index]; } - - #ifdef __WIN64 + + #ifdef __WIN64 // there are a number of places where we allocate a block intended to be accessed as // an array of elements, so this operator allows us to treat the memory as such. T& operator[]( _In_ std::size_t index ) const @@ -616,7 +614,7 @@ public: protected: sqlsrv_auto_ptr( _In_opt_ T* ptr ) : - _ptr( ptr ) + _ptr( ptr ) { } @@ -637,7 +635,7 @@ protected: return ptr; } - T* _ptr; + T* _ptr; }; // an auto_ptr for sqlsrv_malloc/sqlsrv_free. When allocating a chunk of memory using sqlsrv_malloc, wrap that pointer @@ -684,13 +682,13 @@ public: // DO NOT CALL sqlsrv_realloc with a sqlsrv_malloc_auto_ptr. Use the resize member function. // has the same parameter list as sqlsrv_realloc: new_size is the size in bytes of the newly allocated buffer void resize( _In_ size_t new_size ) - { + { sqlsrv_auto_ptr >::_ptr = reinterpret_cast( sqlsrv_realloc( sqlsrv_auto_ptr >::_ptr, new_size )); } }; -// auto ptr for Zend hash tables. Used to clean up a hash table allocated when +// auto ptr for Zend hash tables. Used to clean up a hash table allocated when // something caused an early exit from the function. This is used when the hash_table is // allocated in a zval that itself can't be released. Otherwise, use the zval_auto_ptr. @@ -726,8 +724,8 @@ private: }; -// an auto_ptr for zvals. When allocating a zval, wrap that pointer in a variable of zval_auto_ptr. -// zval_auto_ptr will "own" that zval and assure that it is freed when the variable is destroyed +// an auto_ptr for zvals. When allocating a zval, wrap that pointer in a variable of zval_auto_ptr. +// zval_auto_ptr will "own" that zval and assure that it is freed when the variable is destroyed // (out of scope) or ownership is transferred using the function "transferred". class zval_auto_ptr : public sqlsrv_auto_ptr { @@ -798,7 +796,7 @@ struct sqlsrv_error : public sqlsrv_error_const { native_code = code; format = printf_format; } - + sqlsrv_error( _In_ sqlsrv_error_const const& prototype ) { sqlsrv_error( prototype.sqlstate, prototype.native_message, prototype.native_code, prototype.format ); @@ -865,8 +863,8 @@ class sqlsrv_context; struct sqlsrv_conn; // error_callback -// a driver specific callback for processing errors. -// ctx - the context holding the handles +// a driver specific callback for processing errors. +// ctx - the context holding the handles // sqlsrv_error_code - specific error code to return. typedef bool (*error_callback)( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_error_code, _In_ bool error TSRMLS_DC, _In_opt_ va_list* print_args ); @@ -910,7 +908,7 @@ class sqlsrv_context { } virtual ~sqlsrv_context() - { + { } void set_func( _In_z_ const char* f ) @@ -927,7 +925,7 @@ class sqlsrv_context { { return last_error_; } - + // since the primary responsibility of a context is to hold an ODBC handle, we // provide these convenience operators for using them interchangeably operator SQLHANDLE ( void ) const @@ -997,7 +995,7 @@ class sqlsrv_context { error_callback err_; // driver error callback if error occurs in core layer void* driver_; // points back to the driver for PDO sqlsrv_error_auto_ptr last_error_; // last error that happened on this object - SQLSRV_ENCODING encoding_; // encoding of the context + SQLSRV_ENCODING encoding_; // encoding of the context }; // maps an IANA encoding to a code page @@ -1064,7 +1062,7 @@ enum DRIVER_VERSION { struct sqlsrv_stmt; struct stmt_option; -// This holds the various details of column encryption. +// This holds the various details of column encryption. struct col_encryption_option { bool enabled; // column encryption enabled, false by default SQLINTEGER akv_mode; @@ -1105,13 +1103,13 @@ struct sqlsrv_conn : public sqlsrv_context { driver_version = ODBC_DRIVER_UNKNOWN; } - // sqlsrv_conn has no destructor since its allocated using placement new, which requires that the destructor be + // sqlsrv_conn has no destructor since its allocated using placement new, which requires that the destructor be // called manually. Instead, we leave it to the allocator to invalidate the handle when an error occurs allocating // the sqlsrv_conn with a connection. }; enum SQLSRV_STMT_OPTIONS { - + SQLSRV_STMT_OPTION_INVALID, SQLSRV_STMT_OPTION_QUERY_TIMEOUT, SQLSRV_STMT_OPTION_SEND_STREAMS_AT_EXEC, @@ -1164,7 +1162,7 @@ const char SERVER[] = "Server"; } enum SQLSRV_CONN_OPTIONS { - + SQLSRV_CONN_OPTION_INVALID, SQLSRV_CONN_OPTION_APP, SQLSRV_CONN_OPTION_ACCESS_TOKEN, @@ -1200,7 +1198,7 @@ enum SQLSRV_CONN_OPTIONS { // Driver specific connection options SQLSRV_CONN_OPTION_DRIVER_SPECIFIC = 1000, - + }; @@ -1220,7 +1218,7 @@ struct connection_option { // the name of the option as passed in by the user const char * sqlsrv_name; unsigned int sqlsrv_len; - + unsigned int conn_option_key; // the name of the option in the ODBC connection string const char * odbc_name; @@ -1247,7 +1245,7 @@ struct column_encryption_set_func { static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); }; -struct driver_set_func { +struct driver_set_func { static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC ); }; @@ -1265,8 +1263,8 @@ typedef sqlsrv_conn* (*driver_conn_factory)( _In_ SQLHANDLE h, _In_ error_callba // *** connection functions *** sqlsrv_conn* core_sqlsrv_connect( _In_ sqlsrv_context& henv_cp, _In_ sqlsrv_context& henv_ncp, _In_ driver_conn_factory conn_factory, - _Inout_z_ const char* server, _Inout_opt_z_ const char* uid, _Inout_opt_z_ const char* pwd, - _Inout_opt_ HashTable* options_ht, _In_ error_callback err, _In_ const connection_option valid_conn_opts[], + _Inout_z_ const char* server, _Inout_opt_z_ const char* uid, _Inout_opt_z_ const char* pwd, + _Inout_opt_ HashTable* options_ht, _In_ error_callback err, _In_ const connection_option valid_conn_opts[], _In_ void* driver, _In_z_ const char* driver_func TSRMLS_DC ); SQLRETURN core_odbc_connect( _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str, _In_ bool is_pooled ); void core_sqlsrv_close( _Inout_opt_ sqlsrv_conn* conn TSRMLS_DC ); @@ -1332,9 +1330,9 @@ struct stmt_option { const char * name; // name of the statement option unsigned int name_len; // name length - unsigned int key; + unsigned int key; std::unique_ptr func; // callback that actually handles the work of the option - + }; // holds the stream param and the encoding that it was assigned @@ -1368,9 +1366,9 @@ extern php_stream_wrapper g_sqlsrv_stream_wrapper; // *** parameter metadata struct *** struct param_meta_data { - SQLSMALLINT sql_type; + SQLSMALLINT sql_type; SQLSMALLINT decimal_digits; - SQLSMALLINT nullable; + SQLSMALLINT nullable; SQLULEN column_size; param_meta_data() : sql_type(0), decimal_digits(0), column_size(0), nullable(0) @@ -1416,16 +1414,16 @@ struct sqlsrv_output_param { { } - void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE) - { + void saveMetaData(SQLSMALLINT sql_type, SQLSMALLINT column_size, SQLSMALLINT decimal_digits, SQLSMALLINT nullable = SQL_NULLABLE) + { meta_data.sql_type = sql_type; meta_data.column_size = column_size; meta_data.decimal_digits = decimal_digits; meta_data.nullable = nullable; } - param_meta_data& getMetaData() - { + param_meta_data& getMetaData() + { return meta_data; } }; @@ -1472,13 +1470,13 @@ namespace data_classification { { } - ~column_sensitivity() + ~column_sensitivity() { label_info_pairs.clear(); } }; - struct sensitivity_metadata { + struct sensitivity_metadata { USHORT num_labels; std::vector> labels; USHORT num_infotypes; @@ -1491,7 +1489,7 @@ namespace data_classification { } ~sensitivity_metadata() - { + { reset(); } @@ -1503,7 +1501,7 @@ namespace data_classification { struct sqlsrv_result_set; struct field_meta_data; -// *** Statement resource structure *** +// *** Statement resource structure *** struct sqlsrv_stmt : public sqlsrv_context { void free_param_data( TSRMLS_D ); @@ -1513,13 +1511,13 @@ struct sqlsrv_stmt : public sqlsrv_context { void clean_up_sensitivity_metadata(); sqlsrv_conn* conn; // Connection that created this statement - + bool executed; // Whether the statement has been executed yet (used for error messages) bool past_fetch_end; // Core_sqlsrv_fetch sets this field when the statement goes beyond the last row sqlsrv_result_set* current_results; // Current result set SQLULEN cursor_type; // Type of cursor for the current result set bool has_rows; // Has_rows is set if there are actual rows in the row set - bool fetch_called; // Used by core_sqlsrv_get_field to return an informative error if fetch not yet called + bool fetch_called; // Used by core_sqlsrv_get_field to return an informative error if fetch not yet called int last_field_index; // last field retrieved by core_sqlsrv_get_field bool past_next_result_end; // core_sqlsrv_next_result sets this to true when the statement goes beyond the last results short column_count; // Number of columns in the current result set obtained from SQLNumResultCols @@ -1529,7 +1527,7 @@ struct sqlsrv_stmt : public sqlsrv_context { bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings bool format_decimals; // false by default but the user can set this to true to add the missing leading zeroes and/or control number of decimal digits to show short decimal_places; // indicates number of decimals shown in fetched results (-1 by default, which means no change to number of decimal digits) - bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata + bool data_classification; // false by default but the user can set this to true to retrieve data classification sensitivity metadata // holds output pointers for SQLBindParameter // We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving @@ -1541,10 +1539,10 @@ struct sqlsrv_stmt : public sqlsrv_context { zval param_datetime_buffers; // datetime strings to be converted back to DateTime objects bool send_streams_at_exec; // send all stream data right after execution before returning sqlsrv_stream current_stream; // current stream sending data to the server as an input parameter - unsigned int current_stream_read; // # of bytes read so far. (if we read an empty PHP stream, we send an empty string + unsigned int current_stream_read; // # of bytes read so far. (if we read an empty PHP stream, we send an empty string // to the server) zval field_cache; // cache for a single row of fields, to allow multiple and out of order retrievals - zval col_cache; // Used by get_field_as_string not to call SQLColAttribute() after every fetch. + zval col_cache; // Used by get_field_as_string not to call SQLColAttribute() after every fetch. zval active_stream; // the currently active stream reading data from the database std::vector param_descriptions; @@ -1573,7 +1571,7 @@ struct field_meta_data { SQLSMALLINT field_type; SQLULEN field_size; SQLULEN field_precision; - SQLSMALLINT field_scale; + SQLSMALLINT field_scale; SQLSMALLINT field_is_nullable; bool field_is_money_type; sqlsrv_phptype sqlsrv_php_type; @@ -1584,7 +1582,7 @@ struct field_meta_data { reset_php_type(); } - ~field_meta_data() + ~field_meta_data() { } @@ -1614,7 +1612,7 @@ const size_t SQLSRV_CURSOR_BUFFERED = 0xfffffffeUL; // arbitrary number that doe typedef sqlsrv_stmt* (*driver_stmt_factory)( sqlsrv_conn* conn, SQLHANDLE h, error_callback e, void* drv TSRMLS_DC ); // *** statement functions *** -sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stmt_factory stmt_factory, _In_opt_ HashTable* options_ht, +sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stmt_factory stmt_factory, _In_opt_ HashTable* options_ht, _In_opt_ const stmt_option valid_stmt_opts[], _In_ error_callback const err, _In_opt_ void* driver TSRMLS_DC ); void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT direction, _Inout_ zval* param_z, _In_ SQLSRV_PHPTYPE php_out_type, _Inout_ SQLSRV_ENCODING encoding, _Inout_ SQLSMALLINT sql_type, _Inout_ SQLULEN column_size, @@ -1660,7 +1658,7 @@ struct sqlsrv_result_set { virtual SQLRETURN get_data( _In_ SQLUSMALLINT field_index, _In_ SQLSMALLINT target_type, _Out_writes_bytes_opt_(buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length, bool handle_warning TSRMLS_DC )= 0; - virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, + virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, _Inout_updates_(buffer_length) SQLPOINTER diag_info_buffer, _In_ SQLSMALLINT buffer_length, _Inout_ SQLSMALLINT* out_buffer_length TSRMLS_DC ) = 0; virtual sqlsrv_error* get_diag_rec( _In_ SQLSMALLINT record_number ) = 0; @@ -1677,7 +1675,7 @@ struct sqlsrv_odbc_result_set : public sqlsrv_result_set { virtual SQLRETURN get_data( _In_ SQLUSMALLINT field_index, _In_ SQLSMALLINT target_type, _Out_writes_opt_(buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length, _In_ bool handle_warning TSRMLS_DC ); - virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, + virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, _Inout_updates_(buffer_length) SQLPOINTER diag_info_buffer, _In_ SQLSMALLINT buffer_length, _Inout_ SQLSMALLINT* out_buffer_length TSRMLS_DC ); virtual sqlsrv_error* get_diag_rec( _In_ SQLSMALLINT record_number ); @@ -1715,13 +1713,13 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { virtual SQLRETURN get_data( _In_ SQLUSMALLINT field_index, _In_ SQLSMALLINT target_type, _Out_writes_bytes_opt_(buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length, bool handle_warning TSRMLS_DC ); - virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, + virtual SQLRETURN get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, _Inout_updates_(buffer_length) SQLPOINTER diag_info_buffer, _In_ SQLSMALLINT buffer_length, _Inout_ SQLSMALLINT* out_buffer_length TSRMLS_DC ); virtual sqlsrv_error* get_diag_rec( _In_ SQLSMALLINT record_number ); virtual SQLLEN row_count( TSRMLS_D ); - // buffered result set specific + // buffered result set specific SQLSMALLINT column_count( void ) { return col_count; @@ -1758,7 +1756,7 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { // string conversion functions SQLRETURN binary_to_wide_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length ); - SQLRETURN binary_to_system_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, + SQLRETURN binary_to_system_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length ); SQLRETURN system_to_wide_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Out_ SQLLEN* out_buffer_length ); @@ -1839,7 +1837,7 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, SQLSRV_ERROR_OUTPUT_PARAM_ENCODING_TRANSLATE, SQLSRV_ERROR_CONNECT_STRING_ENCODING_TRANSLATE, - SQLSRV_ERROR_ZEND_STREAM, + SQLSRV_ERROR_ZEND_STREAM, SQLSRV_ERROR_INPUT_STREAM_ENCODING_TRANSLATE, SQLSRV_ERROR_UNKNOWN_SERVER_VERSION, SQLSRV_ERROR_FETCH_PAST_END, @@ -1910,11 +1908,11 @@ enum error_handling_flags { // 2/code) driver specific error code // 3/message) driver specific error message // The fetch type determines if the indices are numeric, associative, or both. -bool core_sqlsrv_get_odbc_error( _Inout_ sqlsrv_context& ctx, _In_ int record_number, _Inout_ sqlsrv_error_auto_ptr& error, +bool core_sqlsrv_get_odbc_error( _Inout_ sqlsrv_context& ctx, _In_ int record_number, _Inout_ sqlsrv_error_auto_ptr& error, _In_ logging_severity severity TSRMLS_DC ); // format and return a driver specfic error -void core_sqlsrv_format_driver_error( _In_ sqlsrv_context& ctx, _In_ sqlsrv_error_const const* custom_error, +void core_sqlsrv_format_driver_error( _In_ sqlsrv_context& ctx, _In_ sqlsrv_error_const const* custom_error, _Out_ sqlsrv_error_auto_ptr& formatted_error, _In_ logging_severity severity TSRMLS_DC, _In_opt_ va_list* args ); @@ -1949,19 +1947,19 @@ inline bool call_error_handler( _Inout_ sqlsrv_context* ctx, _In_ unsigned long // we don't want on a web server #define SQLSRV_ASSERT( condition, msg, ...) if( !(condition)) DIE( msg, ## __VA_ARGS__ ); - -#if defined( PHP_DEBUG ) + +#if defined( PHP_DEBUG ) #define DEBUG_SQLSRV_ASSERT( condition, msg, ... ) \ if( !(condition)) { \ DIE (msg, ## __VA_ARGS__ ); \ - } + } #else #define DEBUG_SQLSRV_ASSERT( condition, msg, ... ) ((void)0) -#endif +#endif // check to see if the sqlstate is 01004, truncated field retrieved. Used for retrieving large fields. inline bool is_truncated_warning( _In_ SQLCHAR* state ) @@ -1984,7 +1982,7 @@ inline bool is_truncated_warning( _In_ SQLCHAR* state ) ignored##unique = call_error_handler( context, ssphp TSRMLS_CC, /*warning*/false, ## __VA_ARGS__ ); \ } \ if( !ignored##unique ) - + #define CHECK_ERROR_UNIQUE( unique, condition, context, ssphp, ...) \ CHECK_ERROR_EX( unique, condition, context, ssphp, ## __VA_ARGS__ ) @@ -2003,11 +2001,11 @@ inline bool is_truncated_warning( _In_ SQLCHAR* state ) if( condition ) { \ ignored##unique = call_error_handler( context, ssphp TSRMLS_CC, /*warning*/true, ## __VA_ARGS__ ); \ } \ - if( !ignored##unique ) + if( !ignored##unique ) #define CHECK_SQL_WARNING_AS_ERROR( result, context, ... ) \ CHECK_WARNING_AS_ERROR_UNIQUE( __COUNTER__,( result == SQL_SUCCESS_WITH_INFO ), context, SQLSRV_ERROR_ODBC, ## __VA_ARGS__ ) - + #define CHECK_SQL_WARNING( result, context, ... ) \ if( result == SQL_SUCCESS_WITH_INFO ) { \ (void)call_error_handler( context, 0 TSRMLS_CC, /*warning*/ true, ## __VA_ARGS__ ); \ @@ -2015,7 +2013,7 @@ inline bool is_truncated_warning( _In_ SQLCHAR* state ) #define CHECK_CUSTOM_WARNING_AS_ERROR( condition, context, ssphp, ... ) \ CHECK_WARNING_AS_ERROR_UNIQUE( __COUNTER__, condition, context, ssphp, ## __VA_ARGS__ ) - + #define CHECK_ZEND_ERROR( zr, ctx, error, ... ) \ CHECK_ERROR_UNIQUE( __COUNTER__, ( zr == FAILURE ), ctx, error, ## __VA_ARGS__ ) \ @@ -2029,7 +2027,7 @@ inline bool is_truncated_warning( _In_ SQLCHAR* state ) ignored = call_error_handler( context, SQLSRV_ERROR_ODBC TSRMLS_CC, true TSRMLS_CC, ##__VA_ARGS__ ); \ } \ if( !ignored ) - + // throw an exception after it has been hooked into the custom error handler #define THROW_CORE_ERROR( ctx, custom, ... ) \ (void)call_error_handler( ctx, custom TSRMLS_CC, /*warning*/ false, ## __VA_ARGS__ ); \ @@ -2051,25 +2049,25 @@ namespace core { inline void check_for_mars_error( _Inout_ sqlsrv_stmt* stmt, _In_ SQLRETURN r TSRMLS_DC ) { - // Skip this if not SQL_ERROR - + // Skip this if not SQL_ERROR - // We check for the 'connection busy' error caused by having MultipleActiveResultSets off // and return a more helpful message prepended to the ODBC errors if that error occurs if (r == SQL_ERROR) { SQLCHAR err_msg[SQL_MAX_MESSAGE_LENGTH + 1] = {'\0'}; SQLSMALLINT len = 0; - - SQLRETURN rtemp = ::SQLGetDiagField( stmt->handle_type(), stmt->handle(), 1, SQL_DIAG_MESSAGE_TEXT, + + SQLRETURN rtemp = ::SQLGetDiagField( stmt->handle_type(), stmt->handle(), 1, SQL_DIAG_MESSAGE_TEXT, err_msg, SQL_MAX_MESSAGE_LENGTH, &len ); if (rtemp == SQL_SUCCESS_WITH_INFO && len > SQL_MAX_MESSAGE_LENGTH) { - // if the error message is this long, then it must not be the mars message - // defined as ODBC_CONNECTION_BUSY_ERROR -- so return here and continue the + // if the error message is this long, then it must not be the mars message + // defined as ODBC_CONNECTION_BUSY_ERROR -- so return here and continue the // regular error handling return; } CHECK_SQL_ERROR_OR_WARNING( rtemp, stmt ) { - + throw CoreException(); } @@ -2078,7 +2076,7 @@ namespace core { const std::string returned_error( reinterpret_cast( err_msg )); if(( returned_error.find( connection_busy_error ) != std::string::npos )) { - + THROW_CORE_ERROR( stmt, SQLSRV_ERROR_MARS_OFF ); } } @@ -2092,11 +2090,11 @@ namespace core { // These functions take the sqlsrv_context type. However, since the error handling code can alter // the context to hold the error, they are not passed as const. - inline SQLRETURN SQLGetDiagField( _Inout_ sqlsrv_context* ctx, _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, + inline SQLRETURN SQLGetDiagField( _Inout_ sqlsrv_context* ctx, _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, _Out_writes_opt_(buffer_length) SQLPOINTER diag_info_buffer, _In_ SQLSMALLINT buffer_length, _Out_opt_ SQLSMALLINT* out_buffer_length TSRMLS_DC ) { - SQLRETURN r = ::SQLGetDiagField( ctx->handle_type(), ctx->handle(), record_number, diag_identifier, + SQLRETURN r = ::SQLGetDiagField( ctx->handle_type(), ctx->handle(), record_number, diag_identifier, diag_info_buffer, buffer_length, out_buffer_length ); CHECK_SQL_ERROR_OR_WARNING( r, ctx ) { @@ -2106,7 +2104,7 @@ namespace core { return r; } - inline void SQLAllocHandle( _In_ SQLSMALLINT HandleType, _Inout_ sqlsrv_context& InputHandle, + inline void SQLAllocHandle( _In_ SQLSMALLINT HandleType, _Inout_ sqlsrv_context& InputHandle, _Out_ SQLHANDLE* OutputHandlePtr TSRMLS_DC ) { SQLRETURN r; @@ -2116,7 +2114,7 @@ namespace core { } } - inline void SQLBindParameter( _Inout_ sqlsrv_stmt* stmt, + inline void SQLBindParameter( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT ParameterNumber, _In_ SQLSMALLINT InputOutputType, _In_ SQLSMALLINT ValueType, @@ -2129,9 +2127,9 @@ namespace core { TSRMLS_DC ) { SQLRETURN r; - r = ::SQLBindParameter( stmt->handle(), ParameterNumber, InputOutputType, ValueType, ParameterType, ColumnSize, + r = ::SQLBindParameter( stmt->handle(), ParameterNumber, InputOutputType, ValueType, ParameterType, ColumnSize, DecimalDigits, ParameterValuePtr, BufferLength, StrLen_Or_IndPtr ); - + CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw CoreException(); } @@ -2146,7 +2144,7 @@ namespace core { } } - inline void SQLColAttribute( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLUSMALLINT field_identifier, + inline void SQLColAttribute( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLUSMALLINT field_identifier, _Out_writes_bytes_opt_(buffer_length) SQLPOINTER field_type_char, _In_ SQLSMALLINT buffer_length, _Out_opt_ SQLSMALLINT* out_buffer_length, _Out_opt_ SQLLEN* field_type_num TSRMLS_DC ) { @@ -2175,9 +2173,9 @@ namespace core { _Out_opt_ SQLSMALLINT* decimal_digits, _Out_opt_ SQLSMALLINT* nullable TSRMLS_DC ) { SQLRETURN r; - r = ::SQLDescribeCol( stmt->handle(), colno, col_name, col_name_length, col_name_length_out, + r = ::SQLDescribeCol( stmt->handle(), colno, col_name, col_name_length, col_name_length_out, data_type, col_size, decimal_digits, nullable); - + CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw CoreException(); } @@ -2220,17 +2218,17 @@ namespace core { inline void SQLEndTran( _In_ SQLSMALLINT handleType, _Inout_ sqlsrv_conn* conn, _In_ SQLSMALLINT completionType TSRMLS_DC ) { SQLRETURN r = ::SQLEndTran( handleType, conn->handle(), completionType ); - + CHECK_SQL_ERROR_OR_WARNING( r, conn ) { throw CoreException(); } } - // SQLExecDirect returns the status code since it returns either SQL_NEED_DATA or SQL_NO_DATA besides just errors/success + // SQLExecDirect returns the status code since it returns either SQL_NEED_DATA or SQL_NO_DATA besides just errors/success inline SQLRETURN SQLExecDirect( _Inout_ sqlsrv_stmt* stmt, _In_ char* sql TSRMLS_DC ) { SQLRETURN r = ::SQLExecDirect( stmt->handle(), reinterpret_cast( sql ), SQL_NTS ); - + check_for_mars_error( stmt, r TSRMLS_CC ); CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { @@ -2258,7 +2256,7 @@ namespace core { { SQLRETURN r; r = ::SQLExecute( stmt->handle() ); - + check_for_mars_error( stmt, r TSRMLS_CC ); CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { @@ -2271,7 +2269,7 @@ namespace core { inline SQLRETURN SQLFetchScroll( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orientation, _In_ SQLLEN fetch_offset TSRMLS_DC ) { SQLRETURN r = ::SQLFetchScroll( stmt->handle(), fetch_orientation, fetch_offset ); - + CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw CoreException(); } @@ -2304,7 +2302,7 @@ namespace core { if( r == SQL_NO_DATA ) return r; - + CHECK_SQL_ERROR( r, stmt ) { throw CoreException(); } @@ -2318,13 +2316,13 @@ namespace core { return r; } - + inline void SQLGetInfo( _Inout_ sqlsrv_conn* conn, _In_ SQLUSMALLINT info_type, _Out_writes_bytes_opt_(buffer_len) SQLPOINTER info_value, _In_ SQLSMALLINT buffer_len, _Out_opt_ SQLSMALLINT* str_len TSRMLS_DC ) { SQLRETURN r; r = ::SQLGetInfo( conn->handle(), info_type, info_value, buffer_len, str_len ); - + CHECK_SQL_ERROR_OR_WARNING( r, conn ) { throw CoreException(); } @@ -2335,7 +2333,7 @@ namespace core { { SQLRETURN r; r = ::SQLGetTypeInfo( stmt->handle(), data_type ); - + CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw CoreException(); } @@ -2363,7 +2361,7 @@ namespace core { CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw CoreException(); } - + return num_cols; } @@ -2405,14 +2403,14 @@ namespace core { SQLLEN rows_affected; r = ::SQLRowCount( stmt->handle(), &rows_affected ); - + // On Linux platform // DriverName: libmsodbcsql-13.0.so.0.0 // DriverODBCVer: 03.52 // DriverVer: 13.00.0000 // unixODBC: 2.3.1 - // r = ::SQLRowCount( stmt->handle(), &rows_affected ); - // returns r=-1 for an empty result set. + // r = ::SQLRowCount( stmt->handle(), &rows_affected ); + // returns r=-1 for an empty result set. #ifndef _WIN32 if( r == -1 && rows_affected == -1 ) return 0; @@ -2430,7 +2428,7 @@ namespace core { { SQLRETURN r; r = ::SQLSetConnectAttr( ctx.handle(), attr, value_ptr, str_len ); - + CHECK_SQL_ERROR_OR_WARNING( r, ctx ) { throw CoreException(); } @@ -2460,13 +2458,13 @@ namespace core { inline void SQLSetConnectAttr( _Inout_ sqlsrv_conn* conn, _In_ SQLINTEGER attribute, _In_reads_bytes_opt_(value_len) SQLPOINTER value_ptr, _In_ SQLINTEGER value_len TSRMLS_DC ) { - SQLRETURN r = ::SQLSetConnectAttr( conn->handle(), attribute, value_ptr, value_len ); - + SQLRETURN r = ::SQLSetConnectAttr( conn->handle(), attribute, value_ptr, value_len ); + CHECK_SQL_ERROR_OR_WARNING( r, conn ) { throw CoreException(); } } - + inline void SQLSetStmtAttr( _Inout_ sqlsrv_stmt* stmt, _In_ SQLINTEGER attr, _In_reads_(str_len) SQLPOINTER value_ptr, _In_ SQLINTEGER str_len TSRMLS_DC ) { SQLRETURN r; @@ -2505,7 +2503,7 @@ namespace core { // If there is a zend function in the source that isn't found here, it is because it returns void and there is no error // that can be thrown from it. - inline void sqlsrv_add_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zend_ulong index, _In_ zval* value TSRMLS_DC) + inline void sqlsrv_add_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zend_ulong index, _In_ zval* value TSRMLS_DC) { int zr = add_index_zval( array, index, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { @@ -2513,7 +2511,7 @@ namespace core { } } - inline void sqlsrv_add_next_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zval* value TSRMLS_DC) + inline void sqlsrv_add_next_index_zval( _Inout_ sqlsrv_context& ctx, _Inout_ zval* array, _In_ zval* value TSRMLS_DC) { int zr = add_next_index_zval( array, value ); CHECK_ZEND_ERROR( zr, ctx, SQLSRV_ERROR_ZEND_HASH ) { @@ -2556,7 +2554,7 @@ namespace core { } } - inline void sqlsrv_array_init( _Inout_ sqlsrv_context& ctx, _Out_ zval* new_array TSRMLS_DC) + inline void sqlsrv_array_init( _Inout_ sqlsrv_context& ctx, _Out_ zval* new_array TSRMLS_DC) { #if PHP_VERSION_ID < 70300 CHECK_ZEND_ERROR(::array_init(new_array), ctx, SQLSRV_ERROR_ZEND_HASH) { @@ -2599,7 +2597,7 @@ namespace core { throw CoreException(); } } - + inline void sqlsrv_zend_hash_index_update( _Inout_ sqlsrv_context& ctx, _Inout_ HashTable* ht, _In_ zend_ulong index, _In_ zval* data_z TSRMLS_DC ) { int zr = (data_z = ::zend_hash_index_update(ht, index, data_z)) != NULL ? SUCCESS : FAILURE; @@ -2624,7 +2622,7 @@ namespace core { throw CoreException(); } } - + inline void sqlsrv_zend_hash_next_index_insert( _Inout_ sqlsrv_context& ctx, _Inout_ HashTable* ht, _In_ zval* data TSRMLS_DC ) { int zr = (data = ::zend_hash_next_index_insert(ht, data)) != NULL ? SUCCESS : FAILURE; @@ -2648,7 +2646,7 @@ namespace core { throw CoreException(); } } - + inline void sqlsrv_zend_hash_init(sqlsrv_context& ctx, _Inout_ HashTable* ht, _Inout_ uint32_t initial_size, _In_ dtor_func_t dtor_fn, _In_ zend_bool persistent TSRMLS_DC ) { diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 090322a5..32b10ad5 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -3,7 +3,7 @@ // // Contents: Core routines that use statement handles shared between sqlsrv and pdo_sqlsrv // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -329,7 +329,7 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm } // The query timeout setting is inherited from the corresponding connection attribute, but - // the user may override that the query timeout setting using the statement option. + // the user may override that the query timeout setting using the statement option. // In any case, set query timeout using the latest value stmt->set_query_timeout(); @@ -850,7 +850,7 @@ bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orient CHECK_CUSTOM_ERROR( stmt->past_fetch_end, stmt, SQLSRV_ERROR_FETCH_PAST_END ) { throw core::CoreException(); } - + // First time only if ( !stmt->fetch_called ) { SQLSMALLINT has_fields; @@ -860,7 +860,7 @@ bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orient has_fields = core::SQLNumResultCols( stmt TSRMLS_CC ); stmt->column_count = has_fields; } - + CHECK_CUSTOM_ERROR( has_fields == 0, stmt, SQLSRV_ERROR_NO_FIELDS ) { throw core::CoreException(); } @@ -1009,7 +1009,7 @@ void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) } // Reference: https://docs.microsoft.com/sql/connect/odbc/data-classification - // To retrieve sensitivity classfication data, the first step is to retrieve the IRD(Implementation Row Descriptor) handle by + // To retrieve sensitivity classfication data, the first step is to retrieve the IRD(Implementation Row Descriptor) handle by // calling SQLGetStmtAttr with SQL_ATTR_IMP_ROW_DESC statement attribute r = ::SQLGetStmtAttr(stmt->handle(), SQL_ATTR_IMP_ROW_DESC, (SQLPOINTER)&ird, SQL_IS_POINTER, 0); CHECK_SQL_ERROR_OR_WARNING(r, stmt) { @@ -1074,7 +1074,7 @@ void core_sqlsrv_sensitivity_metadata( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) CHECK_CUSTOM_ERROR(dcptr != dcend, stmt, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, "Metadata parsing ends unexpectedly") { throw core::CoreException(); } - + stmt->current_sensitivity_metadata = sensitivity_meta; sensitivity_meta.transferred(); } catch (core::CoreException& e) { @@ -1172,7 +1172,7 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i // use the previously saved php type sqlsrv_php_type = stmt->current_meta_data[field_index]->sqlsrv_php_type; } - } + } // Verify that we have an acceptable type to convert. CHECK_CUSTOM_ERROR(!is_valid_sqlsrv_phptype(sqlsrv_php_type), stmt, SQLSRV_ERROR_INVALID_TYPE) { @@ -1209,7 +1209,7 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) { SQLSMALLINT num_cols; SQLLEN rows_affected; - + if (stmt->column_count != ACTIVE_NUM_COLS_INVALID) { num_cols = stmt->column_count; } @@ -1218,7 +1218,7 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); stmt->column_count = num_cols; } - + if (stmt->row_count != ACTIVE_NUM_ROWS_INVALID) { rows_affected = stmt->row_count; } @@ -1227,7 +1227,7 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) rows_affected = core::SQLRowCount( stmt TSRMLS_CC ); stmt->row_count = rows_affected; } - + return (num_cols != 0) || (rows_affected > 0); } @@ -1266,7 +1266,7 @@ void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC, _In_ bool fin if( r == SQL_NO_DATA ) { - if( &(stmt->output_params) && finalize_output_params ) { + if( finalize_output_params ) { // if we're finished processing result sets, handle the output parameters finalize_output_parameters( stmt TSRMLS_CC ); } @@ -1416,7 +1416,7 @@ void core_sqlsrv_set_decimal_places(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_ } stmt->decimal_places = static_cast(decimal_places); - } + } catch( core::CoreException& ) { throw; } @@ -1858,7 +1858,7 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i // Reference: https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-timestamp // Retrieve the datetime data as a string, which may be cached for later use. - // The string is converted to a DateTime object only when it is required to + // The string is converted to a DateTime object only when it is required to // be returned as a zval. case SQLSRV_PHPTYPE_DATETIME: { @@ -1885,7 +1885,7 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i break; } - + // create a stream wrapper around the field and return that object to the PHP script. calls to fread // on the stream will result in calls to SQLGetData. This is handled in stream.cpp. See that file // for how these fields are used. @@ -2012,7 +2012,7 @@ bool convert_input_param_to_utf16( _In_ zval* input_param_z, _Inout_ zval* conve // conversion, to avoid the performance penalty of calling ToUtf16 wchar_size = buffer_len; #else - // Calculate the size of the necessary buffer from the length of the string - + // Calculate the size of the necessary buffer from the length of the string - // no performance penalty because MultiByteToWidechar is highly optimised wchar_size = MultiByteToWideChar( CP_UTF8, MB_ERR_INVALID_CHARS, reinterpret_cast( buffer ), static_cast( buffer_len ), NULL, 0 ); #endif // !_WIN32 @@ -2275,7 +2275,7 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f // number of decimals adheres to the column field scale. If smaller, the output value may be rounded up. // // Note: it's possible that the decimal data does not contain a decimal dot because the field scale is 0. - // Thus, first check if the decimal dot exists. If not, no formatting necessary, regardless of + // Thus, first check if the decimal dot exists. If not, no formatting necessary, regardless of // format_decimals and decimals_places // std::string str = field_value; @@ -2292,8 +2292,8 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f } // We want the rounding to be consistent with php number_format(), http://php.net/manual/en/function.number-format.php - // as well as SQL Server Management studio, such that the least significant digit will be rounded up if it is - // followed by 5 or above. + // as well as SQL Server Management studio, such that the least significant digit will be rounded up if it is + // followed by 5 or above. bool isNegative = false; @@ -2313,16 +2313,16 @@ void format_decimal_numbers(_In_ SQLSMALLINT decimals_places, _In_ SQLSMALLINT f str = oss.str(); pos++; } - + if (num_decimals == NO_CHANGE_DECIMAL_PLACES) { // Add the minus sign back if negative if (isNegative) { std::ostringstream oss; oss << '-' << str.substr(0); str = oss.str(); - } + } } else { - // Start formatting + // Start formatting size_t last = 0; if (num_decimals == 0) { // Chop all decimal digits, including the decimal dot @@ -2532,7 +2532,7 @@ void finalize_output_parameters( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) throw core::CoreException(); } } - // if the output param is a boolean, still convert to + // if the output param is a boolean, still convert to // a long integer first to take care of rounding convert_to_long(value_z); if (output_param->is_bool) { @@ -2911,9 +2911,9 @@ void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* // account for the NULL terminator returned by ODBC and needed by Zend to avoid a "String not null terminated" debug warning SQLULEN field_size = column_size; - // with AE on, when column_size is retrieved from SQLDescribeParam, column_size + // with AE on, when column_size is retrieved from SQLDescribeParam, column_size // does not include the negative sign or decimal place for numeric values - // VSO Bug 2913: without AE, the same can happen as well, in particular to decimals + // VSO Bug 2913: without AE, the same can happen as well, in particular to decimals // and numerics with precision/scale specified if (sql_type == SQL_DECIMAL || sql_type == SQL_NUMERIC || sql_type == SQL_BIGINT || sql_type == SQL_INTEGER || sql_type == SQL_SMALLINT) { // include the possible negative sign @@ -2948,8 +2948,8 @@ void resize_output_buffer_if_necessary( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* // A zval string len doesn't include the null. This calculates the length it should be // regardless of whether the ODBC type contains the NULL or not. - // initialize the newly allocated space - char *p = ZSTR_VAL(param_z_string); + // initialize the newly allocated space + char *p = ZSTR_VAL(param_z_string); p = p + original_len; memset(p, '\0', expected_len - original_len); ZVAL_NEW_STR(param_z, param_z_string); diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index 9ca8e8c0..e0177652 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -3,7 +3,7 @@ // // Contents: Implementation of PHP streams for reading SQL Server data // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 9f2e0eea..2853a22a 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/globalization.h b/source/shared/globalization.h index f7e1afd3..81091fc5 100644 --- a/source/shared/globalization.h +++ b/source/shared/globalization.h @@ -4,7 +4,7 @@ // Contents: Contains functions for handling Windows format strings // and UTF-16 on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic.h b/source/shared/interlockedatomic.h index 12456143..c2ae4d26 100644 --- a/source/shared/interlockedatomic.h +++ b/source/shared/interlockedatomic.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedatomic_gcc.h b/source/shared/interlockedatomic_gcc.h index 171c1ad2..9eff99f5 100644 --- a/source/shared/interlockedatomic_gcc.h +++ b/source/shared/interlockedatomic_gcc.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, atomic // operations on int32_t and pointer types. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/interlockedslist.h b/source/shared/interlockedslist.h index 4bc04c1f..8def1dc2 100644 --- a/source/shared/interlockedslist.h +++ b/source/shared/interlockedslist.h @@ -4,7 +4,7 @@ // Contents: Contains a portable abstraction for interlocked, singly // linked list. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index cb624951..fae23e2f 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -3,7 +3,7 @@ // // Contents: Contains portable classes for localization // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index 0e733826..6a69aaa8 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -5,7 +5,7 @@ // Must be included in one c/cpp file per binary // A build error will occur if this inclusion policy is not followed // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index b0496f5d..bcca3087 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -20,7 +20,7 @@ // pecuniary loss) arising out of the use of or inability to use // this SDK, even if Microsoft has been advised of the possibility // of such damages. -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/sal_def.h b/source/shared/sal_def.h index 7d4efb17..7b191eb7 100644 --- a/source/shared/sal_def.h +++ b/source/shared/sal_def.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/typedefs_for_linux.h b/source/shared/typedefs_for_linux.h index 82e33eba..b4fb63a6 100644 --- a/source/shared/typedefs_for_linux.h +++ b/source/shared/typedefs_for_linux.h @@ -1,7 +1,7 @@ //--------------------------------------------------------------------------------------------------------------------------------- // File: typedefs_for_linux.h // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/version.h b/source/shared/version.h index c875410d..4369a3c8 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -4,7 +4,7 @@ // File: version.h // Contents: Version number constants // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -26,12 +26,12 @@ // Increase Minor with backward compatible new functionalities and API changes. // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 -#define SQLVERSION_MINOR 7 -#define SQLVERSION_PATCH 1 +#define SQLVERSION_MINOR 8 +#define SQLVERSION_PATCH 0 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 -#define PREVIEW 1 +#define PREVIEW 0 #define SEMVER_PRERELEASE // Semantic versioning build metadata, build meta data is not counted in precedence order. diff --git a/source/shared/xplat.h b/source/shared/xplat.h index a32cdda0..dbf862b1 100644 --- a/source/shared/xplat.h +++ b/source/shared/xplat.h @@ -3,7 +3,7 @@ // // Contents: include for definition of Windows types for non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_intsafe.h b/source/shared/xplat_intsafe.h index 90d2f150..e64e4c51 100644 --- a/source/shared/xplat_intsafe.h +++ b/source/shared/xplat_intsafe.h @@ -4,7 +4,7 @@ // Contents: This module defines helper functions to prevent // integer overflow bugs. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winerror.h b/source/shared/xplat_winerror.h index 0ebd1f2a..8ae8ee40 100644 --- a/source/shared/xplat_winerror.h +++ b/source/shared/xplat_winerror.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/shared/xplat_winnls.h b/source/shared/xplat_winnls.h index 22bfedc9..f6d75a61 100644 --- a/source/shared/xplat_winnls.h +++ b/source/shared/xplat_winnls.h @@ -3,7 +3,7 @@ // // Contents: Contains the minimal definitions to build on non-Windows platforms // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index a8a4e89d..aa460ef9 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -4,7 +4,7 @@ dnl dnl Contents: the code that will go into the configure script, indicating options, dnl external libraries and includes, and what source files are to be compiled. dnl -dnl Microsoft Drivers 5.7 for PHP for SQL Server +dnl Microsoft Drivers 5.8 for PHP for SQL Server dnl Copyright(c) Microsoft Corporation dnl All rights reserved. dnl MIT License diff --git a/source/sqlsrv/config.w32 b/source/sqlsrv/config.w32 index 33cac9bd..57cae3e7 100644 --- a/source/sqlsrv/config.w32 +++ b/source/sqlsrv/config.w32 @@ -3,7 +3,7 @@ // // Contents: JScript build configuration used by buildconf.bat // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index 5500da23..c4ed6362 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -3,7 +3,7 @@ // // Contents: Routines that use connection handles // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index 2b394862..bc069205 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -2,7 +2,7 @@ // File: init.cpp // Contents: initialization routines for the extension // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -654,6 +654,25 @@ PHP_RINIT_FUNCTION(sqlsrv) SQLSRV_G( log_subsystems ) = INI_INT( subsystems ); SQLSRV_G( buffered_query_limit ) = INI_INT( buffered_limit ); +#ifndef _WIN32 + char set_locale_info[] = INI_PREFIX INI_SET_LOCALE_INFO; + SQLSRV_G(set_locale_info) = INI_INT(set_locale_info); + + // if necessary, set locale from the environment for ODBC, which MUST be done before any connection + int set_locale = SQLSRV_G(set_locale_info); + if (set_locale == 2) { + setlocale(LC_ALL, ""); + } + else if (set_locale == 1) { + setlocale(LC_CTYPE, ""); + } + else { + // Do nothing + } + + LOG(SEV_NOTICE, INI_PREFIX INI_SET_LOCALE_INFO " = %1!d!", set_locale); +#endif + LOG( SEV_NOTICE, INI_PREFIX INI_WARNINGS_RETURN_AS_ERRORS " = %1!s!", SQLSRV_G( warnings_return_as_errors ) ? "On" : "Off"); LOG( SEV_NOTICE, INI_PREFIX INI_LOG_SEVERITY " = %1!d!", SQLSRV_G( log_severity )); LOG( SEV_NOTICE, INI_PREFIX INI_LOG_SUBSYSTEMS " = %1!d!", SQLSRV_G( log_subsystems )); diff --git a/source/sqlsrv/php_sqlsrv.h b/source/sqlsrv/php_sqlsrv.h index 36bb9a95..45fb8bae 100644 --- a/source/sqlsrv/php_sqlsrv.h +++ b/source/sqlsrv/php_sqlsrv.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -42,6 +42,10 @@ zend_long current_subsystem; zend_bool warnings_return_as_errors; zend_long buffered_query_limit; +#ifndef _WIN32 +zend_long set_locale_info; +#endif + ZEND_END_MODULE_GLOBALS(sqlsrv) ZEND_EXTERN_MODULE_GLOBALS(sqlsrv); diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index 42148b03..9ca59d9e 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -8,7 +8,7 @@ // // Comments: Also contains "internal" declarations shared across source files. // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License @@ -37,6 +37,10 @@ #define INI_BUFFERED_QUERY_LIMIT "ClientBufferMaxKBSize" #define INI_PREFIX "sqlsrv." +#ifndef _WIN32 +#define INI_SET_LOCALE_INFO "SetLocaleInfo" +#endif + PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN( INI_PREFIX INI_WARNINGS_RETURN_AS_ERRORS , "1", PHP_INI_ALL, OnUpdateBool, warnings_return_as_errors, zend_sqlsrv_globals, sqlsrv_globals ) @@ -46,6 +50,11 @@ PHP_INI_BEGIN() sqlsrv_globals ) STD_PHP_INI_ENTRY( INI_PREFIX INI_BUFFERED_QUERY_LIMIT, INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, buffered_query_limit, zend_sqlsrv_globals, sqlsrv_globals ) +#ifndef _WIN32 + STD_PHP_INI_ENTRY(INI_PREFIX INI_SET_LOCALE_INFO, "2", PHP_INI_ALL, OnUpdateLong, set_locale_info, + zend_sqlsrv_globals, sqlsrv_globals) +#endif + PHP_INI_END() diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index b67af51a..05d46b6c 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -3,17 +3,17 @@ // // Contents: Routines that use statement handles // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), -// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files(the ""Software""), +// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, // and / or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions : // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //--------------------------------------------------------------------------------------------------------------------------------- @@ -85,11 +85,6 @@ const char* NULLABLE = "Nullable"; } -// warning message printed when a parameter variable is not passed by reference -const char SS_SQLSRV_WARNING_PARAM_VAR_NOT_REF[] = "Variable parameter %d not passed by reference (prefaced with an &). " - "Variable parameters passed to sqlsrv_prepare or sqlsrv_query should be passed by reference, not by value. " - "For more information, see sqlsrv_prepare or sqlsrv_query in the API Reference section of the product documentation."; - /* internal functions */ void convert_to_zval( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _In_opt_ void* in_val, _In_ SQLLEN field_len, _Inout_ zval& out_zval ); @@ -103,7 +98,7 @@ void determine_stmt_has_rows( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ); bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ); bool is_valid_sqlsrv_sqltype( _In_ sqlsrv_sqltype type ); void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, zend_ulong index, _Out_ SQLSMALLINT& direction, - _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, + _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits TSRMLS_DC ); void type_and_encoding( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ); void type_and_size_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ); @@ -156,7 +151,7 @@ ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void ) if( fetch_field_names != NULL ) { for( int i=0; i < fetch_fields_count; ++i ) { - + sqlsrv_free( fetch_field_names[i].name ); } sqlsrv_free( fetch_field_names ); @@ -165,16 +160,16 @@ ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void ) zval_ptr_dtor( params_z ); sqlsrv_free(params_z); } -} +} // to be called whenever a new result set is created, such as after an // execute or next_result. Resets the state variables and calls the subclass. -void ss_sqlsrv_stmt::new_result_set( TSRMLS_D ) +void ss_sqlsrv_stmt::new_result_set( TSRMLS_D ) { if( fetch_field_names != NULL ) { for( int i=0; i < fetch_fields_count; ++i ) { - + sqlsrv_free( fetch_field_names[i].name ); } sqlsrv_free( fetch_field_names ); @@ -185,7 +180,7 @@ void ss_sqlsrv_stmt::new_result_set( TSRMLS_D ) sqlsrv_stmt::new_result_set( TSRMLS_C ); } -// Returns a php type for a given sql type. Also sets the encoding wherever applicable. +// Returns a php type for a given sql type. Also sets the encoding wherever applicable. sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ) { sqlsrv_phptype ss_phptype; @@ -299,13 +294,13 @@ void ss_sqlsrv_stmt::set_query_timeout() } // sqlsrv_execute( resource $stmt ) -// +// // Executes a previously prepared statement. See sqlsrv_prepare for information // on preparing a statement for execution. -// +// // This function is ideal for executing a prepared statement multiple times with // different parameter values. See the MSDN documentation -// +// // Parameters // $stmt: A resource specifying the statement to be executed. For more // information about how to create a statement resource, see sqlsrv_prepare. @@ -316,12 +311,12 @@ void ss_sqlsrv_stmt::set_query_timeout() PHP_FUNCTION( sqlsrv_execute ) { LOG_FUNCTION( "sqlsrv_execute" ); - + ss_sqlsrv_stmt* stmt = NULL; - + try { - PROCESS_PARAMS( stmt, "r", _FN_, 0 ); + PROCESS_PARAMS( stmt, "r", _FN_, 0 ); CHECK_CUSTOM_ERROR(( !stmt->prepared ), stmt, SS_SQLSRV_ERROR_STATEMENT_NOT_PREPARED ) { throw ss::SSException(); } @@ -340,11 +335,11 @@ PHP_FUNCTION( sqlsrv_execute ) bind_params( stmt TSRMLS_CC ); core_sqlsrv_execute( stmt TSRMLS_CC ); - + RETURN_TRUE; } catch( core::CoreException& ) { - + RETURN_FALSE; } catch( ... ) { @@ -382,8 +377,8 @@ PHP_FUNCTION( sqlsrv_fetch ) PROCESS_PARAMS( stmt, "r|ll", _FN_, 2, &fetch_style, &fetch_offset ); try { - - CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, + + CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, SS_SQLSRV_ERROR_INVALID_FETCH_STYLE ) { throw ss::SSException(); } @@ -406,7 +401,7 @@ PHP_FUNCTION( sqlsrv_fetch ) } // sqlsrv_fetch_array( resource $stmt [, int $fetchType] ) -// +// // Retrieves the next row of data as an array. // // Parameters @@ -425,7 +420,7 @@ PHP_FUNCTION( sqlsrv_fetch ) PHP_FUNCTION( sqlsrv_fetch_array ) { LOG_FUNCTION( "sqlsrv_fetch_array" ); - + ss_sqlsrv_stmt* stmt = NULL; zend_long fetch_type = SQLSRV_FETCH_BOTH; // default value for parameter if one isn't supplied zend_long fetch_style = SQL_FETCH_NEXT; // default value for parameter if one isn't supplied @@ -436,13 +431,13 @@ PHP_FUNCTION( sqlsrv_fetch_array ) PROCESS_PARAMS( stmt, "r|lll", _FN_, 3, &fetch_type, &fetch_style, &fetch_offset ); try { - - CHECK_CUSTOM_ERROR(( fetch_type < MIN_SQLSRV_FETCH || fetch_type > MAX_SQLSRV_FETCH ), stmt, + + CHECK_CUSTOM_ERROR(( fetch_type < MIN_SQLSRV_FETCH || fetch_type > MAX_SQLSRV_FETCH ), stmt, SS_SQLSRV_ERROR_INVALID_FETCH_TYPE ) { throw ss::SSException(); } - CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, + CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, SS_SQLSRV_ERROR_INVALID_FETCH_STYLE ) { throw ss::SSException(); } @@ -467,7 +462,7 @@ PHP_FUNCTION( sqlsrv_fetch_array ) } // sqlsrv_field_metadata( resource $stmt ) -// +// // Retrieves metadata for the fields of a prepared statement. For information // about preparing a statement, see sqlsrv_query or sqlsrv_prepare. Note that // sqlsrv_field_metadata can be called on any prepared statement, pre- or @@ -477,7 +472,7 @@ PHP_FUNCTION( sqlsrv_fetch_array ) // $stmt: A statement resource for which field metadata is sought. // // Return Value -// retrieve an array of metadata for the current result set on a statement. Each element of the +// retrieve an array of metadata for the current result set on a statement. Each element of the // array is a sub-array containing 5 elements accessed by key: // name - name of the field. // type - integer of the type. Can be compared against the SQLSRV_SQLTYPE constants. @@ -507,10 +502,10 @@ PHP_FUNCTION( sqlsrv_field_metadata ) zval result_meta_data; ZVAL_UNDEF( &result_meta_data ); core::sqlsrv_array_init( *stmt, &result_meta_data TSRMLS_CC ); - + for( SQLSMALLINT f = 0; f < num_cols; ++f ) { field_meta_data* core_meta_data = stmt->current_meta_data[f]; - + // initialize the array zval field_array; ZVAL_UNDEF( &field_array ); @@ -555,7 +550,7 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // add the nullability to the array core::sqlsrv_add_assoc_long( *stmt, &field_array, FieldMetaData::NULLABLE, core_meta_data->field_is_nullable TSRMLS_CC ); - + if (stmt->data_classification) { data_classification::fill_column_sensitivity_array(stmt, f, &field_array TSRMLS_CC); } @@ -580,7 +575,7 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // sqlsrv_next_result( resource $stmt ) -// +// // Makes the next result (result set, row count, or output parameter) of the // specified statement active. The first (or only) result returned by a batch // query or stored procedure is active without a call to sqlsrv_next_result. @@ -619,7 +614,7 @@ PHP_FUNCTION( sqlsrv_next_result ) RETURN_TRUE; } catch( core::CoreException& ) { - + RETURN_FALSE; } catch( ... ) { @@ -708,7 +703,7 @@ PHP_FUNCTION( sqlsrv_num_rows ) // make sure that the statement is scrollable and the cursor is not dynamic. // if the cursor is dynamic, then the number of rows returned is always -1. - CHECK_CUSTOM_ERROR( stmt->cursor_type == SQL_CURSOR_FORWARD_ONLY || stmt->cursor_type == SQL_CURSOR_DYNAMIC, stmt, + CHECK_CUSTOM_ERROR( stmt->cursor_type == SQL_CURSOR_FORWARD_ONLY || stmt->cursor_type == SQL_CURSOR_DYNAMIC, stmt, SS_SQLSRV_ERROR_STATEMENT_NOT_SCROLLABLE ) { throw ss::SSException(); } @@ -750,10 +745,10 @@ PHP_FUNCTION( sqlsrv_num_fields ) PROCESS_PARAMS( stmt, "r", _FN_, 0 ); try { - + // retrieve the number of columns from ODBC fields = core::SQLNumResultCols( stmt TSRMLS_CC ); - + RETURN_LONG( fields ); } @@ -768,7 +763,7 @@ PHP_FUNCTION( sqlsrv_num_fields ) } // sqlsrv_fetch_object( resource $stmt [, string $className [, array $ctorParams]]) -// +// // Retrieves the next row of data as a PHP object. // // Parameters @@ -801,7 +796,7 @@ PHP_FUNCTION( sqlsrv_num_fields ) // object and the result set value is applied to the property. For more // information about calling sqlsrv_fetch_object with the $className parameter, // see How to: Retrieve Data as an Object (Microsoft Drivers for PHP for SQL Server). -// +// // If a field with no name is returned, sqlsrv_fetch_object will discard the // field value and issue a warning. @@ -824,19 +819,19 @@ PHP_FUNCTION( sqlsrv_fetch_object ) // retrieve the statement resource and optional fetch type (see enum SQLSRV_FETCH_TYPE), // fetch style (see SQLSRV_SCROLL_* constants) and fetch offset - // we also use z! instead of s and a so that null may be passed in as valid values for + // we also use z! instead of s and a so that null may be passed in as valid values for // the class name and ctor params PROCESS_PARAMS( stmt, "r|z!z!ll", _FN_, 4, &class_name_z, &ctor_params_z, &fetch_style, &fetch_offset ); - + try { - - CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, + + CHECK_CUSTOM_ERROR(( fetch_style < SQL_FETCH_NEXT || fetch_style > SQL_FETCH_RELATIVE ), stmt, SS_SQLSRV_ERROR_INVALID_FETCH_STYLE ) { throw ss::SSException(); } if( class_name_z ) { - + CHECK_CUSTOM_ERROR(( Z_TYPE_P( class_name_z ) != IS_STRING ), stmt, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { throw ss::SSException(); } @@ -847,7 +842,7 @@ PHP_FUNCTION( sqlsrv_fetch_object ) if( ctor_params_z && Z_TYPE_P( ctor_params_z ) != IS_ARRAY ) { THROW_SS_ERROR( stmt, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ); } - + // fetch the data bool result = core_sqlsrv_fetch( stmt, static_cast(fetch_style), fetch_offset TSRMLS_CC ); if( !result ) { @@ -855,8 +850,8 @@ PHP_FUNCTION( sqlsrv_fetch_object ) } fetch_fields_common( stmt, SQLSRV_FETCH_ASSOC, retval_z, false /*allow_empty_field_names*/ TSRMLS_CC ); - properties_ht = Z_ARRVAL( retval_z ); - + properties_ht = Z_ARRVAL( retval_z ); + // find the zend_class_entry of the class the user requested (stdClass by default) for use below zend_class_entry* class_entry = NULL; zend_string* class_name_str_z = zend_string_init( class_name, class_name_len, 0 ); @@ -885,13 +880,13 @@ PHP_FUNCTION( sqlsrv_fetch_object ) // find and call the object's constructor // The header files (zend.h and zend_API.h) declare - // these functions and structures, so by working with those, we were able to + // these functions and structures, so by working with those, we were able to // develop this as a suitable snippet for calling constructors. Some observations: // params must be an array of zval**, not a zval** to an array as we originally // thought. Also, a constructor doesn't show up in the function table, but // is put into the "magic methods" section of the class entry. - // - // The default values of the fci and fcic structures were determined by + // + // The default values of the fci and fcic structures were determined by // calling zend_fcall_info_init with a test callable. // if there is a constructor (e.g., stdClass doesn't have one) @@ -919,7 +914,7 @@ PHP_FUNCTION( sqlsrv_fetch_object ) i++; } ZEND_HASH_FOREACH_END(); } //if( !Z_ISUNDEF( ctor_params_z )) - + // call the constructor function itself. zend_fcall_info fci; zend_fcall_info_cache fcic; @@ -933,7 +928,7 @@ PHP_FUNCTION( sqlsrv_fetch_object ) fci.retval = &ctor_retval_z; fci.param_count = num_params; fci.params = params_m; // purposefully not transferred since ownership isn't actually transferred. - + fci.object = Z_OBJ_P( &retval_z ); memset( &fcic, 0, sizeof( fcic )); @@ -950,7 +945,7 @@ PHP_FUNCTION( sqlsrv_fetch_object ) throw ss::SSException(); } - } //if( class_entry->constructor ) + } //if( class_entry->constructor ) RETURN_ZVAL( &retval_z, 1, 1 ); } @@ -985,7 +980,7 @@ PHP_FUNCTION( sqlsrv_fetch_object ) // for using a function like this: // 1) To know if there are any actual rows, not just a result set (empty or not). Use sqlsrv_has_rows to determine this. // The guarantee is that if sqlsrv_has_rows returns true immediately after a query, that sqlsrv_fetch_* will return at least -// one row of data. +// one row of data. // 2) To know if there is any sort of result set, empty or not, that has to be bypassed to get to something else, such as // output parameters being returned. Use sqlsrv_num_fields > 0 to check if there is any result set that must be bypassed // until sqlsrv_fetch returns NULL. @@ -1048,8 +1043,8 @@ PHP_FUNCTION( sqlsrv_has_rows ) PHP_FUNCTION( sqlsrv_send_stream_data ) { sqlsrv_stmt* stmt = NULL; - - LOG_FUNCTION( "sqlsrv_send_stream_data" ); + + LOG_FUNCTION( "sqlsrv_send_stream_data" ); // get the statement resource that we've bound streams to PROCESS_PARAMS( stmt, "r", _FN_, 0 ); @@ -1079,14 +1074,14 @@ PHP_FUNCTION( sqlsrv_send_stream_data ) RETURN_FALSE; } catch( ... ) { - + DIE( "sqlsrv_send_stream_data: Unknown exception caught." ); } } // sqlsrv_get_field( resource $stmt, int $fieldIndex [, int $getAsType] ) -// +// // Retrieves data from the specified field of the current row. Field data must // be accessed in order. For example, data from the first field cannot be // accessed after data from the second field has been accessed. @@ -1111,7 +1106,7 @@ PHP_FUNCTION( sqlsrv_send_stream_data ) PHP_FUNCTION( sqlsrv_get_field ) { LOG_FUNCTION( "sqlsrv_get_field" ); - + ss_sqlsrv_stmt* stmt = NULL; sqlsrv_phptype sqlsrv_php_type; sqlsrv_php_type.typeinfo.type = SQLSRV_PHPTYPE_INVALID; @@ -1121,7 +1116,7 @@ PHP_FUNCTION( sqlsrv_get_field ) SQLLEN field_len = -1; zval retval_z; ZVAL_UNDEF(&retval_z); - + // get the statement, the field index and the optional type PROCESS_PARAMS( stmt, "rl|l", _FN_, 2, &field_index, &sqlsrv_php_type ); @@ -1136,7 +1131,7 @@ PHP_FUNCTION( sqlsrv_get_field ) core_sqlsrv_get_field( stmt, static_cast( field_index ), sqlsrv_php_type, false, field_value, &field_len, false/*cache_field*/, &sqlsrv_php_type_out TSRMLS_CC ); - convert_to_zval( stmt, sqlsrv_php_type_out, field_value, field_len, retval_z ); + convert_to_zval( stmt, sqlsrv_php_type_out, field_value, field_len, retval_z ); sqlsrv_free( field_value ); RETURN_ZVAL( &retval_z, 1, 1 ); } @@ -1232,9 +1227,9 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) stmt->executed = false; zval* params_z = stmt->params_z; - + HashTable* params_ht = Z_ARRVAL_P( params_z ); - + zend_ulong index = -1; zend_string *key = NULL; zval* param_z = NULL; @@ -1256,7 +1251,7 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) CHECK_CUSTOM_ERROR( type != HASH_KEY_IS_LONG, stmt, SS_SQLSRV_ERROR_PARAM_INVALID_INDEX ) { throw ss::SSException(); } - + // if it's a parameter array if( Z_TYPE_P( param_z ) == IS_ARRAY ) { @@ -1279,7 +1274,7 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) } // bind the parameter SQLSRV_ASSERT( value_z != NULL, "bind_params: value_z is null." ); - core_sqlsrv_bind_param( stmt, static_cast( index ), direction, value_z, php_out_type, encoding, sql_type, column_size, + core_sqlsrv_bind_param( stmt, static_cast( index ), direction, value_z, php_out_type, encoding, sql_type, column_size, decimal_digits TSRMLS_CC ); } ZEND_HASH_FOREACH_END(); @@ -1294,7 +1289,7 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) } // sqlsrv_cancel( resource $stmt ) -// +// // Cancels a statement. This means that any pending results for the statement // are discarded. After this function is called, the statement can be // re-executed if it was prepared with sqlsrv_prepare. Calling this function is @@ -1313,12 +1308,12 @@ PHP_FUNCTION( sqlsrv_cancel ) LOG_FUNCTION( "sqlsrv_cancel" ); ss_sqlsrv_stmt* stmt = NULL; PROCESS_PARAMS( stmt, "r", _FN_, 0 ); - + try { // close the stream to release the resource close_active_stream( stmt TSRMLS_CC ); - + SQLRETURN r = SQLCancel( stmt->handle() ); CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { throw ss::SSException(); @@ -1360,7 +1355,7 @@ void __cdecl sqlsrv_stmt_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ) // cannot be used again after this function has been called. // // Parameters -// $stmt: The statement to be closed. +// $stmt: The statement to be closed. // // Return Value // The Boolean value true unless the function is called with an invalid @@ -1384,7 +1379,7 @@ PHP_FUNCTION( sqlsrv_free_stmt ) sqlsrv_context_auto_ptr error_ctx; reset_errors( TSRMLS_C ); - + try { // dummy context to pass to the error handler @@ -1393,14 +1388,14 @@ PHP_FUNCTION( sqlsrv_free_stmt ) // take only the statement resource if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "r", &stmt_r ) == FAILURE ) { - + // Check if it was a zval int zr = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "z", &stmt_r ); CHECK_CUSTOM_ERROR(( zr == FAILURE ), error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { throw ss::SSException(); - } - + } + if( Z_TYPE_P( stmt_r ) == IS_NULL ) { RETURN_TRUE; @@ -1413,19 +1408,19 @@ PHP_FUNCTION( sqlsrv_free_stmt ) // verify the resource so we know we're deleting a statement stmt = static_cast(zend_fetch_resource_ex(stmt_r TSRMLS_CC, ss_sqlsrv_stmt::resource_name, ss_sqlsrv_stmt::descriptor)); - + // if sqlsrv_free_stmt was called on an already closed statment then we just return success. // zend_list_close sets the type of the closed statment to -1. SQLSRV_ASSERT( stmt_r != NULL, "sqlsrv_free_stmt: stmt_r is null." ); if ( Z_RES_TYPE_P( stmt_r ) == RSRC_INVALID_TYPE ) { RETURN_TRUE; } - + if( stmt == NULL ) { THROW_CORE_ERROR( error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ); } - + // delete the resource from Zend's master list, which will trigger the statement's destructor if( zend_list_close( Z_RES_P(stmt_r) ) == FAILURE ) { LOG( SEV_ERROR, "Failed to remove stmt resource %1!d!", Z_RES_P( stmt_r )->handle); @@ -1436,17 +1431,17 @@ PHP_FUNCTION( sqlsrv_free_stmt ) // zend_list_close only destroy the resource pointed to by Z_RES_P( stmt_r ), not the zend_resource itself Z_TRY_DELREF_P(stmt_r); ZVAL_NULL( stmt_r ); - + RETURN_TRUE; - + } catch( core::CoreException& ) { - + RETURN_FALSE; } - + catch( ... ) { - + DIE( "sqlsrv_free_stmt: Unknown exception caught." ); } } @@ -1456,16 +1451,16 @@ void stmt_option_ss_scrollable:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_opt CHECK_CUSTOM_ERROR(( Z_TYPE_P( value_z ) != IS_STRING ), stmt, SQLSRV_ERROR_INVALID_OPTION_SCROLLABLE ) { throw ss::SSException(); } - + const char* scroll_type = Z_STRVAL_P( value_z ); unsigned long cursor_type = -1; - + // find which cursor type they would like and set the ODBC statement attribute as such - if( !stricmp( scroll_type, SSCursorTypes::QUERY_OPTION_SCROLLABLE_STATIC )) { + if( !stricmp( scroll_type, SSCursorTypes::QUERY_OPTION_SCROLLABLE_STATIC )) { cursor_type = SQL_CURSOR_STATIC; } - + else if( !stricmp( scroll_type, SSCursorTypes::QUERY_OPTION_SCROLLABLE_DYNAMIC )) { cursor_type = SQL_CURSOR_DYNAMIC; @@ -1477,12 +1472,12 @@ void stmt_option_ss_scrollable:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_opt } else if( !stricmp( scroll_type, SSCursorTypes::QUERY_OPTION_SCROLLABLE_FORWARD )) { - + cursor_type = SQL_CURSOR_FORWARD_ONLY; } else if( !stricmp( scroll_type, SSCursorTypes::QUERY_OPTION_SCROLLABLE_BUFFERED )) { - + cursor_type = SQLSRV_CURSOR_BUFFERED; } @@ -1553,7 +1548,7 @@ void convert_to_zval( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_ // put in the column size and scale/decimal digits of the sql server type // these values are taken from the MSDN page at http://msdn2.microsoft.com/en-us/library/ms711786(VS.85).aspx // for SQL_VARBINARY, SQL_VARCHAR, and SQL_WLONGVARCHAR types, see https://msdn.microsoft.com/en-CA/library/ms187993.aspx -bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sqltype sqlsrv_type, _Inout_ SQLULEN* column_size, +bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sqltype sqlsrv_type, _Inout_ SQLULEN* column_size, _Out_ SQLSMALLINT* decimal_digits ) { *decimal_digits = 0; @@ -1608,7 +1603,7 @@ bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sq } break; case SQL_WCHAR: - case SQL_WVARCHAR: + case SQL_WVARCHAR: *column_size = sqlsrv_type.typeinfo.size; if( *column_size == SQLSRV_SIZE_MAX_TYPE ) { *column_size = SQL_SS_LENGTH_UNLIMITED; @@ -1731,7 +1726,7 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ case SQL_SS_TIME2: case SQL_TYPE_TIMESTAMP: { - if (stmt->date_as_string) { + if (stmt->date_as_string) { sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; sqlsrv_phptype.typeinfo.encoding = stmt->encoding(); } @@ -1749,7 +1744,7 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ if( sqlsrv_phptype.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) { sqlsrv_phptype.typeinfo.encoding = stmt->conn->encoding(); } - + return sqlsrv_phptype; } @@ -1793,9 +1788,9 @@ void determine_stmt_has_rows( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) } } else { - + // otherwise, we fetch the first row, but record that we did. sqlsrv_fetch checks this - // flag and simply skips the first fetch, knowing it was already done. It records its own + // flag and simply skips the first fetch, knowing it was already done. It records its own // flags to know if it should fetch on subsequent calls. r = core::SQLFetchScroll( stmt, SQL_FETCH_NEXT, 0 TSRMLS_CC ); @@ -1812,7 +1807,7 @@ SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt * stmt) { // get the numer of columns in the result set SQLSMALLINT num_cols = -1; - + num_cols = stmt->current_meta_data.size(); bool getMetaData = false; @@ -1887,8 +1882,8 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ throw ss::SSException(); } #else - array_init(&fields); -#endif + array_init(&fields); +#endif for( int i = 0; i < num_cols; ++i ) { SQLLEN field_len = -1; @@ -1924,7 +1919,7 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ } } //only addref when the fetch_type is BOTH because this is the only case when fields(hashtable) - //has 2 elements pointing to field. Do not addref if the type is NUMERIC or ASSOC because + //has 2 elements pointing to field. Do not addref if the type is NUMERIC or ASSOC because //fields now only has 1 element pointing to field and we want the ref count to be only 1 if (fetch_type == SQLSRV_FETCH_BOTH) { Z_TRY_ADDREF(field); @@ -1934,7 +1929,7 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ } void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, zend_ulong index, _Out_ SQLSMALLINT& direction, - _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, + _Out_ SQLSRV_PHPTYPE& php_out_type, _Out_ SQLSRV_ENCODING& encoding, _Out_ SQLSMALLINT& sql_type, _Out_ SQLULEN& column_size, _Out_ SQLSMALLINT& decimal_digits TSRMLS_DC ) { @@ -1954,7 +1949,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, // handle the array parameters that contain the value/var, direction, php_type, sql_type zend_hash_internal_pointer_reset_ex( param_ht, &pos ); - if( zend_hash_has_more_elements_ex( param_ht, &pos ) == FAILURE || + if( zend_hash_has_more_elements_ex( param_ht, &pos ) == FAILURE || (var_or_val = zend_hash_get_current_data_ex(param_ht, &pos)) == NULL) { THROW_SS_ERROR( stmt, SS_SQLSRV_ERROR_VAR_REQUIRED, index + 1 ); @@ -1977,7 +1972,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, CHECK_CUSTOM_ERROR( !Z_ISREF_P( var_or_val ) && ( direction == SQL_PARAM_OUTPUT || direction == SQL_PARAM_INPUT_OUTPUT ), stmt, SS_SQLSRV_ERROR_PARAM_VAR_NOT_REF, index + 1 ) { throw ss::SSException(); } - + } else { direction = SQL_PARAM_INPUT; @@ -1986,7 +1981,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, // extract the php type and encoding from the 3rd parameter if ( zend_hash_move_forward_ex( param_ht, &pos ) == SUCCESS && ( temp = zend_hash_get_current_data_ex( param_ht, &pos )) != NULL && Z_TYPE_P( temp ) != IS_NULL ) { - + php_type_param_was_null = false; sqlsrv_phptype sqlsrv_phptype; @@ -1997,7 +1992,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, sqlsrv_phptype.value = Z_LVAL_P( temp ); - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_phptype ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, + CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_phptype ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, index + 1 ) { throw ss::SSException(); @@ -2005,7 +2000,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, php_out_type = static_cast( sqlsrv_phptype.typeinfo.type ); encoding = ( SQLSRV_ENCODING ) sqlsrv_phptype.typeinfo.encoding; - // if the call has a SQLSRV_PHPTYPE_STRING/STREAM('default'), then the stream is in the encoding established + // if the call has a SQLSRV_PHPTYPE_STRING/STREAM('default'), then the stream is in the encoding established // by the connection if( encoding == SQLSRV_ENCODING_DEFAULT ) { encoding = stmt->conn->encoding(); @@ -2013,7 +2008,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, } // set default for php type and encoding if not supplied else { - + php_type_param_was_null = true; if ( Z_ISREF_P( var_or_val )){ @@ -2025,7 +2020,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, encoding = stmt->encoding(); if( encoding == SQLSRV_ENCODING_DEFAULT ) { encoding = stmt->conn->encoding(); - } + } } // get the server type, column size/precision and the decimal digits if provided @@ -2042,12 +2037,12 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, sqlsrv_sql_type.value = Z_LVAL_P( temp ); // since the user supplied this type, make sure it's valid - CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_sqltype( sqlsrv_sql_type ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, + CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_sqltype( sqlsrv_sql_type ), stmt, SQLSRV_ERROR_INVALID_PARAMETER_SQLTYPE, index + 1 ) { throw ss::SSException(); - } - + } + bool size_okay = determine_column_size_or_precision( stmt, sqlsrv_sql_type, &column_size, &decimal_digits ); CHECK_CUSTOM_ERROR( !size_okay, stmt, SS_SQLSRV_ERROR_INVALID_PARAMETER_PRECISION, index + 1 ) { @@ -2076,7 +2071,7 @@ void parse_param_array( _Inout_ ss_sqlsrv_stmt* stmt, _Inout_ zval* param_array, sqlsrv_phptype sqlsrv_phptype; sqlsrv_phptype = determine_sqlsrv_php_type( stmt, sql_type, (SQLUINTEGER)column_size, true ); - + // we DIE here since everything should have been validated already and to return the user an error // for our own logic error would be confusing/misleading. SQLSRV_ASSERT( sqlsrv_phptype.typeinfo.type != PHPTYPE_INVALID, "An invalid php type was returned with (supposed) " @@ -2121,7 +2116,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) case SQLSRV_PHPTYPE_STRING: case SQLSRV_PHPTYPE_STREAM: { - if( type.typeinfo.encoding == SQLSRV_ENCODING_BINARY || type.typeinfo.encoding == SQLSRV_ENCODING_CHAR + if( type.typeinfo.encoding == SQLSRV_ENCODING_BINARY || type.typeinfo.encoding == SQLSRV_ENCODING_CHAR || type.typeinfo.encoding == CP_UTF8 || type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) { return true; } @@ -2152,7 +2147,7 @@ bool is_valid_sqlsrv_sqltype( _In_ sqlsrv_sqltype sql_type ) case SQL_BINARY: case SQL_CHAR: case SQL_WCHAR: - case SQL_WVARCHAR: + case SQL_WVARCHAR: case SQL_VARBINARY: case SQL_VARCHAR: case SQL_DECIMAL: @@ -2202,7 +2197,7 @@ void type_and_size_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) int size = 0; if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "s", &size_p, &size_len ) == FAILURE ) { - + return; } if (size_p) { @@ -2226,7 +2221,7 @@ void type_and_size_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) } int max_size = SQL_SERVER_MAX_FIELD_SIZE; - // size is actually the number of characters, not the number of bytes, so if they ask for a + // size is actually the number of characters, not the number of bytes, so if they ask for a // 2 byte per character type, then we half the maximum size allowed. if( type == SQL_WVARCHAR || type == SQL_WCHAR ) { max_size >>= 1; @@ -2236,7 +2231,7 @@ void type_and_size_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) LOG( SEV_ERROR, "invalid size. size must be > 0 and <= %1!d! characters or 'max'", max_size ); size = SQLSRV_INVALID_SIZE; } - + sqlsrv_sqltype sql_type; sql_type.typeinfo.type = type; sql_type.typeinfo.size = size; @@ -2253,15 +2248,15 @@ void type_and_precision_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) zend_long scale = SQLSRV_INVALID_SCALE; if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "|ll", &prec, &scale ) == FAILURE ) { - + return; } - + if( prec > SQL_SERVER_MAX_PRECISION ) { LOG( SEV_ERROR, "Invalid precision. Precision can't be > 38" ); prec = SQLSRV_INVALID_PRECISION; } - + if( prec < 0 ) { LOG( SEV_ERROR, "Invalid precision. Precision can't be negative" ); prec = SQLSRV_INVALID_PRECISION; @@ -2285,11 +2280,11 @@ void type_and_precision_calc( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) void type_and_encoding( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) { - SQLSRV_ASSERT(( type == SQLSRV_PHPTYPE_STREAM || type == SQLSRV_PHPTYPE_STRING ), "type_and_encoding: Invalid type passed." ); + SQLSRV_ASSERT(( type == SQLSRV_PHPTYPE_STREAM || type == SQLSRV_PHPTYPE_STRING ), "type_and_encoding: Invalid type passed." ); char* encoding_param; size_t encoding_param_len = 0; - + // set the default encoding values to invalid so that // if the encoding isn't validated, it will return the invalid setting. sqlsrv_phptype sqlsrv_php_type; @@ -2297,7 +2292,7 @@ void type_and_encoding( INTERNAL_FUNCTION_PARAMETERS, _In_ int type ) sqlsrv_php_type.typeinfo.encoding = SQLSRV_ENCODING_INVALID; if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "s", &encoding_param, &encoding_param_len ) == FAILURE ) { - + ZVAL_LONG( return_value, sqlsrv_php_type.value ); } diff --git a/source/sqlsrv/template.rc b/source/sqlsrv/template.rc index e95041ae..7aa9c0cf 100644 --- a/source/sqlsrv/template.rc +++ b/source/sqlsrv/template.rc @@ -3,7 +3,7 @@ // // Contents: Version resource // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index 9e545b6a..671f6bf4 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -5,7 +5,7 @@ // // Comments: Mostly error handling and some type handling // -// Microsoft Drivers 5.7 for PHP for SQL Server +// Microsoft Drivers 5.8 for PHP for SQL Server // Copyright(c) Microsoft Corporation // All rights reserved. // MIT License diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index 969d1446..1f5d997b 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -1707,19 +1707,21 @@ function CallProcEx($conn, $procName, $procPrefix, $procArgs, $procValues) function CreateFunc($conn, $funcName, $funcArgs, $retType, $funcCode) { DropFunc($conn, $funcName); - $stmt = sqlsrv_query($conn, "CREATE FUNCTION [$funcName] ($funcArgs) RETURNS $retType AS BEGIN $funcCode END"); - if ($stmt === false) { - FatalError("Failed to create test function"); + try { + $stmt = $conn->query("CREATE FUNCTION [$funcName] ($funcArgs) RETURNS $retType AS BEGIN $funcCode END"); + } catch (PDOException $e) { + echo "Failed to create test function\n"; + var_dump($e); } - sqlsrv_free_stmt($stmt); + unset($stmt); } function DropFunc($conn, $funcName) { - $stmt = sqlsrv_query($conn, "DROP FUNCTION [$funcName]"); - if ($stmt === false) { - } else { - sqlsrv_free_stmt($stmt); + try { + $conn->query("DROP FUNCTION [$funcName]"); + } catch (PDOException $e) { + ; // do nothing } } diff --git a/test/functional/pdo_sqlsrv/pdo_1063_locale_configs.phpt b/test/functional/pdo_sqlsrv/pdo_1063_locale_configs.phpt new file mode 100644 index 00000000..32c35f7a --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1063_locale_configs.phpt @@ -0,0 +1,76 @@ +--TEST-- +GitHub issue 1063 - make setting locale info configurable +--DESCRIPTION-- +This test assumes LC_ALL is 'en_US.UTF-8' and verifies that the users can configure using ini file to set application locale using the system locale or not. This test is valid for Linux and macOS systems only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $file"); + print_r(shell_exec(PHP_BINARY." ".dirname(__FILE__)."/pdo_1063_test_locale.php $val")); + print_r(shell_exec(PHP_BINARY." ".dirname(__FILE__)."/pdo_1063_test_locale.php $val $locale")); +} + +$inifile = PHP_CONFIG_FILE_SCAN_DIR."/99-overrides.ini"; + +$locale1 = strtoupper(PHP_OS) === 'LINUX' ? "en_US.ISO-8859-1" : "en_US.ISO8859-1"; +$locale2 = 'de_DE.UTF-8'; + +runTest(0, $inifile, $locale1); +runTest(1, $inifile, $locale2); +runTest(2, $inifile, $locale2); +?> +--EXPECT-- + +***sqlsrv.SetLocaleInfo = 0 +pdo_sqlsrv.set_locale_info = 0*** + +**Begin** +Amount formatted: 10000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: $10,000.99 +Friday +December +3.14159 +**End** + +***sqlsrv.SetLocaleInfo = 1 +pdo_sqlsrv.set_locale_info = 1*** + +**Begin** +Amount formatted: 10000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: 10.000,99 € +Freitag +Dezember +3,14159 +**End** + +***sqlsrv.SetLocaleInfo = 2 +pdo_sqlsrv.set_locale_info = 2*** + +**Begin** +Amount formatted: $10,000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: 10.000,99 € +Freitag +Dezember +3,14159 +**End** diff --git a/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php b/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php new file mode 100644 index 00000000..b3717449 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1063_test_locale.php @@ -0,0 +1,121 @@ +exec($tsql); +} + +function printMoney($amt, $info) +{ + // The money_format() function is deprecated in PHP 7.4, so use intl NumberFormatter + $loc = setlocale(LC_MONETARY, 0); + $symbol = $info['int_curr_symbol']; + + echo "Amount formatted: "; + if (empty($symbol)) { + echo number_format($amt, 2, '.', ''); + } else { + $fmt = new NumberFormatter($loc, NumberFormatter::CURRENCY); + $fmt->setTextAttribute(NumberFormatter::CURRENCY_CODE, $symbol); + $fmt->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + echo $fmt->format($amt); + } + echo PHP_EOL; +} + +require_once('MsSetup.inc'); + +$setLocaleInfo = ($_SERVER['argv'][1]); +$locale = ($_SERVER['argv'][2] ?? ''); + +echo "**Begin**" . PHP_EOL; + +// Assuming LC_ALL is 'en_US.UTF-8', so is LC_CTYPE +// But default LC_MONETARY varies +$ctype = 'en_US.UTF-8'; +switch ($setLocaleInfo) { + case 0: + case 1: + $m = 'C'; $symbol = ''; $sep = ''; + break; + case 2: + $m = 'en_US.UTF-8'; $symbol = '$'; $sep = ','; + break; + default: + die("Unexpected $setLocaleInfo\n"); + break; +} + +$m1 = setlocale(LC_MONETARY, 0); +if ($m !== $m1) { + echo "Unexpected LC_MONETARY: $m1" . PHP_EOL; +} +$c1 = setlocale(LC_CTYPE, 0); +if ($ctype !== $c1) { + echo "Unexpected LC_CTYPE: $c1" . PHP_EOL; +} + +// Set a different locale, if the input is not empty +if (!empty($locale)) { + $loc = setlocale(LC_ALL, $locale); + if ($loc !== $locale) { + echo "Unexpected $loc for LC_ALL " . PHP_EOL; + } + + // Currency symbol and thousands separator in Linux and macOS may be different + if ($loc === 'de_DE.UTF-8') { + $symbol = strtoupper(PHP_OS) === 'LINUX' ? '€' : 'Eu'; + $sep = strtoupper(PHP_OS) === 'LINUX' ? '.' : ''; + } else { + $symbol = '$'; + $sep = ','; + } +} + +$info = localeconv(); +if ($symbol !== $info['currency_symbol']) { + echo "$locale: Expected currency symbol '$symbol' but get '" . $info['currency_symbol'] . "'"; + echo PHP_EOL; +} +if ($sep !== $info['thousands_sep']) { + echo "$locale: Expected thousands separator '$sep' but get '" . $info['currency_symbol'] . "'"; + echo PHP_EOL; +} + +$n1 = 10000.98765; +printMoney($n1, $info); + +echo strftime("%A", strtotime("12/25/2020")) . PHP_EOL; +echo strftime("%B", strtotime("12/25/2020")) . PHP_EOL; + +try { + $conn = new PDO("sqlsrv:server = $server; database=$databaseName; driver=$driver", $uid, $pwd ); + + $tableName = "[" . "pdo1063" . $locale . "]"; + + dropTable($conn, $tableName); + + $pi = "3.14159"; + + $stmt = $conn->query("CREATE TABLE $tableName (c1 FLOAT)"); + $stmt = $conn->query("INSERT INTO $tableName (c1) VALUES ($pi)"); + + $sql = "SELECT c1 FROM $tableName"; + $stmt = $conn->prepare($sql, array(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true)); + $stmt->execute(); + + $row = $stmt->fetch(PDO::FETCH_NUM); + echo ($row[0]) . PHP_EOL; + unset($stmt); + + dropTable($conn, $tableName); + + unset($conn); +} catch( PDOException $e ) { + print_r( $e->getMessage() ); +} + +echo "**End**" . PHP_EOL; +?> \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt b/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt new file mode 100644 index 00000000..1ba60606 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_1079_sql_variant_buffered_queries.phpt @@ -0,0 +1,59 @@ +--TEST-- +GitHub issue 1079 - fetching sql_variant types using client buffers +--DESCRIPTION-- +This test verifies that fetching sql_variant types using client buffers is supported. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- += @OP2 THEN @OP1 ELSE @OP2 END RETURN @Result END"; + + $conn->exec($tsql); + + $tsql = "SELECT [dbo].[$funcName](5, 6) AS RESULT"; + $stmt = $conn->prepare($tsql, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $metadata = $stmt->getColumnMeta(0); + var_dump($metadata); + + dropFunc($conn, $funcName); + + unset($stmt); + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +array(8) { + ["flags"]=> + int(0) + ["sqlsrv:decl_type"]=> + string(11) "sql_variant" + ["native_type"]=> + string(6) "string" + ["table"]=> + string(0) "" + ["pdo_type"]=> + int(2) + ["name"]=> + string(6) "RESULT" + ["len"]=> + int(10) + ["precision"]=> + int(0) +} \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt b/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt index 2e22a203..188bb176 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_azure_key_vault_keywords.phpt @@ -59,7 +59,7 @@ if (!$conn) { $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; $stmt = $conn->query($query); $info = $stmt->fetch(); - if ($info['value'] == 1 and $info['value_in_use'] == 1) { + if (!empty($info) and $info['value'] == 1 and $info['value_in_use'] == 1) { $isEnclaveEnabled = true; } diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt index e99aae85..954efbdc 100644 --- a/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt +++ b/test/functional/pdo_sqlsrv/pdo_fetch_variants_diff_styles.phpt @@ -170,11 +170,15 @@ function doValuesMatched($value1, $value2, $row, $col) } } -function fetchColumns($conn, $tableName, $numRows, $numCols) +function fetchColumns($conn, $tableName, $numRows, $numCols, $buffered = false) { try { // insert column data from a row of the original table - $stmtOriginal = $conn->prepare("SELECT * FROM $tableName WHERE c1_int = :row"); + if ($buffered) { + $stmtOriginal = $conn->prepare("SELECT * FROM $tableName WHERE c1_int = :row", array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + } else { + $stmtOriginal = $conn->prepare("SELECT * FROM $tableName WHERE c1_int = :row"); + } for ($i = 1; $i <= $numRows; $i++) { $c1_int = $i; @@ -263,6 +267,7 @@ try { $numCols = fetchBoundMixed($conn, $tableName, $numRows); fetchColumns($conn, $tableName, $numRows, $numCols); + fetchColumns($conn, $tableName, $numRows, $numCols, true); dropTable($conn, $tableName); unset($conn); @@ -278,3 +283,7 @@ Insert all columns from row 1 into one column of type sql_variant string(11) "sql_variant" Insert all columns from row 2 into one column of type sql_variant string(11) "sql_variant" +Insert all columns from row 1 into one column of type sql_variant +string(11) "sql_variant" +Insert all columns from row 2 into one column of type sql_variant +string(11) "sql_variant" diff --git a/test/functional/pdo_sqlsrv/pdo_simple_update_variants.phpt b/test/functional/pdo_sqlsrv/pdo_simple_update_variants.phpt index 866c5e2c..86c5e3a7 100644 --- a/test/functional/pdo_sqlsrv/pdo_simple_update_variants.phpt +++ b/test/functional/pdo_sqlsrv/pdo_simple_update_variants.phpt @@ -76,10 +76,15 @@ function updateFood($conn, $tableName, $id, $food, $category) } } -function fetchRows($conn, $tableName) +function fetchRows($conn, $tableName, $buffered = false) { $query = "SELECT * FROM $tableName ORDER BY id"; - $stmt = $conn->query($query); + if ($buffered) { + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + } else { + $stmt = $conn->query($query); + } $stmt->setFetchMode(PDO::FETCH_CLASS, 'Food'); while ($food = $stmt->fetch()) { @@ -108,7 +113,7 @@ try { updateID($conn, $tableName, 4, 'Milk', 'Diary Products'); - fetchRows($conn, $tableName); + fetchRows($conn, $tableName, true); updateFood($conn, $tableName, 4, 'Cheese', 'Diary Products'); @@ -118,7 +123,7 @@ try { insertData($conn, $tableName, 6, 'Salmon', 'Fish'); insertData($conn, $tableName, 2, 'Broccoli', 'Vegetables'); - fetchRows($conn, $tableName); + fetchRows($conn, $tableName, true); dropTable($conn, $tableName); unset($conn); diff --git a/test/functional/pdo_sqlsrv/skipif.inc b/test/functional/pdo_sqlsrv/skipif.inc index bf4ac502..ab29e6c5 100644 --- a/test/functional/pdo_sqlsrv/skipif.inc +++ b/test/functional/pdo_sqlsrv/skipif.inc @@ -1,4 +1,4 @@ +if (!extension_loaded("pdo_sqlsrv")) { + die("skip Extension not loaded"); +} diff --git a/test/functional/pdo_sqlsrv/skipif_azure.inc b/test/functional/pdo_sqlsrv/skipif_azure.inc index aec21ea5..e154005b 100644 --- a/test/functional/pdo_sqlsrv/skipif_azure.inc +++ b/test/functional/pdo_sqlsrv/skipif_azure.inc @@ -1,8 +1,8 @@ +if ($daasMode) { + die("skip test not applicable in Azure\n"); +} diff --git a/test/functional/pdo_sqlsrv/skipif_mid-refactor.inc b/test/functional/pdo_sqlsrv/skipif_mid-refactor.inc index 5c1e87a0..a0c6032d 100644 --- a/test/functional/pdo_sqlsrv/skipif_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/skipif_mid-refactor.inc @@ -1,17 +1,17 @@ - \ No newline at end of file +if (!extension_loaded("pdo") || !extension_loaded('pdo_sqlsrv')) { + die("skip extension not loaded"); +} diff --git a/test/functional/pdo_sqlsrv/skipif_unix_locales.inc b/test/functional/pdo_sqlsrv/skipif_unix_locales.inc new file mode 100644 index 00000000..938307ba --- /dev/null +++ b/test/functional/pdo_sqlsrv/skipif_unix_locales.inc @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/test/functional/sqlsrv/TC34_PrepAndExec.phpt b/test/functional/sqlsrv/TC34_PrepAndExec.phpt index ae9365ca..6364f22f 100644 --- a/test/functional/sqlsrv/TC34_PrepAndExec.phpt +++ b/test/functional/sqlsrv/TC34_PrepAndExec.phpt @@ -7,9 +7,6 @@ Validates that a prepared statement can be successfully executed more than once. PHPT_EXEC=true --SKIPIF-- --FILE-- diff --git a/test/functional/sqlsrv/TC42_FetchField.phpt b/test/functional/sqlsrv/TC42_FetchField.phpt index 21240a66..01a4d993 100644 --- a/test/functional/sqlsrv/TC42_FetchField.phpt +++ b/test/functional/sqlsrv/TC42_FetchField.phpt @@ -7,9 +7,6 @@ retrieving fields from a table including rows with all supported SQL types (28 t PHPT_EXEC=true --SKIPIF-- --FILE-- diff --git a/test/functional/sqlsrv/TC43_FetchData.phpt b/test/functional/sqlsrv/TC43_FetchData.phpt index 023c77da..ad446f0c 100644 --- a/test/functional/sqlsrv/TC43_FetchData.phpt +++ b/test/functional/sqlsrv/TC43_FetchData.phpt @@ -3,12 +3,7 @@ Fetch Field Data Test verifies the data retrieved via sqlsrv_get_field --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- + --FILE-- --FILE-- diff --git a/test/functional/sqlsrv/TC46_FetchNextResult.phpt b/test/functional/sqlsrv/TC46_FetchNextResult.phpt index 4c3dc4bf..c8b7ef98 100644 --- a/test/functional/sqlsrv/TC46_FetchNextResult.phpt +++ b/test/functional/sqlsrv/TC46_FetchNextResult.phpt @@ -5,12 +5,7 @@ Verifies the functionality of "sqlsrv_next_result" --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- --FILE-- diff --git a/test/functional/sqlsrv/TC51_StreamRead.phpt b/test/functional/sqlsrv/TC51_StreamRead.phpt index dd9a0a43..967aab1d 100644 --- a/test/functional/sqlsrv/TC51_StreamRead.phpt +++ b/test/functional/sqlsrv/TC51_StreamRead.phpt @@ -6,12 +6,7 @@ can be successfully retrieved as streams. --ENV-- PHPT_EXEC=true --SKIPIF-- - + --FILE-- + --FILE-- \ No newline at end of file + \ No newline at end of file diff --git a/test/functional/sqlsrv/skipif_unix_locales.inc b/test/functional/sqlsrv/skipif_unix_locales.inc new file mode 100644 index 00000000..dc44f973 --- /dev/null +++ b/test/functional/sqlsrv/skipif_unix_locales.inc @@ -0,0 +1,28 @@ + \ No newline at end of file +"buffered")); $stmt2 = sqlsrv_query($conn, $select); $metadata = sqlsrv_field_metadata($stmt); diff --git a/test/functional/sqlsrv/sqlsrv_simple_fetch_variants.phpt b/test/functional/sqlsrv/sqlsrv_simple_fetch_variants.phpt index 07a59820..0022509b 100644 --- a/test/functional/sqlsrv/sqlsrv_simple_fetch_variants.phpt +++ b/test/functional/sqlsrv/sqlsrv_simple_fetch_variants.phpt @@ -39,7 +39,7 @@ function Fetch($conn, $tableName, $numRows) { $select = "SELECT * FROM $tableName ORDER BY c1_int"; $stmt = sqlsrv_query($conn, $select); - $stmt2 = sqlsrv_query($conn, $select); + $stmt2 = sqlsrv_query($conn, $select, array(), array("Scrollable"=>"buffered")); $stmt3 = sqlsrv_query($conn, $select); $metadata = sqlsrv_field_metadata($stmt); diff --git a/test/functional/sqlsrv/sqlsrv_simple_update_variants.phpt b/test/functional/sqlsrv/sqlsrv_simple_update_variants.phpt index 70fa7955..b4b3b66b 100644 --- a/test/functional/sqlsrv/sqlsrv_simple_update_variants.phpt +++ b/test/functional/sqlsrv/sqlsrv_simple_update_variants.phpt @@ -89,10 +89,15 @@ function updateCountry($conn, $tableName, $id, $country, $continent) } } -function fetch($conn, $tableName) +function fetch($conn, $tableName, $buffered = false) { $select = "SELECT * FROM $tableName ORDER BY id"; - $stmt = sqlsrv_query($conn, $select); + + if ($buffered) { + $stmt = sqlsrv_query($conn, $select, array(), array("Scrollable"=>"buffered")); + } else { + $stmt = sqlsrv_query($conn, $select); + } while ($country = sqlsrv_fetch_object($stmt, "Country")) { echo "\nID: " . $country->id . " "; @@ -125,13 +130,13 @@ try { updateID($conn, $tableName, 4, 'Canada', 'North America'); // Read data - fetch($conn, $tableName); + fetch($conn, $tableName, true); // Update country updateCountry($conn, $tableName, 4, 'Mexico', 'North America'); // Read data - fetch($conn, $tableName); + fetch($conn, $tableName, true); // Add two more countries addCountry($conn, $tableName, 6, 'Brazil', 'South America'); diff --git a/test/functional/sqlsrv/srv_1063_locale_configs.phpt b/test/functional/sqlsrv/srv_1063_locale_configs.phpt new file mode 100644 index 00000000..0f5ea5e9 --- /dev/null +++ b/test/functional/sqlsrv/srv_1063_locale_configs.phpt @@ -0,0 +1,76 @@ +--TEST-- +GitHub issue 1063 - make setting locale info configurable +--DESCRIPTION-- +This test assumes LC_ALL is 'en_US.UTF-8' and verifies that the users can configure using ini file to set application locale using the system locale or not. This test is valid for Linux and macOS systems only. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + $file"); + print_r(shell_exec(PHP_BINARY." ".dirname(__FILE__)."/srv_1063_test_locale.php $val")); + print_r(shell_exec(PHP_BINARY." ".dirname(__FILE__)."/srv_1063_test_locale.php $val $locale")); +} + +$inifile = PHP_CONFIG_FILE_SCAN_DIR."/99-overrides.ini"; + +$locale1 = strtoupper(PHP_OS) === 'LINUX' ? "en_US.ISO-8859-1" : "en_US.ISO8859-1"; +$locale2 = 'de_DE.UTF-8'; + +runTest(0, $inifile, $locale1); +runTest(1, $inifile, $locale2); +runTest(2, $inifile, $locale2); +?> +--EXPECT-- + +***sqlsrv.SetLocaleInfo = 0 +pdo_sqlsrv.set_locale_info = 0*** + +**Begin** +Amount formatted: 10000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: $10,000.99 +Friday +December +3.14159 +**End** + +***sqlsrv.SetLocaleInfo = 1 +pdo_sqlsrv.set_locale_info = 1*** + +**Begin** +Amount formatted: 10000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: 10.000,99 € +Freitag +Dezember +3,14159 +**End** + +***sqlsrv.SetLocaleInfo = 2 +pdo_sqlsrv.set_locale_info = 2*** + +**Begin** +Amount formatted: $10,000.99 +Friday +December +3.14159 +**End** +**Begin** +Amount formatted: 10.000,99 € +Freitag +Dezember +3,14159 +**End** diff --git a/test/functional/sqlsrv/srv_1063_test_locale.php b/test/functional/sqlsrv/srv_1063_test_locale.php new file mode 100644 index 00000000..06ab387d --- /dev/null +++ b/test/functional/sqlsrv/srv_1063_test_locale.php @@ -0,0 +1,136 @@ +setTextAttribute(NumberFormatter::CURRENCY_CODE, $symbol); + $fmt->setAttribute(NumberFormatter::FRACTION_DIGITS, 2); + echo $fmt->format($amt) . PHP_EOL; + } +} + +require_once('MsSetup.inc'); + +$setLocaleInfo = ($_SERVER['argv'][1]); +$locale = ($_SERVER['argv'][2] ?? ''); + +echo "**Begin**" . PHP_EOL; + +// Assuming LC_ALL is 'en_US.UTF-8', so is LC_CTYPE +// But default LC_MONETARY varies +$ctype = 'en_US.UTF-8'; +switch ($setLocaleInfo) { + case 0: + case 1: + $m = 'C'; $symbol = ''; $sep = ''; + break; + case 2: + $m = 'en_US.UTF-8'; $symbol = '$'; $sep = ','; + break; + default: + fatalError("Unexpected $setLocaleInfo\n"); + break; +} + +$m1 = setlocale(LC_MONETARY, 0); +if ($m !== $m1) { + echo "Unexpected LC_MONETARY: $m1" . PHP_EOL; +} +$c1 = setlocale(LC_CTYPE, 0); +if ($ctype !== $c1) { + echo "Unexpected LC_CTYPE: $c1" . PHP_EOL; +} + +// Set a different locale, if the input is not empty +if (!empty($locale)) { + $loc = setlocale(LC_ALL, $locale); + if ($loc !== $locale) { + echo "Unexpected $loc for LC_ALL " . PHP_EOL; + } + + // Currency symbol and thousands separator in Linux and macOS may be different + if ($loc === 'de_DE.UTF-8') { + $symbol = strtoupper(PHP_OS) === 'LINUX' ? '€' : 'Eu'; + $sep = strtoupper(PHP_OS) === 'LINUX' ? '.' : ''; + } else { + $symbol = '$'; + $sep = ','; + } +} + +$info = localeconv(); + +if ($symbol !== $info['currency_symbol']) { + echo "$locale: Expected currency symbol '$symbol' but get '" . $info['currency_symbol'] . "'"; + echo PHP_EOL; +} +if ($sep !== $info['thousands_sep']) { + echo "$locale: Expected thousands separator '$sep' but get '" . $info['currency_symbol'] . "'"; + echo PHP_EOL; +} + +$n1 = 10000.98765; +printMoney($n1, $info); + +echo strftime("%A", strtotime("12/25/2020")) . PHP_EOL; +echo strftime("%B", strtotime("12/25/2020")) . PHP_EOL; + +$conn = sqlsrv_connect($server, $connectionOptions); +if (!$conn) { + fatalError("Failed to connect to $server."); +} + +$tableName = "[" . "srv1063" . $locale . "]"; +dropTable($conn, $tableName); + +$pi = "3.14159"; + +$stmt = sqlsrv_query($conn, "CREATE TABLE $tableName (c1 FLOAT)"); +if (!$stmt) { + fatalError("Failed to create test table $tableName"); +} +$stmt = sqlsrv_query($conn, "INSERT INTO $tableName (c1) VALUES ($pi)"); +if (!$stmt) { + fatalError("Failed to insert into test table $tableName"); +} + +$sql = "SELECT c1 FROM $tableName"; +$stmt = sqlsrv_query($conn, $sql); +if (!$stmt) { + fatalError("Failed in running query $sql"); +} + +while (sqlsrv_fetch($stmt)) { + $value = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_FLOAT); + echo $value . PHP_EOL; +} + +sqlsrv_free_stmt($stmt); + +dropTable($conn, $tableName); + +sqlsrv_close($conn); + +echo "**End**" . PHP_EOL; +?> diff --git a/test/functional/sqlsrv/srv_1079_sql_variant_buffered_queries.phpt b/test/functional/sqlsrv/srv_1079_sql_variant_buffered_queries.phpt new file mode 100644 index 00000000..51a98553 --- /dev/null +++ b/test/functional/sqlsrv/srv_1079_sql_variant_buffered_queries.phpt @@ -0,0 +1,62 @@ +--TEST-- +GitHub issue 1079 - fetching sql_variant types using client buffers +--DESCRIPTION-- +This test verifies that fetching sql_variant types using client buffers is supported. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- += @OP2 THEN @OP1 ELSE @OP2 END RETURN @Result END"; + +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + fatalError('Could not create function\n'); +} + +$tsql = "SELECT [dbo].[$funcName](5, 6) AS RESULT"; +$stmt = sqlsrv_prepare($conn, $tsql, array(), array("Scrollable" => SQLSRV_CURSOR_CLIENT_BUFFERED, "ClientBufferMaxKBSize" => 1000)); + +if (!$stmt) { + fatalError('Could not prepare query\n'); +} + +$result = sqlsrv_execute($stmt); +if (!$result) { + fatalError('Executing the query failed\n'); +} + +foreach (sqlsrv_field_metadata($stmt) as $fieldMetadata) { + var_dump($fieldMetadata); +} + +dropFunc($conn, $funcName); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +array(6) { + ["Name"]=> + string(6) "RESULT" + ["Type"]=> + int(-150) + ["Size"]=> + int(10) + ["Precision"]=> + NULL + ["Scale"]=> + NULL + ["Nullable"]=> + int(1) +} \ No newline at end of file diff --git a/test/functional/sqlsrv/test_stream_large_data.phpt b/test/functional/sqlsrv/test_stream_large_data.phpt index eacf853a..f73594e4 100644 --- a/test/functional/sqlsrv/test_stream_large_data.phpt +++ b/test/functional/sqlsrv/test_stream_large_data.phpt @@ -1,12 +1,7 @@ --TEST-- streaming large amounts of data into a database and getting it out as a string exactly the same. --SKIPIF-- - + --FILE-- Date: Wed, 15 Apr 2020 13:49:52 -0700 Subject: [PATCH 7/7] 5.8.1 hotfix release (#1126) --- CHANGELOG.md | 24 + Linux-mac-install.md | 18 +- README.md | 2 +- appveyor.yml | 4 +- azure-pipelines.yml | 4 +- codecov.yml | 21 + source/pdo_sqlsrv/config.m4 | 11 +- source/pdo_sqlsrv/pdo_dbh.cpp | 21 +- source/pdo_sqlsrv/pdo_init.cpp | 16 +- source/pdo_sqlsrv/pdo_stmt.cpp | 19 +- source/pdo_sqlsrv/pdo_util.cpp | 25 +- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 2 +- source/pdo_sqlsrv/php_pdo_sqlsrv_int.h | 10 +- source/shared/core_results.cpp | 101 +- source/shared/core_sqlsrv.h | 17 +- source/shared/core_stmt.cpp | 6 +- source/shared/core_util.cpp | 39 +- source/shared/localization.hpp | 3 + source/shared/localizationimpl.cpp | 108 +- source/shared/version.h | 2 +- source/sqlsrv/config.m4 | 20 +- source/sqlsrv/conn.cpp | 15 +- source/sqlsrv/init.cpp | 4 +- source/sqlsrv/php_sqlsrv_int.h | 12 +- source/sqlsrv/stmt.cpp | 3 +- source/sqlsrv/util.cpp | 28 +- test/bvt/sqlsrv/break.inc | 15 +- test/bvt/sqlsrv/connect.inc | 2 + test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt | 5 +- .../msdn_sqlsrv_get_field_stream_binary.php | 4 +- .../sqlsrv/msdn_sqlsrv_query_scrollable.phpt | 5 +- test/extended/AE_v2_values.inc | 220 ++++ test/extended/MsSetup.inc | 58 + test/extended/pdo_AE_functions.inc | 911 ++++++++++++++++ .../pdo_aev2_plaintext_nonstring.phpt | 41 + test/extended/pdo_aev2_plaintext_string.phpt | 41 + ...do_aev2_reencrypt_encrypted_nonstring.phpt | 39 + .../pdo_aev2_reencrypt_encrypted_string.phpt | 39 + test/extended/skipif_not_hgs.inc | 36 + test/extended/sqlsrv_AE_functions.inc | 991 ++++++++++++++++++ .../sqlsrv_aev2_plaintext_nonstring.phpt | 41 + .../sqlsrv_aev2_plaintext_string.phpt | 41 + ...rv_aev2_reencrypt_encrypted_nonstring.phpt | 39 + ...qlsrv_aev2_reencrypt_encrypted_string.phpt | 39 + test/functional/pdo_sqlsrv/break_pdo.php | 22 +- .../pdo_sqlsrv/pdo_ae_insert_numeric.phpt | 45 +- .../pdo_sqlsrv/pdo_buffered_fetch_types.phpt | 229 ++++ .../pdo_sqlsrv/pdo_connect_encrypted.phpt | 116 +- .../pdo_sqlsrv/pdo_connection_logs.phpt | 65 ++ .../pdo_sqlsrv/pdo_errorMode_logs.phpt | 116 ++ .../pdo_prepare_emulatePrepare_decimal.phpt | 10 +- .../pdo_prepare_emulatePrepare_float.phpt | 10 +- .../pdo_prepare_emulatePrepare_money.phpt | 10 +- .../pdo_sqlsrv/pdo_test_non_LOB_types.phpt | 34 +- .../pdo_sqlsrv/pdostatement_fetchAll.phpt | 10 +- .../pdo_sqlsrv/pdostatement_nextRowset.phpt | 6 +- test/functional/pdo_sqlsrv/skipif_not_hgs.inc | 30 +- test/functional/setup/setup_dbs.py | 5 + test/functional/sqlsrv/53_0021.phpt | 33 +- test/functional/sqlsrv/AEData.inc | 31 +- test/functional/sqlsrv/MsCommon.inc | 19 +- test/functional/sqlsrv/MsSetup.inc | 1 + test/functional/sqlsrv/break.php | 82 +- test/functional/sqlsrv/skipif_not_hgs.inc | 6 +- .../sqlsrv_ae_insert_sqltype_numeric.phpt | 2 +- ...lsrv_ae_output_param_sqltype_datetime.phpt | 243 +++-- ...qlsrv_ae_output_param_sqltype_numeric.phpt | 306 +++--- ...sqlsrv_ae_output_param_sqltype_string.phpt | 256 +++-- .../sqlsrv/sqlsrv_buffered_fetch_types.phpt | 278 +++++ .../functional/sqlsrv/sqlsrv_commit_logs.phpt | 63 ++ test/functional/sqlsrv/sqlsrv_connect.phpt | 2 +- .../sqlsrv/sqlsrv_connect_encrypted.phpt | 111 +- .../sqlsrv/sqlsrv_connect_log_to_file.phpt | 56 + .../sqlsrv/sqlsrv_connect_logs.phpt | 49 + test/functional/sqlsrv/sqlsrv_get_field.phpt | 4 +- .../sqlsrv/srv_007_login_timeout.phpt | 11 +- ...srv_230_sqlsrv_buffered_numeric_types.phpt | 41 +- 77 files changed, 4518 insertions(+), 886 deletions(-) create mode 100644 codecov.yml create mode 100644 test/extended/AE_v2_values.inc create mode 100644 test/extended/MsSetup.inc create mode 100644 test/extended/pdo_AE_functions.inc create mode 100644 test/extended/pdo_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/pdo_aev2_plaintext_string.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_string.phpt create mode 100644 test/extended/skipif_not_hgs.inc create mode 100644 test/extended/sqlsrv_AE_functions.inc create mode 100644 test/extended/sqlsrv_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_plaintext_string.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_connection_logs.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_commit_logs.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt create mode 100644 test/functional/sqlsrv/sqlsrv_connect_logs.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eab1ff0..0f255250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## 5.8.1 - 2020-04-15 +Updated PECL release packages. Here is the list of updates: + +### Fixed +- Pull Request [#1094](https://github.com/microsoft/msphpsql/pull/1094) - Fixed default locale issues in Alpine Linux +- Pull Request [#1095](https://github.com/microsoft/msphpsql/pull/1095) - Removed unnecessary data structure to support Client-Side Cursors feature in Alpine Linux +- Pull Request [#1095](https://github.com/microsoft/msphpsql/pull/1107) - Fixed logging issues when both drivers are enabled in Alpine Linux + +### Limitations +- No support for inout / output params when using sql_variant type +- No support for inout / output params when formatting decimal values +- In Linux and macOS, setlocale() only takes effect if it is invoked before the first connection. Attempting to set the locale after connecting will not work +- Always Encrypted requires [MS ODBC Driver 17+](https://docs.microsoft.com/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server) + - Only Windows Certificate Store and Azure Key Vault are supported. Custom Keystores are not yet supported + - Issue [#716](https://github.com/Microsoft/msphpsql/issues/716) - With Always Encrypted enabled, named parameters in subqueries are not supported + - Issue [#1050](https://github.com/microsoft/msphpsql/issues/1050) - With Always Encrypted enabled, insertion requires the column list for any tables with identity columns + - [Always Encrypted limitations](https://docs.microsoft.com/sql/connect/php/using-always-encrypted-php-drivers#limitations-of-the-php-drivers-when-using-always-encrypted) + +### Known Issues +- Connection pooling on Linux or macOS is not recommended with [unixODBC](http://www.unixodbc.org/) < 2.3.7 +- When pooling is enabled in Linux or macOS + - unixODBC <= 2.3.4 (Linux and macOS) might not return proper diagnostic information, such as error messages, warnings and informative messages + - due to this unixODBC bug, fetch large data (such as xml, binary) as streams as a workaround. See the examples [here](https://github.com/Microsoft/msphpsql/wiki/Features#pooling) + ## 5.8.0 - 2020-01-31 Updated PECL release packages. Here is the list of updates: diff --git a/Linux-mac-install.md b/Linux-mac-install.md index b3decb93..86f27567 100644 --- a/Linux-mac-install.md +++ b/Linux-mac-install.md @@ -1,7 +1,7 @@ # Linux and macOS Installation Tutorial for the Microsoft Drivers for PHP for SQL Server -The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 19.10, RedHat 7 and 8, Debian 8, 9, and 10, Suse 12 and 15, Alpine 3.11 (experimental), and macOS 10.13, 10.14, and 10.15. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver#loading-the-driver-at-php-startup). +The following instructions assume a clean environment and show how to install PHP 7.x, the Microsoft ODBC driver, the Apache web server, and the Microsoft Drivers for PHP for SQL Server on Ubuntu 16.04, 18.04, and 19.10, RedHat 7 and 8, Debian 8, 9, and 10, Suse 12 and 15, Alpine 3.11, and macOS 10.13, 10.14, and 10.15. These instructions advise installing the drivers using PECL, but you can also download the prebuilt binaries from the [Microsoft Drivers for PHP for SQL Server](https://github.com/Microsoft/msphpsql/releases) Github project page and install them following the instructions in [Loading the Microsoft Drivers for PHP for SQL Server](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver). For an explanation of extension loading and why we do not add the extensions to php.ini, see the section on [loading the drivers](https://docs.microsoft.com/sql/connect/php/loading-the-php-sql-driver#loading-the-driver-at-php-startup). -These instructions install PHP 7.4 by default. Note that some supported Linux distros default to PHP 7.1 or earlier, which is not supported for the latest version of the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.2 or 7.3 instead. +These instructions install PHP 7.4 by default using `pecl install`. You may need to run `pecl channel-update pecl.php.net` first. Note that some supported Linux distros default to PHP 7.1 or earlier, which is not supported for the latest version of the PHP drivers for SQL Server -- please see the notes at the beginning of each section to install PHP 7.2 or 7.3 instead. Also included are instructions for installing the PHP FastCGI Process Manager, PHP-FPM, on Ubuntu. This is needed if using the nginx web server instead of Apache. @@ -293,13 +293,10 @@ To test your installation, see [Testing your installation](#testing-your-install ## Installing the drivers on Alpine 3.11 > [!NOTE] -> Alpine support is experimental. - -> [!NOTE] -> The default version of PHP is 7.3. Alternate versions of PHP are not available from other repositories for Alpine 3.11. You can instead compile PHP from source. +> The default version of PHP is 7.3. Alternate versions of PHP may be available from other repositories for Alpine 3.11. You can instead compile PHP from source. ### Step 1. Install PHP -PHP packages for Alpine are found in the `edge/community` repository. Add the following line to `/etc/apt/repositories`, replacing `` with the URL of an Alpine repository mirror: +PHP packages for Alpine can be found in the `edge/community` repository. Please check [Enable Community Repository](https://wiki.alpinelinux.org/wiki/Enable_Community_Repository) on their WIKI page. Add the following line to `/etc/apt/repositories`, replacing `` with the URL of an Alpine repository mirror: ``` http:///alpine/edge/community ``` @@ -320,10 +317,7 @@ sudo su echo extension=pdo_sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/10_pdo_sqlsrv.ini echo extension=sqlsrv.so >> `php --ini | grep "Scan for additional .ini files" | sed -e "s|.*:\s*||"`/00_sqlsrv.ini ``` -You may need to define a locale: -``` -export LC_ALL=C -``` + ### Step 4. Install Apache and configure driver loading ``` sudo apk add php7-apache2 apache2 @@ -391,7 +385,7 @@ To test your installation, see [Testing your installation](#testing-your-install ## Testing Your Installation -To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, `/var/www/localhost/htdocs` on Alpine, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. On Alpine 3.11, you may also need to specify the **CharacterSet** as 'UTF-8' in the `$connectionOptions` array. +To test this sample script, create a file called testsql.php in your system's document root. This is `/var/www/html/` on Ubuntu, Debian, and Redhat, `/srv/www/htdocs` on SUSE, `/var/www/localhost/htdocs` on Alpine, or `/usr/local/var/www` on macOS. Copy the following script to it, replacing the server, database, username, and password as appropriate. ``` ( dbh->driver_data ); \ - driver_dbh->set_func( __FUNCTION__ ); \ - int length = strlen( __FUNCTION__ ) + strlen( ": entering" ); \ - char func[length+1]; \ - memset(func, '\0', length+1); \ - strcpy_s( func, sizeof( __FUNCTION__ ), __FUNCTION__ ); \ - strcat_s( func, length+1, ": entering" ); \ - LOG( SEV_NOTICE, func ); \ + if (driver_dbh != NULL) driver_dbh->set_func(__FUNCTION__); \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", __FUNCTION__); \ } -#else -#define PDO_LOG_DBH_ENTRY \ -{ \ - pdo_sqlsrv_dbh* driver_dbh = reinterpret_cast( dbh->driver_data ); \ - driver_dbh->set_func( __FUNCTION__ ); \ - LOG( SEV_NOTICE, __FUNCTION__ ## ": entering" ); \ -} -#endif // constructor for the internal object for connections pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ) : @@ -547,7 +534,7 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo // 0 for failure, 1 for success. int pdo_sqlsrv_db_handle_factory( _Inout_ pdo_dbh_t *dbh, _In_opt_ zval *driver_options TSRMLS_DC) { - LOG( SEV_NOTICE, "pdo_sqlsrv_db_handle_factory: entering" ); + PDO_LOG_DBH_ENTRY; hash_auto_ptr pdo_conn_options_ht; pdo_error_mode prev_err_mode = dbh->error_mode; diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index 4f1a8845..e471561f 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -128,11 +128,11 @@ PHP_MINIT_FUNCTION(pdo_sqlsrv) ZEND_TSRMLS_CACHE_UPDATE(); #endif - core_sqlsrv_register_logger( pdo_sqlsrv_log ); + core_sqlsrv_register_severity_checker(pdo_severity_check); REGISTER_INI_ENTRIES(); - LOG( SEV_NOTICE, "pdo_sqlsrv: entering minit" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering minit"); // initialize list of pdo errors g_pdo_errors_ht = reinterpret_cast( pemalloc( sizeof( HashTable ), 1 )); @@ -200,7 +200,7 @@ PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv) } catch( ... ) { - LOG( SEV_NOTICE, "Unknown exception caught in PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv)" ); + PDO_LOG_NOTICE("Unknown exception caught in PHP_MSHUTDOWN_FUNCTION(pdo_sqlsrv)"); return FAILURE; } @@ -225,18 +225,18 @@ PHP_RINIT_FUNCTION(pdo_sqlsrv) int set_locale = PDO_SQLSRV_G(set_locale_info); if (set_locale == 2) { setlocale(LC_ALL, ""); - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_ALL"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale LC_ALL"); } else if (set_locale == 1) { setlocale(LC_CTYPE, ""); - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale LC_CTYPE"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale LC_CTYPE"); } else { - LOG(SEV_NOTICE, "pdo_sqlsrv: setlocale NONE"); + PDO_LOG_NOTICE("pdo_sqlsrv: setlocale NONE"); } #endif - LOG( SEV_NOTICE, "pdo_sqlsrv: entering rinit" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering rinit"); return SUCCESS; } @@ -250,7 +250,7 @@ PHP_RSHUTDOWN_FUNCTION(pdo_sqlsrv) SQLSRV_UNUSED( module_number ); SQLSRV_UNUSED( type ); - LOG( SEV_NOTICE, "pdo_sqlsrv: entering rshutdown" ); + PDO_LOG_NOTICE("pdo_sqlsrv: entering rshutdown"); return SUCCESS; } diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 65f5a7da..37afbf38 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -351,26 +351,13 @@ void stmt_option_fetch_datetime:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_op } // log a function entry point -#ifndef _WIN32 #define PDO_LOG_STMT_ENTRY \ { \ pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); \ - driver_stmt->set_func( __FUNCTION__ ); \ - int length = strlen( __FUNCTION__ ) + strlen( ": entering" ); \ - char func[length+1]; \ - memset(func, '\0', length+1); \ - strcpy_s( func, sizeof( __FUNCTION__ ), __FUNCTION__ ); \ - strcat_s( func, length+1, ": entering" ); \ - LOG( SEV_NOTICE, func ); \ + if (driver_stmt != NULL) driver_stmt->set_func( __FUNCTION__ ); \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", __FUNCTION__); \ } -#else -#define PDO_LOG_STMT_ENTRY \ -{ \ - pdo_sqlsrv_stmt* driver_stmt = reinterpret_cast( stmt->driver_data ); \ - driver_stmt->set_func( __FUNCTION__ ); \ - LOG( SEV_NOTICE, __FUNCTION__ ## ": entering" ); \ -} -#endif // PDO SQLSRV statement destructor pdo_sqlsrv_stmt::~pdo_sqlsrv_stmt( void ) diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index 56048330..34e1a4ec 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -39,13 +39,6 @@ const int MAX_DIGITS = 11; // +-2 billion = 10 digits + 1 for the sign if negati // the warning message is not the error message alone; it must take WARNING_TEMPLATE above into consideration without the formats const int WARNING_MIN_LENGTH = static_cast( strlen( WARNING_TEMPLATE ) - strlen( "%1!s!%2!d!%3!s!" )); -// buffer used to hold a formatted log message prior to actually logging it. -const int LOG_MSG_SIZE = 2048; -char log_msg[LOG_MSG_SIZE] = {'\0'}; - -// internal error that says that FormatMessage failed -SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; - // Returns a sqlsrv_error for a given error code. sqlsrv_error_const* get_error_message( _In_opt_ unsigned int sqlsrv_error_code); @@ -623,22 +616,10 @@ void pdo_sqlsrv_retrieve_context_error( _In_ sqlsrv_error const* last_error, _Ou } } -// Formats the error message and writes to the php error log. -void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ) +// check the global variable of pdo_sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool pdo_severity_check(_In_ unsigned int severity TSRMLS_DC) { - if( (severity & PDO_SQLSRV_G( log_severity )) == 0 ) { - return; - } - - DWORD rc = FormatMessage( FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args ); - - // if an error occurs for FormatMessage, we just output an internal error occurred. - if( rc == 0 ) { - SQLSRV_STATIC_ASSERT( sizeof( INTERNAL_FORMAT_ERROR ) < sizeof( log_msg )); - std::copy( INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof( INTERNAL_FORMAT_ERROR ), log_msg ); - } - - php_log_err( log_msg TSRMLS_CC ); + return ((severity & PDO_SQLSRV_G(pdo_log_severity))); } namespace { diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index ab1aaf26..79a294f9 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -29,7 +29,7 @@ // request level variables ZEND_BEGIN_MODULE_GLOBALS(pdo_sqlsrv) -unsigned int log_severity; +unsigned int pdo_log_severity; zend_long client_buffer_max_size; #ifndef _WIN32 diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h index 2934e11f..2caf1985 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv_int.h @@ -60,7 +60,7 @@ extern HMODULE g_sqlsrv_hmodule; #endif PHP_INI_BEGIN() - STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, log_severity, + STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_LOG , "0", PHP_INI_ALL, OnUpdateLong, pdo_log_severity, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) STD_PHP_INI_ENTRY( INI_PREFIX INI_PDO_SQLSRV_CLIENT_BUFFER_MAX_SIZE , INI_BUFFERED_QUERY_LIMIT_DEFAULT, PHP_INI_ALL, OnUpdateLong, client_buffer_max_size, zend_pdo_sqlsrv_globals, pdo_sqlsrv_globals ) @@ -326,6 +326,10 @@ inline void pdo_reset_dbh_error( _Inout_ pdo_dbh_t* dbh TSRMLS_DC ) } } +#define PDO_LOG_NOTICE(message) \ + core_sqlsrv_register_severity_checker(pdo_severity_check); \ + LOG(SEV_NOTICE, message); + #define PDO_RESET_DBH_ERROR pdo_reset_dbh_error( dbh TSRMLS_CC ); inline void pdo_reset_stmt_error( _Inout_ pdo_stmt_t* stmt ) @@ -417,8 +421,8 @@ namespace pdo { } // namespace pdo -// logger for pdo_sqlsrv called by the core layer when it wants to log something with the LOG macro -void pdo_sqlsrv_log( _In_opt_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); +// check the global variable of pdo_sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool pdo_severity_check(_In_ unsigned int severity TSRMLS_DC); #endif /* PHP_PDO_SQLSRV_INT_H */ diff --git a/source/shared/core_results.cpp b/source/shared/core_results.cpp index 0b9c8fd9..afa3bf56 100644 --- a/source/shared/core_results.cpp +++ b/source/shared/core_results.cpp @@ -30,11 +30,6 @@ using namespace core; -// conversion matrix -// each entry holds a function that can perform the conversion or NULL which means the conversion isn't supported -// this is initialized the first time the buffered result set is created. -sqlsrv_buffered_result_set::conv_matrix_t sqlsrv_buffered_result_set::conv_matrix; - namespace { // *** internal types *** @@ -454,34 +449,6 @@ sqlsrv_buffered_result_set::sqlsrv_buffered_result_set( _Inout_ sqlsrv_stmt* stm meta = static_cast( sqlsrv_malloc( col_count * sizeof( sqlsrv_buffered_result_set::meta_data ))); - // set up the conversion matrix if this is the first time we're called - if( conv_matrix.size() == 0 ) { - - conv_matrix[SQL_C_CHAR][SQL_C_CHAR] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_CHAR][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::system_to_wide_string; - conv_matrix[SQL_C_CHAR][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_binary_string; - conv_matrix[SQL_C_CHAR][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::string_to_double; - conv_matrix[SQL_C_CHAR][SQL_C_LONG] = &sqlsrv_buffered_result_set::string_to_long; - conv_matrix[SQL_C_WCHAR][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_WCHAR][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_binary_string; - conv_matrix[SQL_C_WCHAR][SQL_C_CHAR] = &sqlsrv_buffered_result_set::wide_to_system_string; - conv_matrix[SQL_C_WCHAR][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::wstring_to_double; - conv_matrix[SQL_C_WCHAR][SQL_C_LONG] = &sqlsrv_buffered_result_set::wstring_to_long; - conv_matrix[SQL_C_BINARY][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_same_string; - conv_matrix[SQL_C_BINARY][SQL_C_CHAR] = &sqlsrv_buffered_result_set::binary_to_system_string; - conv_matrix[SQL_C_BINARY][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::binary_to_wide_string; - conv_matrix[SQL_C_LONG][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::long_to_double; - conv_matrix[SQL_C_LONG][SQL_C_LONG] = &sqlsrv_buffered_result_set::to_long; - conv_matrix[SQL_C_LONG][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_long; - conv_matrix[SQL_C_LONG][SQL_C_CHAR] = &sqlsrv_buffered_result_set::long_to_system_string; - conv_matrix[SQL_C_LONG][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::long_to_wide_string; - conv_matrix[SQL_C_DOUBLE][SQL_C_DOUBLE] = &sqlsrv_buffered_result_set::to_double; - conv_matrix[SQL_C_DOUBLE][SQL_C_BINARY] = &sqlsrv_buffered_result_set::to_double; - conv_matrix[SQL_C_DOUBLE][SQL_C_CHAR] = &sqlsrv_buffered_result_set::double_to_system_string; - conv_matrix[SQL_C_DOUBLE][SQL_C_LONG] = &sqlsrv_buffered_result_set::double_to_long; - conv_matrix[SQL_C_DOUBLE][SQL_C_WCHAR] = &sqlsrv_buffered_result_set::double_to_wide_string; - } - SQLSRV_ENCODING encoding = (( stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); @@ -844,18 +811,70 @@ SQLRETURN sqlsrv_buffered_result_set::get_data( _In_ SQLUSMALLINT field_index, _ *out_buffer_length = SQL_NULL_DATA; return SQL_SUCCESS; } - // check to make sure the conversion type is valid - conv_matrix_t::const_iterator conv_iter = conv_matrix.find( meta[field_index].c_type ); - if( conv_iter == conv_matrix.end() || conv_iter->second.find( target_type ) == conv_iter->second.end() ) { - last_error = new (sqlsrv_malloc( sizeof( sqlsrv_error ))) - sqlsrv_error( (SQLCHAR*) "07006", (SQLCHAR*) "Restricted data type attribute violation", 0 ); - return SQL_ERROR; + switch (meta[field_index].c_type) { + case SQL_C_CHAR: + switch (target_type) { + case SQL_C_CHAR: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::system_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_binary_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::string_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::string_to_long(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_WCHAR: + switch (target_type) { + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_binary_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::wide_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::wstring_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::wstring_to_long(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_BINARY: + switch (target_type) { + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_same_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::binary_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::binary_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_LONG: + switch (target_type) { + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::long_to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::long_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::long_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + case SQL_C_DOUBLE: + switch (target_type) { + case SQL_C_DOUBLE: return sqlsrv_buffered_result_set::to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_BINARY: return sqlsrv_buffered_result_set::to_double(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_CHAR: return sqlsrv_buffered_result_set::double_to_system_string(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_LONG: return sqlsrv_buffered_result_set::double_to_long(field_index, buffer, buffer_length, out_buffer_length); + case SQL_C_WCHAR: return sqlsrv_buffered_result_set::double_to_wide_string(field_index, buffer, buffer_length, out_buffer_length); + default: + break; + } + break; + default: + break; } - return (( this )->*( conv_matrix[meta[field_index].c_type][target_type] ))( field_index, buffer, buffer_length, - out_buffer_length ); + // Should not have reached here, return an error + last_error = new (sqlsrv_malloc(sizeof(sqlsrv_error))) + sqlsrv_error((SQLCHAR*) "07006", (SQLCHAR*) "Restricted data type attribute violation", 0); + return SQL_ERROR; } SQLRETURN sqlsrv_buffered_result_set::get_diag_field( _In_ SQLSMALLINT record_number, _In_ SQLSMALLINT diag_identifier, diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index ae27400e..e91b44c5 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -287,14 +287,12 @@ struct sqlsrv_static_assert { _In_ static const int value = 1; }; // Logging //********************************************************************************************************************************* // log_callback -// a driver specific callback for logging messages +// a driver specific callback for checking if the messages are qualified to be logged: // severity - severity of the message: notice, warning, or error -// msg - the message to log in a FormatMessage style formatting -// print_args - args to the message -typedef void (*log_callback)( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, _In_opt_ va_list* print_args ); +typedef bool (*severity_callback)(_In_ unsigned int severity TSRMLS_DC); -// each driver must register a log callback. This should be the first thing a driver does. -void core_sqlsrv_register_logger( _In_ log_callback ); +// each driver must register a severity checker callback for logging to work according to the INI settings +void core_sqlsrv_register_severity_checker(_In_ severity_callback driver_checker); // a simple wrapper around a PHP error logging function. void write_to_log( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, ... ); @@ -1746,13 +1744,6 @@ struct sqlsrv_buffered_result_set : public sqlsrv_result_set { sqlsrv_malloc_auto_ptr temp_string; // temp buffer to hold a converted field while in use SQLLEN temp_length; // number of bytes in the temp conversion buffer - typedef SQLRETURN (sqlsrv_buffered_result_set::*conv_fn)( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, - _Inout_ SQLLEN* out_buffer_length ); - typedef std::map< SQLINTEGER, std::map< SQLINTEGER, conv_fn > > conv_matrix_t; - - // two dimentional sparse matrix that holds the [from][to] functions that do conversions - static conv_matrix_t conv_matrix; - // string conversion functions SQLRETURN binary_to_wide_string( _In_ SQLSMALLINT field_index, _Out_writes_z_(*out_buffer_length) void* buffer, _In_ SQLLEN buffer_length, _Inout_ SQLLEN* out_buffer_length ); diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 32b10ad5..7fac8e5b 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -1799,11 +1799,11 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i case SQLSRV_PHPTYPE_INT: { - sqlsrv_malloc_auto_ptr field_value_temp; - field_value_temp = static_cast( sqlsrv_malloc( sizeof( long ))); + sqlsrv_malloc_auto_ptr field_value_temp; + field_value_temp = static_cast( sqlsrv_malloc( sizeof( SQLLEN ))); *field_value_temp = 0; - SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_LONG, field_value_temp, sizeof( long ), + SQLRETURN r = stmt->current_results->get_data( field_index + 1, SQL_C_LONG, field_value_temp, sizeof( SQLLEN ), field_len, true /*handle_warning*/ TSRMLS_CC ); CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 2853a22a..bc51b34b 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -23,10 +23,16 @@ namespace { +severity_callback g_driver_severity; + // *** internal constants *** -log_callback g_driver_log; + +// buffer used to hold a formatted log message prior to actually logging it. +const int LOG_MSG_SIZE = 2048; + // internal error that says that FormatMessage failed SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; + // buffer used to hold a formatted log message prior to actually logging it. char last_err_msg[2048] = {'\0'}; // 2k to hold the error messages @@ -35,6 +41,25 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin _In_ unsigned int mbcs_len, _Out_writes_(utf16_len) __transfer( mbcs_in_string ) SQLWCHAR* utf16_out_string, _In_ unsigned int utf16_len, bool use_strict_conversion = false ); + +// invoked by write_to_log() when the message severity qualifies to be logged +// msg - the message to log in a FormatMessage style formatting +// print_args - args to the message +void log_activity(_In_opt_ const char* msg, _In_opt_ va_list* print_args) +{ + char log_msg[LOG_MSG_SIZE] = { '\0' }; + + DWORD rc = FormatMessage(FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args); + + // if an error occurs for FormatMessage, we just output an internal error occurred. + if (rc == 0) { + SQLSRV_STATIC_ASSERT(sizeof(INTERNAL_FORMAT_ERROR) < sizeof(log_msg)); + std::copy(INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof(INTERNAL_FORMAT_ERROR), log_msg); + } + + php_log_err(log_msg TSRMLS_CC); +} + } // SQLSTATE for all internal errors @@ -47,22 +72,24 @@ SQLCHAR SSPWARN[] = "01SSP"; // the script (sqlsrv_configure). void write_to_log( _In_ unsigned int severity TSRMLS_DC, _In_ const char* msg, ...) { - SQLSRV_ASSERT( !(g_driver_log == NULL), "Must register a driver log function." ); + SQLSRV_ASSERT( !(g_driver_severity == NULL), "Must register a driver checker function." ); + if (!g_driver_severity(severity TSRMLS_CC)) { + return; + } va_list args; va_start( args, msg ); - g_driver_log( severity TSRMLS_CC, msg, &args ); + log_activity(msg, &args); va_end( args ); } -void core_sqlsrv_register_logger( _In_ log_callback driver_logger ) +void core_sqlsrv_register_severity_checker(_In_ severity_callback driver_checker) { - g_driver_log = driver_logger; + g_driver_severity = driver_checker; } - // convert a string from utf-16 to the encoding and return the new string in the pointer parameter and new // length in the len parameter. If no errors occurred during convertion, true is returned and the original // utf-16 string is released by this function if no errors occurred. Otherwise the parameters are not changed diff --git a/source/shared/localization.hpp b/source/shared/localization.hpp index fae23e2f..ec339d8c 100644 --- a/source/shared/localization.hpp +++ b/source/shared/localization.hpp @@ -41,6 +41,9 @@ #define CP_UTF16 1200 #define CP_ACP 0 // default to ANSI code page +bool _setLocale(const char * localeName, std::locale ** pLocale); +void setDefaultLocale(const char ** localeName, std::locale ** pLocale); + // This class provides allocation policies for the SystemLocale and AutoArray classes. // This is primarily needed for the self-allocating ToUtf16/FromUtf16 methods. // SNI needs all its allocations to use its own allocator so it would create a separate diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index 6a69aaa8..22cc8bf6 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -50,38 +50,44 @@ struct cp_iconv // CodePage 2 corresponds to binary. If the attribute PDO::SQLSRV_ENCODING_BINARY // is set, GetIndex() above hits the assert(false) directive unless we include // CodePage 2 below and assign an empty string to it. +#ifdef __MUSL__ +#define TRANSLIT "" +#else +#define TRANSLIT "//TRANSLIT" +#endif + const cp_iconv cp_iconv::g_cp_iconv[] = { { 65001, "UTF-8" }, { 1200, "UTF-16LE" }, { 3, "UTF-8" }, { 2, "" }, - { 1252, "CP1252//TRANSLIT" }, - { 850, "CP850//TRANSLIT" }, - { 437, "CP437//TRANSLIT" }, - { 874, "CP874//TRANSLIT" }, - { 932, "CP932//TRANSLIT" }, - { 936, "CP936//TRANSLIT" }, - { 949, "CP949//TRANSLIT" }, - { 950, "CP950//TRANSLIT" }, - { 1250, "CP1250//TRANSLIT" }, - { 1251, "CP1251//TRANSLIT" }, - { 1253, "CP1253//TRANSLIT" }, - { 1254, "CP1254//TRANSLIT" }, - { 1255, "CP1255//TRANSLIT" }, - { 1256, "CP1256//TRANSLIT" }, - { 1257, "CP1257//TRANSLIT" }, - { 1258, "CP1258//TRANSLIT" }, - { CP_ISO8859_1, "ISO8859-1//TRANSLIT" }, - { CP_ISO8859_2, "ISO8859-2//TRANSLIT" }, - { CP_ISO8859_3, "ISO8859-3//TRANSLIT" }, - { CP_ISO8859_4, "ISO8859-4//TRANSLIT" }, - { CP_ISO8859_5, "ISO8859-5//TRANSLIT" }, - { CP_ISO8859_6, "ISO8859-6//TRANSLIT" }, - { CP_ISO8859_7, "ISO8859-7//TRANSLIT" }, - { CP_ISO8859_8, "ISO8859-8//TRANSLIT" }, - { CP_ISO8859_9, "ISO8859-9//TRANSLIT" }, - { CP_ISO8859_13, "ISO8859-13//TRANSLIT" }, - { CP_ISO8859_15, "ISO8859-15//TRANSLIT" }, + { 1252, "CP1252" TRANSLIT }, + { 850, "CP850" TRANSLIT }, + { 437, "CP437" TRANSLIT }, + { 874, "CP874" TRANSLIT }, + { 932, "CP932" TRANSLIT }, + { 936, "CP936" TRANSLIT }, + { 949, "CP949" TRANSLIT }, + { 950, "CP950" TRANSLIT }, + { 1250, "CP1250" TRANSLIT }, + { 1251, "CP1251" TRANSLIT }, + { 1253, "CP1253" TRANSLIT }, + { 1254, "CP1254" TRANSLIT }, + { 1255, "CP1255" TRANSLIT }, + { 1256, "CP1256" TRANSLIT }, + { 1257, "CP1257" TRANSLIT }, + { 1258, "CP1258" TRANSLIT }, + { CP_ISO8859_1, "ISO8859-1" TRANSLIT }, + { CP_ISO8859_2, "ISO8859-2" TRANSLIT }, + { CP_ISO8859_3, "ISO8859-3" TRANSLIT }, + { CP_ISO8859_4, "ISO8859-4" TRANSLIT }, + { CP_ISO8859_5, "ISO8859-5" TRANSLIT }, + { CP_ISO8859_6, "ISO8859-6" TRANSLIT }, + { CP_ISO8859_7, "ISO8859-7" TRANSLIT }, + { CP_ISO8859_8, "ISO8859-8" TRANSLIT }, + { CP_ISO8859_9, "ISO8859-9" TRANSLIT }, + { CP_ISO8859_13, "ISO8859-13" TRANSLIT }, + { CP_ISO8859_15, "ISO8859-15" TRANSLIT }, { 12000, "UTF-32LE" } }; const size_t cp_iconv::g_cp_iconv_count = ARRAYSIZE(cp_iconv::g_cp_iconv); @@ -279,22 +285,46 @@ bool EncodingConverter::Initialize() using namespace std; +#ifndef _countof + #define _countof(obj) (sizeof(obj)/sizeof(obj[0])) +#endif + +const char* DEFAULT_LOCALES[] = {"en_US.UTF-8", "C"}; + +bool _setLocale(const char * localeName, std::locale ** pLocale) +{ + try + { + *pLocale = new std::locale(localeName); + } + catch(const std::exception& e) + { + return false; + } + + return true; +} + +void setDefaultLocale(const char ** localeName, std::locale ** pLocale) +{ + if(!localeName || !_setLocale(*localeName, pLocale)) + { + int count = 0; + while(!_setLocale(DEFAULT_LOCALES[count], pLocale) && count < _countof(DEFAULT_LOCALES)) + { + count++; + } + + if(localeName) + *localeName = count < _countof(DEFAULT_LOCALES)?DEFAULT_LOCALES[count]:NULL; + } +} + SystemLocale::SystemLocale( const char * localeName ) : m_uAnsiCP(CP_UTF8) , m_pLocale(NULL) { - const char* DEFAULT_LOCALE = "en_US.UTF-8"; - - try { - m_pLocale = new std::locale(localeName); - } - catch(const std::exception& e) { - localeName = DEFAULT_LOCALE; - } - - if(!m_pLocale) { - m_pLocale = new std::locale(localeName); - } + setDefaultLocale(&localeName, &m_pLocale); // Mapping from locale charset to codepage struct LocaleCP diff --git a/source/shared/version.h b/source/shared/version.h index 4369a3c8..40ae57f6 100644 --- a/source/shared/version.h +++ b/source/shared/version.h @@ -27,7 +27,7 @@ // Increase Patch for backward compatible fixes. #define SQLVERSION_MAJOR 5 #define SQLVERSION_MINOR 8 -#define SQLVERSION_PATCH 0 +#define SQLVERSION_PATCH 1 #define SQLVERSION_BUILD 0 // For previews, set this constant to 1. Otherwise, set it to 0 diff --git a/source/sqlsrv/config.m4 b/source/sqlsrv/config.m4 index aa460ef9..240c09cf 100644 --- a/source/sqlsrv/config.m4 +++ b/source/sqlsrv/config.m4 @@ -44,13 +44,13 @@ if test "$PHP_SQLSRV" != "no"; then pdo_sqlsrv_inc_path=$srcdir/ext/pdo_sqlsrv/shared/ shared_src_class="" elif test -f $srcdir/ext/sqlsrv/shared/core_sqlsrv.h; then - sqlsrv_inc_path=$srcdir/ext/sqlsrv/shared/ + sqlsrv_inc_path=$srcdir/ext/sqlsrv/shared/ elif test -f $srcdir/shared/core_sqlsrv.h; then - sqlsrv_inc_path=$srcdir/shared/ + sqlsrv_inc_path=$srcdir/shared/ else - AC_MSG_ERROR([Cannot find SQLSRV headers]) + AC_MSG_ERROR([Cannot find SQLSRV headers]) fi - AC_MSG_RESULT($sqlsrv_inc_path) + AC_MSG_RESULT($sqlsrv_inc_path) CXXFLAGS="$CXXFLAGS -std=c++11" CXXFLAGS="$CXXFLAGS -D_FORTIFY_SOURCE=2 -O2" @@ -58,11 +58,17 @@ if test "$PHP_SQLSRV" != "no"; then HOST_OS_ARCH=`uname` if test "${HOST_OS_ARCH}" = "Darwin"; then - SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-bind_at_load" - MACOSX_DEPLOYMENT_TARGET=`sw_vers -productVersion` + SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-bind_at_load" + MACOSX_DEPLOYMENT_TARGET=`sw_vers -productVersion` else - SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-z,now" + SQLSRV_SHARED_LIBADD="$SQLSRV_SHARED_LIBADD -Wl,-z,now" + IS_ALPINE_1=`uname -a | cut -f 4 -d ' ' | cut -f 2 -d '-'` + IS_ALPINE_2=`cat /etc/os-release | grep ID | grep alpine | cut -f 2 -d '='` + if test "${IS_ALPINE_1}" = "Alpine" || test "${IS_ALPINE_2}" = "alpine"; then + AC_DEFINE(__MUSL__, 1, [ ]) + fi fi + PHP_REQUIRE_CXX() PHP_ADD_LIBRARY(stdc++, 1, SQLSRV_SHARED_LIBADD) diff --git a/source/sqlsrv/conn.cpp b/source/sqlsrv/conn.cpp index c4ed6362..58aaf14b 100644 --- a/source/sqlsrv/conn.cpp +++ b/source/sqlsrv/conn.cpp @@ -629,10 +629,9 @@ const connection_option SS_CONN_OPTS[] = { PHP_FUNCTION ( sqlsrv_connect ) { - LOG_FUNCTION( "sqlsrv_connect" ); - SET_FUNCTION_NAME( *g_ss_henv_cp ); - SET_FUNCTION_NAME( *g_ss_henv_ncp ); + g_ss_henv_cp->set_func(_FN_); + g_ss_henv_ncp->set_func(_FN_); reset_errors( TSRMLS_C ); @@ -785,7 +784,7 @@ PHP_FUNCTION( sqlsrv_close ) // dummy context to pass to the error handler error_ctx = new (sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); if( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &conn_r) == FAILURE ) { @@ -816,7 +815,7 @@ PHP_FUNCTION( sqlsrv_close ) throw ss::SSException(); } - SET_FUNCTION_NAME( *conn ); + conn->set_func(_FN_); // cause any variables still holding a reference to this to be invalid so they cause // an error when passed to a sqlsrv function. There's nothing we can do if the @@ -845,13 +844,15 @@ PHP_FUNCTION( sqlsrv_close ) void __cdecl sqlsrv_conn_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ) { - LOG_FUNCTION( "sqlsrv_conn_dtor" ); + // Without sqlsrv_close(), this function is invoked by php during the final clean up stage. + // To prevent memory/resource leaks, no more logging at this point. + //LOG_FUNCTION( "sqlsrv_conn_dtor" ); // get the structure ss_sqlsrv_conn *conn = static_cast( rsrc->ptr ); SQLSRV_ASSERT( conn != NULL, "sqlsrv_conn_dtor: connection was null"); - SET_FUNCTION_NAME( *conn ); + conn->set_func(__func__); // close all statements associated with the connection. sqlsrv_conn_close_stmts( conn TSRMLS_CC ); diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index bc069205..96f5338f 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -271,8 +271,8 @@ PHP_MINIT_FUNCTION(sqlsrv) { SQLSRV_UNUSED( type ); - core_sqlsrv_register_logger( ss_sqlsrv_log ); - + core_sqlsrv_register_severity_checker(ss_severity_check); + // our global variables are initialized in the RINIT function #if defined(ZTS) if( ts_allocate_id( &sqlsrv_globals_id, diff --git a/source/sqlsrv/php_sqlsrv_int.h b/source/sqlsrv/php_sqlsrv_int.h index 9ca59d9e..fae7082e 100644 --- a/source/sqlsrv/php_sqlsrv_int.h +++ b/source/sqlsrv/php_sqlsrv_int.h @@ -313,15 +313,11 @@ public: #define LOG_FUNCTION( function_name ) \ const char* _FN_ = function_name; \ SQLSRV_G( current_subsystem ) = current_log_subsystem; \ - LOG( SEV_NOTICE, "%1!s!: entering", _FN_ ); + core_sqlsrv_register_severity_checker(ss_severity_check); \ + LOG(SEV_NOTICE, "%1!s!: entering", _FN_); -#define SET_FUNCTION_NAME( context ) \ -{ \ - (context).set_func( _FN_ ); \ -} - -// logger for ss_sqlsrv called by the core layer when it wants to log something with the LOG macro -void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ); +// check the global variables of sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool ss_severity_check(_In_ unsigned int severity TSRMLS_DC); // subsystems that may report log messages. These may be used to filter which systems write to the log to prevent noise. enum logging_subsystems { diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 05d46b6c..c7f002dc 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -1371,7 +1371,6 @@ void __cdecl sqlsrv_stmt_dtor( _Inout_ zend_resource *rsrc TSRMLS_DC ) PHP_FUNCTION( sqlsrv_free_stmt ) { - LOG_FUNCTION( "sqlsrv_free_stmt" ); zval* stmt_r = NULL; @@ -1384,7 +1383,7 @@ PHP_FUNCTION( sqlsrv_free_stmt ) // dummy context to pass to the error handler error_ctx = new (sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); // take only the statement resource if( zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "r", &stmt_r ) == FAILURE ) { diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index 671f6bf4..b91f17c2 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -30,13 +30,6 @@ namespace { // current subsytem. defined for the CHECK_SQL_{ERROR|WARNING} macros unsigned int current_log_subsystem = LOG_UTIL; -// buffer used to hold a formatted log message prior to actually logging it. -const int LOG_MSG_SIZE = 2048; -char log_msg[LOG_MSG_SIZE] = {'\0'}; - -// internal error that says that FormatMessage failed -SQLCHAR INTERNAL_FORMAT_ERROR[] = "An internal error occurred. FormatMessage failed writing an error message."; - // *** internal functions *** sqlsrv_error_const* get_error_message( _In_ unsigned int sqlsrv_error_code ); @@ -457,21 +450,10 @@ ss_error SS_ERRORS[] = { { UINT_MAX, {} } }; -// Formats an error message and finally writes it to the php log. -void ss_sqlsrv_log( _In_ unsigned int severity TSRMLS_DC, _In_opt_ const char* msg, _In_opt_ va_list* print_args ) +// check the global variables of sqlsrv severity whether the message qualifies to be logged with the LOG macro +bool ss_severity_check(_In_ unsigned int severity TSRMLS_DC) { - if(( severity & SQLSRV_G( log_severity )) && ( SQLSRV_G( current_subsystem ) & SQLSRV_G( log_subsystems ))) { - - DWORD rc = FormatMessage( FORMAT_MESSAGE_FROM_STRING, msg, 0, 0, log_msg, LOG_MSG_SIZE, print_args ); - - // if an error occurs for FormatMessage, we just output an internal error occurred. - if( rc == 0 ) { - SQLSRV_STATIC_ASSERT( sizeof( INTERNAL_FORMAT_ERROR ) < sizeof( log_msg )); - std::copy( INTERNAL_FORMAT_ERROR, INTERNAL_FORMAT_ERROR + sizeof( INTERNAL_FORMAT_ERROR ), log_msg ); - } - - php_log_err( log_msg TSRMLS_CC ); - } + return ((severity & SQLSRV_G(log_severity)) && (SQLSRV_G(current_subsystem) & SQLSRV_G(log_subsystems))); } bool ss_error_handler( _Inout_ sqlsrv_context& ctx, _In_ unsigned int sqlsrv_error_code, _In_ bool warning TSRMLS_DC, _In_opt_ va_list* print_args ) @@ -598,7 +580,7 @@ PHP_FUNCTION( sqlsrv_configure ) // dummy context to pass onto the error handler error_ctx = new ( sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); int zr = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "sz", &option, &option_len, &value_z ); CHECK_CUSTOM_ERROR(( zr == FAILURE ), error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { @@ -718,7 +700,7 @@ PHP_FUNCTION( sqlsrv_get_config ) // dummy context to pass onto the error handler error_ctx = new ( sqlsrv_malloc( sizeof( sqlsrv_context ))) sqlsrv_context( 0, ss_error_handler, NULL ); - SET_FUNCTION_NAME( *error_ctx ); + error_ctx->set_func(_FN_); int zr = zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "s", &option, &option_len ); CHECK_CUSTOM_ERROR(( zr == FAILURE ), error_ctx, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ) { diff --git a/test/bvt/sqlsrv/break.inc b/test/bvt/sqlsrv/break.inc index 6a2e190e..5e6429f7 100644 --- a/test/bvt/sqlsrv/break.inc +++ b/test/bvt/sqlsrv/break.inc @@ -1,15 +1,18 @@ "$databaseName", "username"=>"$username", "password"=>"$password" ); $conn = sqlsrv_connect( $serverName, $connectionInfo ); diff --git a/test/bvt/sqlsrv/connect.inc b/test/bvt/sqlsrv/connect.inc index 07f9289a..6ac30a9c 100644 --- a/test/bvt/sqlsrv/connect.inc +++ b/test/bvt/sqlsrv/connect.inc @@ -4,6 +4,8 @@ $databaseName = 'TARGET_DATABASE'; $uid = 'TARGET_USERNAME'; $pwd = 'TARGET_PASSWORD'; +$server2 = 'ANOTHER_SERVER'; + // RevisionNumber in SalesOrderHeader is subject to a trigger incrementing it whenever // changes are made to SalesOrderDetail. Since RevisionNumber is a tinyint, it can // overflow quickly if the BVT tests often run. So we change it directly here first diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt index 9b357732..6e6adfb2 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_connect_MARS.phpt @@ -10,8 +10,9 @@ $conn = sqlsrv_connect( $server, $connectionInfo); /* Connect to the local server using Windows Authentication and specify the AdventureWorks database as the database in use. */ -$serverName = "sql-2k14-sp1-1.galaxy.ad"; -$connectionInfo = array( "Database"=>"AdventureWorks2014", "UID"=>"sa", "PWD"=>"Moonshine4me", 'MultipleActiveResultSets'=> false); +$serverName = $server2; +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd, 'MultipleActiveResultSets'=> false); + $conn = sqlsrv_connect( $serverName, $connectionInfo); if( $conn === false ) { diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php b/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php index 751229f1..ea810678 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php +++ b/test/bvt/sqlsrv/msdn_sqlsrv_get_field_stream_binary.php @@ -1,8 +1,8 @@ "AdventureWorks2014", "UID"=>"sa", "PWD"=>"Moonshine4me"); +$serverName = $server2; +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd); $conn = sqlsrv_connect( $serverName, $connectionInfo); if( $conn === false ) { diff --git a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt index e9444ea9..a42c9d2f 100644 --- a/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt +++ b/test/bvt/sqlsrv/msdn_sqlsrv_query_scrollable.phpt @@ -4,11 +4,8 @@ server side cursor specified when querying --FILE-- "tempdb", "UID"=>"sa", "PWD"=>"Moonshine4me")); - require('connect.inc'); -$connectionInfo = array( "Database"=>"$databaseName", "UID"=>"$uid", "PWD"=>"$pwd"); +$connectionInfo = array( "Database"=>$databaseName, "UID"=>$uid, "PWD"=>$pwd); $conn = sqlsrv_connect( $server, $connectionInfo); if ( $conn === false ) { die( print_r( sqlsrv_errors(), true )); diff --git a/test/extended/AE_v2_values.inc b/test/extended/AE_v2_values.inc new file mode 100644 index 00000000..d4f32638 --- /dev/null +++ b/test/extended/AE_v2_values.inc @@ -0,0 +1,220 @@ +$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +$targetCeValues = array('correct' =>$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +// Names of the encryption keys, depending on whether we are using Windows +// or AKV authentication (defined in MsSetup.inc). -enclave keys are enclave +// enabled, -noenclave keys are not enclave enabled. +// $targetKeys are the keys used for re-encrypting encrypted columns +if ($keystore == 'win') { + $keys = array("CEK-win-enclave", + "CEK-win-noenclave" + ); + $targetKeys = array("CEK-win-enclave", + "CEK-win-noenclave", + "CEK-win-enclave2", + "CEK-win-noenclave2" + ); +} elseif ($keystore == 'akv') { + $keys = array("CEK-akv-enclave", + "CEK-akv-noenclave" + ); + $targetKeys = array("CEK-akv-enclave", + "CEK-akv-noenclave", + "CEK-akv-enclave2", + "CEK-akv-noenclave2" + ); +} else { + die("No keystore specified! Aborting...\n"); +} + +// $targetTypes are the encryption types used for re-encrypting encrypted columns +$encryptionTypes = array("Deterministic", + "Randomized", + ); +$targetTypes = array("Deterministic", + "Randomized", + ); + + +// Length of the string-type columns. $slength is length as a string instead of integer +$length = 64; +$slength = '64'; + +// Testing the following data types, split into two arrays because if we try one array, +// at some point we get a CE405 error for no clear reason (might be a memory issue?). +// TODO: Follow up and see if we can use a single type array. +$dataTypes1 = array('integer', + 'bigint', + 'smallint', + 'tinyint', + 'bit', + 'float', + 'real', + 'numeric', + 'date', + 'time', + 'datetime', + 'datetime2', + 'datetimeoffset', + 'smalldatetime', + ); + +$dataTypes2 = array('char', + 'nchar', + 'varchar', + 'nvarchar', + 'varchar(max)', + 'nvarchar(max)', + 'binary', + 'varbinary', + 'varbinary(max)', + ); + +// Construct the array of column names. Two columns for each data type, +// one encrypted (suffixed _AE) and one not encrypted. +$colNames1 = array(); +$colNamesAE1 = array(); +$colNames2 = array(); +$colNamesAE2 = array(); + +foreach ($dataTypes1 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames1[$type] = "c_".$column; + $colNamesAE1[$type] = "c_".$column."_AE"; +} + +foreach ($dataTypes2 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames2[$type] = "c_".$column; + $colNamesAE2[$type] = "c_".$column."_AE"; +} + +// The test data below is a mixture of random data and edge cases +$testValues = array(); + +// integers +$testValues['integer'] = array(0,-1,1,2147483647,-2147483648,65536,-100000000,128,9); +$testValues['bigint'] = array(9223372036854775807,-40,0,1,2147483647,-2147483648,65536,-100000000000000); +$testValues['smallint'] = array(4,-4,-32768,-99,32767,-30000,-12,-1); +$testValues['tinyint'] = array(2,0,255,254,99,101,100,32); +$testValues['bit'] = array(1,1,0,0,0,0,1,0); + +// floating point +$testValues['float'] = array(3.14159,2.3e+12,-2.3e+12,2.23e-308,1,-1.79e+308,892.3098234234001,1.2); +$testValues['real'] = array(3.14159,2.3e+12,-2.3e+12,1.18e-38,1,-3.4e+38,892.3098234234001,1.2); +$testValues['numeric'] = array(-3.14159,1.003456789,45.6789,0.0001,987987.12345,-987987.12345,100000000000,-100000000000); + +// dates and times +$testValues['date'] = array('2010-01-31','0485-03-31','7825-07-23','9999-12-31','1956-02-27','2018-09-01','5401-11-02','1031-10-04'); +$testValues['time'] = array('12:40:40','08:14:54.3096','23:59:59.9999','01:00:34.0101','21:45:45.4545','00:23:45.6','17:48:00.0000','20:31:49.0001'); +$testValues['datetime2'] = array('9801-01-29 11:45:23.5092856','2384-12-31 12:40:12.5434323','1984-09-25 10:40:20.0909111','9999-12-31 23:59:59.999999', + '1259-04-29 23:59:59.9999999','1748-09-21 17:48:54.723','3125-05-31 05:00:32.4','0001-01-01 00:00:00'); +$testValues['datetimeoffset'] = array('9801-01-29 11:45:23.5092856-12:45','0001-01-01 00:00:00-02:30','1984-09-25 10:40:20.0909111+03:00','1748-09-21 17:48:54.723-09:21', + '4896-05-18 23:17:58.3-02:00','1657-08-04 18:14:27.4','2022-03-17 07:31:45.890342+09:30','1987-10-25 14:27:34.6320945-06:00'); +$testValues['datetime'] = array('9801-01-29 11:45:23.509','2384-12-31 12:40:12.543','1984-09-25 10:40:20.090','9999-12-31 23:59:59.997', + '2753-04-29 23:59:59.997','1948-09-21 17:48:54.723','3125-05-31 05:00:32.4','2001-01-01 00:00:00'); +$testValues['smalldatetime'] = array('1998-06-13 04:00:00','1985-03-31 12:40:00','2025-07-23 05:00:00','1999-12-31 00:00:00', + '1956-02-27 23:59:00','2018-09-01 14:35:00','2079-06-06 23:59:00','1931-10-04 19:52:00'); + +// strings, ascii and unicode +$testValues['char'] = array('wvyxz', 'tposw', '%c@kj>5', 'fd4$_w@q^@!coe$7', 'abcd', 'ev72#x*fv=u$', '4rfg3sw', 'voi%###i<@@'); +$testValues['nchar'] = array('⽧㘎ⷅ㪋','af㋮ᶄḉㇼ៌ӗඣ','ኁ㵮ഖᅥ㪮ኸ⮊ߒᙵꇕ⯐គꉟफ़⻦ꈔꇼŞ','ꐷꬕ','㐯㩧㖃⺵㴰ڇལᧆ겴ꕕ겑וֹꔄ若㌉ᒵȅ㗉ꗅᡉ','ʭḪぅᾔᎀ㍏겶ꅫၞ㴉ᴳ㜞҂','','בּŬḛʼꃺꌖ㓵ꗛ᧽ഭწ社⾯㬄౧ຸฬ㐯ꋛ㗾'); +$testValues['varchar'] = array('gop093','*#$@@)%*$@!%','cio4*3do*$','zzz$a#l',' ','v#x%n!k&r@p$f^','6$gt?je#~','0x3dK#?'); +$testValues['nvarchar'] = array('ᾁẴ㔮㖖ୱܝ㐗㴴៸ழ᷂ᵄ葉អ㺓節','ӕᏵ൴ꔓὀ⾼','Ὡ','璉Džꖭ갪ụ⺭','Ӿϰᇬ㭡㇑ᵈᔆ⽹hᙎ՞ꦣ㧼ለͭ','Ĕ㬚㔈♠既','ꁈ ݫ','ꍆફⷌ㏽̗ૣܯ¢⽳㌭ゴᔓᅄѓⷉꘊⶮᏏᴗஈԋ≡ㄊହꂈ꓂ꑽრꖾŞ⽉걹ꩰോఫ㒧㒾㑷藍㵀ဲ更ꧥ'); +$testValues['varchar(max)'] = array('Q0H4@4E%v+ 3*Trx#>*r86-&d$VgjZ','AjEvVABur(A&Q@eG,A$3u"xAzl','z#dFd4z', + '9Dvsg9B?7oktB@|OIqy<\K^\e|*7Y&yH31E-<.hQ:)g Jl`MQV>rdOhjG;B4wQ(WR[`l(pELt0FYu._T3+8tns!}Nqrc1%n@|N|ik C@ 03a/ +H9mBq', + 'SSs$Ie*{:D4;S]',' ','<\K^\e|*7Y&yH31E-<.hQ:','@Kg1Z6XTOgbt?CEJ|M^rkR_L4{1?l', '<=', '>=', '<>', '!<', '!>'); + +// Thresholds against which to use the comparison operators +$thresholds = array('integer' => 0, + 'bigint' => 0, + 'smallint' => 1000, + 'tinyint' => 100, + 'bit' => 0, + 'float' => 1.2, + 'real' => -1.2, + 'numeric' => 45.6789, + 'char' => 'rstuv', + 'nchar' => '㊃ᾞਲ㨴꧶ꁚꅍ', + 'varchar' => '6$gt?je#~', + 'nvarchar' => 'ӕᏵ൴ꔓὀ⾼', + 'varchar(max)' => 'hijkl', + 'nvarchar(max)' => 'xᐕᛙᘡ', + 'binary' => '44EE4A', + 'varbinary' => 'E43004FF', + 'varbinary(max)' => 'D3EA762C78FC', + 'date' => '2010-01-31', + 'time' => '21:45:45.4545', + 'datetime' => '3125-05-31 05:00:32.4', + 'datetime2' => '2384-12-31 12:40:12.5434323', + 'datetimeoffset' => '1984-09-25 10:40:20.0909111+03:00', + 'smalldatetime' => '1998-06-13 04:00:00', + ); + +// String patterns to test with LIKE +// For AE, LIKE only works with string types for now. Additional types +// are listed here because eventually the type conversions required for +// pattern matching non-string types should be supported. +$patterns = array('integer' => array('8', '48', '123'), + 'bigint' => array('000','7', '65536'), + 'smallint' => array('4','768','abc'), + 'tinyint' => array('9','0','25'), + 'bit' => array('0','1','100'), + 'float' => array('14159','.','E+','2.3','308'), + 'real' => array('30','.','e-','2.3','38'), + 'numeric' => array('0','0000','12345','abc','.'), + 'char' => array('w','@','x*fv=u$','e3'), + 'nchar' => array('af㋮','㐯ꋛ㗾','ꦣ㧼ለͭ','123'), + 'varchar' => array(' ','a','#','@@)'), + 'nvarchar' => array(' ','Ӿϰᇬ㭡','璉Džꖭ갪ụ⺭','更ꧥ','ꈔꇼŞ'), + 'varchar(max)' => array('A','|*7Y&','4z','@!@','AjE'), + 'nvarchar(max)' => array('t','㧶ᐁቴƯɋ','ᘷ㬡',' ','ꐾɔᡧ㝚'), + 'binary' => array('44EE4A'), + 'varbinary' => array('E43004FF'), + 'varbinary(max)' => array('D3EA762C78FC'), + 'date' => array('20','%','9-','04'), + 'time' => array('4545','.0','20:','12345',':'), + 'datetime' => array('997','12',':5','9999'), + 'datetime2' => array('3125-05-31 05:','.45','$f#','-29 ','0001'), + 'datetimeoffset' => array('+02','96',' ','5092856',':00'), + 'smalldatetime' => array('00','1999','abc',':','06'), + ); +?> diff --git a/test/extended/MsSetup.inc b/test/extended/MsSetup.inc new file mode 100644 index 00000000..36be4308 --- /dev/null +++ b/test/extended/MsSetup.inc @@ -0,0 +1,58 @@ + $database, "UID" => $userName, "PWD" => $userPassword, "TraceOn" => false, "Driver" => $driver); +$daasMode = false; +$marsMode = true; + +$traceEnabled = false; + +$adServer = 'TARGET_AD_SERVER'; +$adDatabase = 'TARGET_AD_DATABASE'; +$adUser = 'TARGET_AD_USERNAME'; +$adPassword = 'TARGET_AD_PASSWORD'; + +if (isset($_ENV['MSSQL_SERVER']) || isset($_ENV['MSSQL_USER']) || isset($_ENV['MSSQL_PASSWORD'])) { + $server = $_ENV['MSSQL_SERVER']; + $uid = $_ENV['MSSQL_USER']; + $pwd = $_ENV['MSSQL_PASSWORD']; + $databaseName = $_ENV['MSSQL_DATABASE_NAME']; +} else { + $uid = $userName; + $pwd = $userPassword; + $databaseName = $database; +} + +// column encryption variables +$keystore = "none"; // key store provider, acceptable values are none, win, ksp, akv +$dataEncrypted = false; // whether data is to be encrypted + +// for Azure Key Vault +$AKVKeyStoreAuthentication = 'TARGET_AKV_AUTH'; // can be KeyVaultPassword or KeyVaultClientSecret +$AKVPrincipalName = 'TARGET_AKV_PRINCIPAL_NAME'; // for use with KeyVaultPassword +$AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPassword +$AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret +$AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret + +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; +?> \ No newline at end of file diff --git a/test/extended/pdo_AE_functions.inc b/test/extended/pdo_AE_functions.inc new file mode 100644 index 00000000..7f52c939 --- /dev/null +++ b/test/extended/pdo_AE_functions.inc @@ -0,0 +1,911 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = ($attestationType == 'disabled') ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = "sqlsrv:Server=$server;Database=$databaseName;ColumnEncryption=$attestationInfo"; + + if ($keystore == 'akv') { + + $securityInfo = ''; + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVPrincipalName"; + $securityInfo .= ";KeyStoreSecret=$AKVPassword"; + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVClientID"; + $securityInfo .= ";KeyStoreSecret=$AKVSecret"; + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options .= $securityInfo; + } + + try { + $conn = new PDO($options, $uid, $pwd); + } catch (PDOException $error) { + $e = $error->errorInfo; + checkErrors($e, array('CE400', '0')); + return false; + } + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + + $stmt = $conn->query($query); + if (!$stmt) { + print_r($conn->errorInfo()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = $stmt->fetch(); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->exec("DBCC FREEPROCCACHE"); + + unset($stmt); + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Insert the data using PDO::prepare() + try { + $stmt = $conn->prepare($insertQuery); + $i=1; + foreach ($dataTypes as $type) { + $PDOType = getPDOType($type); + if (!dataTypeIsBinary($type)) { + $stmt->bindParam($i, $testValues[$type][$v], $PDOType); + $stmt->bindParam($i+1, $testValues[$type][$v], $PDOType); + } else { + // unset() is necessary because otherwise the same data may be + // inserted into multiple binary columns. + unset($val); + $val=pack('H*', $testValues[$type][$v]); + $stmt->bindParam($i, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam($i+1, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + $i+=2; + } + $stmt->execute(); + } catch (PDOException $error) { + if (!$ceDisabled) { + print_r($error); + die("Inserting values in encrypted table failed\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } + } + } + + unset($stmt); +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + try { + $stmt = $conn->query($alterQuery); + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if ($sameKeyAndType) { + print_r($error); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r($error); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } + + unset($stmt); + + return true; +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + try { + $nonAEstmt->execute(); + } catch(Exception $error) { + print_r($error); + die("Executing non-AE computation statement failed!\n"); + } + + try { + $AEstmt->execute(); + } catch(Exception $error) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + $e = $error->errorInfo; + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + return; + } + + $AEres = $AEstmt->fetchAll(PDO::FETCH_NUM); + $nonAEres = $nonAEstmt->fetchAll(PDO::FETCH_NUM); + $AEcount = count($AEres); + $nonAEcount = count($nonAEres); + + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + return; + } elseif ($AEcount > $nonAEcount) { + print_r("Too many AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } elseif ($AEcount < $nonAEcount) { + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } else { + if ($AEcount != 0) { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr[0] != $nonAEres[$i][0]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i][0]." and non-AE result ".$nonAEres[$i][0]."\n"); + } + ++$i; + } + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the Latin1_General_BIN2 + // collation. If the COLLATE clause is left out, we get different + // results between the encrypted and non-encrypted columns (probably + // because the collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $unicode = dataTypeIsUnicode($type); + $PDOType = getPDOType($type); + unset($threshold); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + try { + $AEstmt = $conn->prepare($AEQuery); + $nonAEstmt = $conn->prepare($nonAEQuery); + + if (!dataTypeIsBinary($type)) { + $AEstmt->bindParam(1, $threshold, $PDOType); + $nonAEstmt->bindParam(1, $threshold, $PDOType); + } else { + $AEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $nonAEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + + $patternArray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternArray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $PDOType = getPDOType($type); + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + try { + $AEstmt = $conn->prepare($AEQuery); + $AEstmt->bindParam(1, $spattern, $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $spattern, $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the PDOException ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0] and $code[1]==$errors[1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPDOType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + return PDO::PARAM_INT; + case "bit": + return PDO::PARAM_BOOL; + case "real": + case "float": + case "double": + case "numeric": + case "time": + case "date": + case "datetime2": + case "datetime": + case "datetimeoffset": + case "smalldatetime": + case "money": + case "smallmoney"; + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return PDO::PARAM_STR; + case "binary": + case "varbinary": + case "varbinary(max)": + return PDO::PARAM_LOB; + default: + die("Case is missing for $type type in getPDOType.\n"); + } +} + +?> diff --git a/test/extended/pdo_aev2_plaintext_nonstring.phpt b/test/extended/pdo_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..81e47198 --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_plaintext_string.phpt b/test/extended/pdo_aev2_plaintext_string.phpt new file mode 100644 index 00000000..14bc276f --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..0821e8e3 --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..e016fbfd --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/skipif_not_hgs.inc b/test/extended/skipif_not_hgs.inc new file mode 100644 index 00000000..dd4614de --- /dev/null +++ b/test/extended/skipif_not_hgs.inc @@ -0,0 +1,36 @@ +$uid, "PWD"=>$pwd, "Driver" => $driver); + +$conn = sqlsrv_connect( $server, $connectionInfo ); +if ($conn === false) { + die( "skip Could not connect during SKIPIF." ); +} + +$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; +$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; +$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; + +if ($msodbcsql_maj < 17) { + die("skip Unsupported ODBC driver version"); +} + +if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { + die("skip Unsupported ODBC driver version"); +} + +// Get SQL Server +$server_info = sqlsrv_server_info($conn); +if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { + die("skip Server is not HGS enabled"); +} +?> diff --git a/test/extended/sqlsrv_AE_functions.inc b/test/extended/sqlsrv_AE_functions.inc new file mode 100644 index 00000000..5760b65f --- /dev/null +++ b/test/extended/sqlsrv_AE_functions.inc @@ -0,0 +1,991 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if ($key == $targetKey and $encryptionType == $targetType and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = $attestationType == 'disabled' ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'CharacterSet'=>'UTF-8', + 'ColumnEncryption'=>$attestationInfo, + 'TraceOn'=>true, + 'TraceOn'=>'c:\Users\davidp\Documents\SQL.LOG', + ); + + if ($keystore == 'akv') { + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVPrincipalName, + 'KeyStoreSecret'=>$AKVPassword, + ); + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVClientID, + 'KeyStoreSecret'=>$AKVSecret, + ); + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options = array_merge($options, $securityInfo); + } + + $conn = sqlsrv_connect($server, $options); + if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); + return false; + } + else + { + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + print_r(sqlsrv_errors()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = sqlsrv_fetch_array($stmt); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Enable rich computations + sqlsrv_query($conn, "DBCC traceon(127,-1);"); + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + } + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + global $length; + + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Use pack() on binary data + $params = array(); + foreach ($dataTypes as $type) { + $SQLType = getSQLType($type, $length); + $PHPType = getPHPType($type); + $val = dataTypeIsBinary($type) ? pack('H*', $testValues[$type][$v]) : $testValues[$type][$v]; + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + } + + // Insert the data using sqlsrv_prepare() + $stmt = sqlsrv_prepare($conn, $insertQuery, $params); + if ($stmt == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at prepare\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + if (sqlsrv_execute($stmt) == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at execute\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + sqlsrv_free_stmt($stmt); + } +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if ($sameKeyAndType) { + print_r(sqlsrv_errors()); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r(sqlsrv_errors()); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } else { + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + unset($stmt); + + return true; + } +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + if (!sqlsrv_execute($nonAEstmt)) { + print_r(sqlsrv_errors()); + die("Executing non-AE computation statement failed!\n"); + } + + if(!sqlsrv_execute($AEstmt)) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } else { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + return; + } + + while($AEres = sqlsrv_fetch_array($AEstmt, SQLSRV_FETCH_NUMERIC)) { + $nonAEres = sqlsrv_fetch_array($nonAEstmt, SQLSRV_FETCH_NUMERIC); + if (!$nonAEres) { + print_r($AEres); + print_r(sqlsrv_errors()); + print_r("Too many AE results for operation $comparison and data type $type!\n"); + } else { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr != $nonAEres[$i]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i]." and non-AE result ".$nonAEres[$i]."\n"); + print_r(sqlsrv_errors()); + } + ++$i; + } + } + } + + if ($rr = sqlsrv_fetch_array($nonAEstmt)) { + print_r($rr); + print_r(sqlsrv_errors()); + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the PHPTYPE to be specified to + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $unicode = dataTypeIsUnicode($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = getPHPType($type); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $param = array(array($threshold, SQLSRV_PARAM_IN, $phptype, getSQLType($type, $length))); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + $patternarray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternarray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $slength = $unicode ? iconv_strlen($spattern) : strlen($spattern); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + $sqltype = $unicode ? SQLSRV_SQLTYPE_NCHAR($slength) : SQLSRV_SQLTYPE_CHAR($slength); + + $param = array(array($spattern, SQLSRV_PARAM_IN, $phptype, $sqltype)); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the output of sqlsrv_errors() ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0][0] and $code[1]==$errors[0][1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPHPType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + case "bit": + return SQLSRV_PHPTYPE_INT; + break; + case "real": + case "float": + case "double": + return SQLSRV_PHPTYPE_FLOAT; + break; + case "numeric": + case "money": + case "smallmoney": + case "time": + case "date": + case "datetime": + case "datetime2": + case "datetimeoffset": + case "smalldatetime": + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + return SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR); + break; + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return SQLSRV_PHPTYPE_STRING('UTF-8'); + break; + case "binary": + case "varbinary": + case "varbinary(max)": + return SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY); + break; + default: + die("Case is missing for $type type in GetPHPType.\n"); + } +} + +function getSQLType($type, $length) +{ + switch($type) { + case "bigint": + return SQLSRV_SQLTYPE_BIGINT; + case "integer": + return SQLSRV_SQLTYPE_INT; + case "smallint": + return SQLSRV_SQLTYPE_SMALLINT; + case "tinyint": + return SQLSRV_SQLTYPE_TINYINT; + case "bit": + return SQLSRV_SQLTYPE_BIT; + case "real": + return SQLSRV_SQLTYPE_REAL; + case "float": + case "double": + return SQLSRV_SQLTYPE_FLOAT; + case "numeric": + return SQLSRV_SQLTYPE_NUMERIC(18,0); + case "time": + return SQLSRV_SQLTYPE_TIME; + case "date": + return SQLSRV_SQLTYPE_DATE; + case "datetime": + return SQLSRV_SQLTYPE_DATETIME; + case "datetime2": + return SQLSRV_SQLTYPE_DATETIME2; + case "datetimeoffset": + return SQLSRV_SQLTYPE_DATETIMEOFFSET; + case "smalldatetime": + return SQLSRV_SQLTYPE_SMALLDATETIME; + case "money": + return SQLSRV_SQLTYPE_MONEY; + case "smallmoney": + return SQLSRV_SQLTYPE_SMALLMONEY; + case "xml": + return SQLSRV_SQLTYPE_XML; + case "uniqueidentifier": + return SQLSRV_SQLTYPE_UNIQUEIDENTIFIER; + case "char": + return SQLSRV_SQLTYPE_CHAR($length); + case "varchar": + return SQLSRV_SQLTYPE_VARCHAR($length); + case "varchar(max)": + return SQLSRV_SQLTYPE_VARCHAR('max'); + case "nchar": + return SQLSRV_SQLTYPE_NCHAR($length); + case "nvarchar": + return SQLSRV_SQLTYPE_NVARCHAR($length); + case "nvarchar(max)": + return SQLSRV_SQLTYPE_NVARCHAR('max'); + case "binary": + return SQLSRV_SQLTYPE_BINARY($length); + break; + case "varbinary": + return SQLSRV_SQLTYPE_VARBINARY($length); + break; + case "varbinary(max)": + return SQLSRV_SQLTYPE_VARBINARY('max'); + break; + default: + die("Case is missing for $type type in getSQLType.\n"); + } +} + +?> diff --git a/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..a4f520f9 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_plaintext_string.phpt b/test/extended/sqlsrv_aev2_plaintext_string.phpt new file mode 100644 index 00000000..b258e534 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..5574b425 --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..6d2635eb --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/break_pdo.php b/test/functional/pdo_sqlsrv/break_pdo.php index c5287a33..0bddb36b 100644 --- a/test/functional/pdo_sqlsrv/break_pdo.php +++ b/test/functional/pdo_sqlsrv/break_pdo.php @@ -22,24 +22,24 @@ function generateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) $stmt = $conn->query($sql); // Insert data - $sql = "INSERT INTO $tableName1 VALUES ( ?, ? )"; + $sql = "INSERT INTO $tableName1 VALUES (?, ?)"; for ($t = 100; $t < 116; $t++) { $stmt = $conn->prepare($sql); $ts = substr(sha1($t), 0, 5); - $params = array( $t,$ts ); + $params = array($t, $ts); $stmt->execute($params); } // Create table - $sql = "CREATE TABLE $tableName2 ( c1 INT, c2 VARCHAR(40) )"; + $sql = "CREATE TABLE $tableName2 (c1 INT, c2 VARCHAR(40))"; $stmt = $conn->query($sql); // Insert data - $sql = "INSERT INTO $tableName2 VALUES ( ?, ? )"; + $sql = "INSERT INTO $tableName2 VALUES (?, ?)"; for ($t = 200; $t < 209; $t++) { $stmt = $conn->prepare($sql); $ts = substr(sha1($t), 0, 5); - $params = array( $t,$ts ); + $params = array($t, $ts); $stmt->execute($params); } @@ -52,8 +52,12 @@ function generateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) // Break connection by getting the session ID and killing it. // Note that breaking a connection and testing reconnection requires a // TCP/IP protocol connection (as opposed to a Shared Memory protocol). +// Wait one second before and after breaking to ensure the break occurs +// in the correct order, otherwise there may be timing issues in Linux +// that can cause tests to fail intermittently and unpredictably. function breakConnection($conn, $conn_break) { + sleep(1); $stmt1 = $conn->query("SELECT @@SPID"); $obj = $stmt1->fetch(PDO::FETCH_NUM); $spid = $obj[0]; @@ -69,11 +73,11 @@ function dropTables($server, $uid, $pwd, $tableName1, $tableName2) $conn = new PDO("sqlsrv:server = $server ; Database = $dbName ;", $uid, $pwd); - $query="IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; - $stmt=$conn->query($query); + $query = "IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; + $stmt = $conn->query($query); - $query="IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; - $stmt=$conn->query($query); + $query = "IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; + $stmt = $conn->query($query); } dropTables($server, $uid, $pwd, $tableName1, $tableName2); diff --git a/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt b/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt index 7d5627a2..60273f34 100644 --- a/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt +++ b/test/functional/pdo_sqlsrv/pdo_ae_insert_numeric.phpt @@ -9,6 +9,9 @@ No PDO::PARAM_ tpe specified when binding parameters require_once("MsCommon_mid-refactor.inc"); require_once("AEData.inc"); $dataTypes = array("bit", "tinyint", "smallint", "int", "bigint", "decimal(18,5)", "numeric(10,5)", "float", "real"); + +// Note the size of a float is platform dependent, with a precision of roughly 14 digits +// http://php.net/manual/en/language.types.float.php try { $conn = connect(); foreach ($dataTypes as $dataType) { @@ -26,7 +29,7 @@ try { if ($r === false) { isIncompatibleTypesError($stmt, $dataType, "default type"); } else { - echo "****Encrypted default type is compatible with encrypted $dataType****\n"; + echo "-----Encrypted default type is compatible with encrypted $dataType-----\n"; fetchAll($conn, $tbname); } dropTable($conn, $tbname); @@ -37,49 +40,49 @@ try { echo $e->getMessage(); } ?> ---EXPECT-- +--EXPECTREGEX-- Testing bit: -****Encrypted default type is compatible with encrypted bit**** +-----Encrypted default type is compatible with encrypted bit----- c_det: 1 c_rand: 0 Testing tinyint: -****Encrypted default type is compatible with encrypted tinyint**** +-----Encrypted default type is compatible with encrypted tinyint----- c_det: 0 c_rand: 255 Testing smallint: -****Encrypted default type is compatible with encrypted smallint**** +-----Encrypted default type is compatible with encrypted smallint----- c_det: -32767 c_rand: 32767 Testing int: -****Encrypted default type is compatible with encrypted int**** +-----Encrypted default type is compatible with encrypted int----- c_det: -2147483647 c_rand: 2147483647 Testing bigint: -****Encrypted default type is compatible with encrypted bigint**** +-----Encrypted default type is compatible with encrypted bigint----- c_det: -922337203685479936 c_rand: 922337203685479936 -Testing decimal(18,5): -****Encrypted default type is compatible with encrypted decimal(18,5)**** -c_det: -9223372036854.80000 -c_rand: 9223372036854.80000 +Testing decimal\(18,5\): +-----Encrypted default type is compatible with encrypted decimal\(18,5\)----- +c_det: -9223372036854\.80000 +c_rand: 9223372036854\.80000 -Testing numeric(10,5): -****Encrypted default type is compatible with encrypted numeric(10,5)**** -c_det: -21474.83647 -c_rand: 21474.83647 +Testing numeric\(10,5\): +-----Encrypted default type is compatible with encrypted numeric\(10,5\)----- +c_det: -21474\.83647 +c_rand: 21474\.83647 Testing float: -****Encrypted default type is compatible with encrypted float**** -c_det: -9223372036.8547993 -c_rand: 9223372036.8547993 +-----Encrypted default type is compatible with encrypted float----- +c_det: (-9223372036\.8547993|-9223372036\.8547992) +c_rand: (9223372036\.8547993|9223372036\.8547992) Testing real: -****Encrypted default type is compatible with encrypted real**** -c_det: -2147.4829 -c_rand: 2147.4829 +-----Encrypted default type is compatible with encrypted real----- +c_det: (-2147\.4829|-2147\.483) +c_rand: (2147\.4829|2147\.483) diff --git a/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt new file mode 100644 index 00000000..a10bf6c3 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_buffered_fetch_types.phpt @@ -0,0 +1,229 @@ +--TEST-- +Prepare with cursor buffered and fetch a variety of types converted to different types +--DESCRIPTION-- +Test various conversion functionalites for buffered queries with PDO_SQLSRV. +--SKIPIF-- + +--FILE-- +prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + + // Fetch all fields as UTF-8 strings + for ($i = 0; $i < count($inputs); $i++) { + $stmt->execute(); + $f = $stmt->fetchColumn($i); + + if ($f !== $inputs[$i]) { + var_dump($f); + } + } + } catch (PdoException $e) { + echo "Caught exception in fetchAsUTF8:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchArray($conn, $tableName, $inputs) +{ + $query = "SELECT * FROM $tableName"; + try { + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + // By default, even numeric or datetime fields are fetched as strings + $result = $stmt->fetch(PDO::FETCH_NUM); + for ($i = 0; $i < count($inputs); $i++) { + if ($result[$i] !== $inputs[$i]) { + var_dump($f); + } + } + } catch (PdoException $e) { + echo "Caught exception in fetchArray:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchBinaryAsNumber($conn, $tableName, $inputs) +{ + global $violation; + + $query = "SELECT c1 FROM $tableName"; + + try { + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED, PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE=>true)); + $stmt->execute(); + + $stmt->bindColumn('c1', $binaryValue, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + echo "in fetchBinaryAsNumber: exception should have been thrown!\n"; + } catch (PdoException $e) { + // The varbinary field - expect the violation error + if (strpos($e->getMessage(), $violation) === false) { + echo "in fetchBinaryAsNumber: expected '$violation' but caught this:\n"; + echo $e->getMessage() . PHP_EOL; + } + } +} + +function fetchBinaryAsBinary($conn, $tableName, $inputs) +{ + try { + $query = "SELECT c1 FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c1', $binaryValue, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + if ($binaryValue !== $inputs[0]) { + echo "Fetched binary value unexpected: $binaryValue\n"; + } + } catch (PdoException $e) { + echo "Caught exception in fetchBinaryAsBinary:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchFloatAsInt($conn, $tableName) +{ + global $truncation; + + try { + $query = "SELECT c3 FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c3', $floatValue, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + // This should return SQL_SUCCESS_WITH_INFO with the truncation error + $info = $stmt->errorInfo(); + if ($info[0] != '01S07' || $info[2] !== $truncation) { + print_r($stmt->errorInfo()); + } + } catch (PdoException $e) { + echo "Caught exception in fetchFloatAsInt:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +function fetchCharAsInt($conn, $tableName, $column) +{ + global $outOfRange; + + try { + $query = "SELECT $column FROM $tableName"; + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn($column, $value, PDO::PARAM_INT); + $row = $stmt->fetch(PDO::FETCH_BOUND); + + // TODO 11297: fix this part outside Windows later + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + echo "in fetchCharAsInt: exception should have been thrown!\n"; + } else { + if ($value != 0) { + var_dump($value); + } + } + } catch (PdoException $e) { + // The (n)varchar field - expect the outOfRange error + if (strpos($e->getMessage(), $outOfRange) === false) { + echo "in fetchCharAsInt ($column): expected '$outOfRange' but caught this:\n"; + echo $e->getMessage() . PHP_EOL; + } + } +} + +function fetchAsNumerics($conn, $tableName, $inputs) +{ + // The following calls expect different errors + fetchFloatAsInt($conn, $tableName); + fetchCharAsInt($conn, $tableName, 'c6'); + fetchCharAsInt($conn, $tableName, 'c7'); + + // The following should work + try { + $query = "SELECT c2, c4 FROM $tableName"; + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + + $stmt = $conn->prepare($query, array(PDO::ATTR_CURSOR=>PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE=>PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + + $stmt->bindColumn('c2', $intValue, PDO::PARAM_INT); + $stmt->bindColumn('c4', $decValue, PDO::PARAM_INT); + + $row = $stmt->fetch(PDO::FETCH_BOUND); + + if ($intValue !== intval($inputs[1])) { + var_dump($intValue); + } + if ($decValue !== intval($inputs[3])) { + var_dump($decValue); + } + } catch (PdoException $e) { + echo "Caught exception in fetchAsNumerics:\n"; + echo $e->getMessage() . PHP_EOL; + } +} + +try { + $conn = connect(); + $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false); + + $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6', 'c7'); + $types = array('varbinary(10)', 'int', 'float(53)', 'decimal(16, 6)', 'datetime2', 'varchar(50)', 'nvarchar(50)'); + $inputs = array('abcdefghij', '34567', '9876.5432', '123456789.012340', '2020-02-02 20:20:20.2220000', 'This is a test', 'Şơмė śäოрŀề'); + + // Create table + $colMeta = array(new ColumnMeta($types[0], $columns[0]), + new ColumnMeta($types[1], $columns[1]), + new ColumnMeta($types[2], $columns[2]), + new ColumnMeta($types[3], $columns[3]), + new ColumnMeta($types[4], $columns[4]), + new ColumnMeta($types[5], $columns[5]), + new ColumnMeta($types[6], $columns[6])); + createTable($conn, $tableName, $colMeta); + + // Prepare the input values and insert one row + $query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + if ($i == 0) { + $stmt->bindParam($i+1, $inputs[$i], PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + } else { + $stmt->bindParam($i+1, $inputs[$i]); + } + } + $stmt->execute(); + unset($stmt); + + // Starting fetching using client buffers + fetchAsUTF8($conn, $tableName, $inputs); + fetchArray($conn, $tableName, $inputs); + fetchBinaryAsNumber($conn, $tableName, $inputs); + fetchBinaryAsBinary($conn, $tableName, $inputs); + fetchAsNumerics($conn, $tableName, $inputs); + + // dropTable($conn, $tableName); + echo "Done\n"; + unset($conn); +} catch (PdoException $e) { + echo $e->getMessage() . PHP_EOL; +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt b/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt index 124df22b..4ca738b5 100644 --- a/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt +++ b/test/functional/pdo_sqlsrv/pdo_connect_encrypted.phpt @@ -1,57 +1,63 @@ --TEST-- Test new connection keyword ColumnEncryption +--DESCRIPTION-- +Some test cases return errors as expected. For testing purposes, an enclave enabled +SQL Server and the HGS server are the same instance. If the server is HGS enabled, +the error message of one test case is not the same. --SKIPIF-- --FILE-- getAttribute( PDO::ATTR_CLIENT_VERSION )['DriverVer']; - $msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; -} -catch( PDOException $e ) -{ +try { + $conn = new PDO("sqlsrv:server = $server", $uid, $pwd); + $msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)['DriverVer']; + $version = explode(".", $msodbcsqlVer); + $msodbcsqlMaj = $version[0]; + + // Next, check if the server is HGS enabled + $serverInfo = $conn->getAttribute(PDO::ATTR_SERVER_INFO); + if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { + $hgsEnabled = false; + } +} catch (PDOException $e) { echo "Failed to connect\n"; - print_r( $e->getMessage() ); + print_r($e->getMessage()); echo "\n"; } -test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ); +testColumnEncryption($server, $uid, $pwd, $msodbcsqlMaj); echo "Done"; -function verify_output( $PDOerror, $expected ) +function verifyOutput($PDOerror, $expected, $caseNum) { - if( strpos( $PDOerror->getMessage(), $expected ) === false ) - { - print_r( $PDOerror->getMessage() ); + if (strpos($PDOerror->getMessage(), $expected) === false) { + echo "Test case $caseNum failed:\n"; + print_r($PDOerror->getMessage()); echo "\n"; } } -function test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ) +function testColumnEncryption($server, $uid, $pwd, $msodbcsqlMaj) { + global $hgsEnabled; + // Only works for ODBC 17 //////////////////////////////////////// $connectionInfo = "ColumnEncryption = Enabled;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - if($msodbcsql_maj < 17) - { + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + if ($msodbcsqlMaj < 17) { $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; - verify_output( $e, $expected ); - } - else - { - print_r( $e->getMessage() ); + verifyOutput($e, $expected, "1"); + } else { + echo "Test case 1 failed:\n"; + print_r($e->getMessage()); echo "\n"; } } @@ -59,50 +65,42 @@ function test_ColumnEncryption( $server, $uid, $pwd, $msodbcsql_maj ) // Works for ODBC 17, ODBC 13 //////////////////////////////////////// $connectionInfo = "ColumnEncryption = Disabled;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - if($msodbcsql_maj < 13) - { + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + if ($msodbcsqlMaj < 13) { $expected = "Invalid connection string attribute"; - verify_output( $e, $expected ); - } - else - { - print_r( $e->getMessage() ); + verifyOutput($e, $expected, "2"); + } else { + echo "Test case 2 failed:\n"; + print_r($e->getMessage()); echo "\n"; } } // should fail for all ODBC drivers + $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; + if ($hgsEnabled) { + $expected = "Requested attestation protocol is invalid."; + } + //////////////////////////////////////// $connectionInfo = "ColumnEncryption = false;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); - } - catch( PDOException $e ) - { - $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - verify_output( $e, $expected ); + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + verifyOutput($e, $expected, "3"); } // should fail for all ODBC drivers //////////////////////////////////////// $connectionInfo = "ColumnEncryption = 1;"; - try - { - $conn = new PDO( "sqlsrv:server = $server ; $connectionInfo", $uid, $pwd ); + try { + $conn = new PDO("sqlsrv:server = $server ; $connectionInfo", $uid, $pwd); + } catch (PDOException $e) { + verifyOutput($e, $expected, "4"); } - catch( PDOException $e ) - { - $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - verify_output( $e, $expected ); - } -} +} ?> --EXPECT-- Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt b/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt new file mode 100644 index 00000000..46ea1cdf --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_connection_logs.phpt @@ -0,0 +1,65 @@ +--TEST-- +Test simple logging with connection, simple query and then close +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + return $conn; +} + +try { + ini_set('log_errors', '1'); + + $logFilename = 'php_errors.log'; + $logFilepath = dirname(__FILE__).'/'.$logFilename; + if (file_exists($logFilepath)) { + unlink($logFilepath); + } + + ini_set('error_log', $logFilepath); + ini_set('pdo_sqlsrv.log_severity', '-1'); + + $conn = toConnect(); + $stmt = $conn->query("SELECT @@Version"); + + // Ignore the fetch results + $stmt->fetchAll(); + + unset($conn); + + if (file_exists($logFilepath)) { + echo file_get_contents($logFilepath); + unlink($logFilepath); + } else { + echo "$logFilepath is missing!\n"; + } + + echo "Done\n"; +} catch (Exception $e) { + var_dump($e); +} + +?> +--EXPECTF-- +[%s UTC] pdo_sqlsrv_db_handle_factory: entering +[%s UTC] pdo_sqlsrv_db_handle_factory: SQLSTATE = 01000 +[%s UTC] pdo_sqlsrv_db_handle_factory: error code = 5701 +[%s UTC] pdo_sqlsrv_db_handle_factory: message = %s[SQL Server]Changed database context to '%s'. +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +[%s UTC] pdo_sqlsrv_stmt_describe_col: entering +[%s UTC] pdo_sqlsrv_stmt_fetch: entering +[%s UTC] pdo_sqlsrv_stmt_get_col_data: entering +[%s UTC] pdo_sqlsrv_stmt_fetch: entering +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt b/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt new file mode 100644 index 00000000..a0169696 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_errorMode_logs.phpt @@ -0,0 +1,116 @@ +--TEST-- +Test different error modes. The queries will try to do a select on a non-existing table +--DESCRIPTION-- +This is similar to pdo_errorMode.phpt but will display the contents of php +error logs based on log severity. +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + global $sql; + try { + $q = $conn->query($sql); + } catch (Exception $e) { + // do nothing + } +} + +function testWarning($conn) +{ + // This forces PHP to log errors rather than displaying errors + // on screen -- only required for PDO::ERRMODE_WARNING + ini_set('display_errors', '0'); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + + global $sql; + $q = $conn->query($sql); +} + +function runtests($severity) +{ + global $conn; + + $logFilename = 'php_errors' . $severity . '.log'; + $logFilepath = dirname(__FILE__).'/'.$logFilename; + if (file_exists($logFilepath)) { + unlink($logFilepath); + } + + ini_set('error_log', $logFilepath); + ini_set('pdo_sqlsrv.log_severity', $severity); + + if ($severity === '2' ) { + testWarning($conn); + } else { + testException($conn); + } + + if (file_exists($logFilepath)) { + if ($severity == '0') { + echo "$logFilepath should not exist\n"; + } + echo file_get_contents($logFilepath); + unlink($logFilepath); + } + + // Now reset logging by disabling it + ini_set('pdo_sqlsrv.log_severity', '0'); + echo "Done with $severity\n\n"; +} + +try { + ini_set('log_errors', '1'); + ini_set('pdo_sqlsrv.log_severity', '0'); + + $conn = toConnect(); + $sql = "SELECT * FROM temp_table"; + + runtests('0'); + runtests('1'); + runtests('2'); + runtests('4'); + runtests('-1'); +} catch (Exception $e) { + var_dump($e); +} + +?> +--EXPECTF-- +Done with 0 + +[%s UTC] pdo_sqlsrv_stmt_execute: SQLSTATE = 42S02 +[%s UTC] pdo_sqlsrv_stmt_execute: error code = 208 +[%s UTC] pdo_sqlsrv_stmt_execute: message = %s[SQL Server]Invalid object name 'temp_table'. +Done with 1 + +[%s UTC] PHP Warning: PDO::query(): SQLSTATE[42S02]: Base table or view not found: 208 %s[SQL Server]Invalid object name 'temp_table'. in %spdo_errorMode_logs.php on line %d +Done with 2 + +[%s UTC] pdo_sqlsrv_stmt_dtor: entering +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +Done with 4 + +[%s UTC] pdo_sqlsrv_stmt_dtor: entering +[%s UTC] pdo_sqlsrv_dbh_prepare: entering +[%s UTC] pdo_sqlsrv_stmt_execute: entering +[%s UTC] pdo_sqlsrv_stmt_execute: SQLSTATE = 42S02 +[%s UTC] pdo_sqlsrv_stmt_execute: error code = 208 +[%s UTC] pdo_sqlsrv_stmt_execute: message = %s[SQL Server]Invalid object name 'temp_table'. +Done with -1 + diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt index 93899043..d433b911 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_decimal.phpt @@ -80,34 +80,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 422 [c2_money] => 132.2220 - [c3_float] => 622.22000000000003 + [c3_float] => 622.22%S ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt index eea43902..dee4ff59 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_float.phpt @@ -79,34 +79,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 411 [c2_money] => 131.1100 - [c3_float] => 611.11099999999999 + [c3_float] => 611.1109999999999%d ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt index ee4b3699..ae581204 100644 --- a/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt +++ b/test/functional/pdo_sqlsrv/pdo_prepare_emulatePrepare_money.phpt @@ -80,34 +80,34 @@ try { } ?> ---EXPECT-- +--EXPECTF-- Prepare without emulate prepare: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and no bind param options: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_SYSTEM: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_UTF8: Array ( [c1_decimal] => 433 [c2_money] => 133.3333 - [c3_float] => 633.33333000000005 + [c3_float] => 633.3333300000000%d ) Prepare with emulate prepare and SQLSRV_ENCODING_BINARY: No results for this query diff --git a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt index cd709eb1..37866235 100644 --- a/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt +++ b/test/functional/pdo_sqlsrv/pdo_test_non_LOB_types.phpt @@ -47,6 +47,10 @@ try { verifyResult($result); // test not streamable types + // The size of a float is platform dependent, with a precision of roughly 14 digits + // http://php.net/manual/en/language.types.float.php + // For example, the input value for column [real_type] in setup\test_types.sql is 1.18E-38 + // but in some distros the fetched value is 1.1799999E-38 $tsql = "SELECT * FROM [test_types]"; $stmt = $conn->query($tsql); $result = $stmt->fetch(PDO::FETCH_NUM); @@ -60,19 +64,19 @@ unset($stmt); unset($conn); ?> ---EXPECT-- +--EXPECTREGEX-- Array -( - [0] => 9223372036854775807 - [1] => 2147483647 - [2] => 32767 - [3] => 255 - [4] => 1 - [5] => 9999999999999999999999999999999999999 - [6] => 922337203685477.5807 - [7] => 214748.3647 - [8] => 1.79E+308 - [9] => 1.1799999E-38 - [10] => 1968-12-12 16:20:00.000 - [11] => -) \ No newline at end of file +\( + \[0\] => 9223372036854775807 + \[1\] => 2147483647 + \[2\] => 32767 + \[3\] => 255 + \[4\] => 1 + \[5\] => 9999999999999999999999999999999999999 + \[6\] => 922337203685477\.5807 + \[7\] => 214748\.3647 + \[8\] => 1\.79E\+308 + \[9\] => (1\.18E-38|1\.1799999E-38) + \[10\] => 1968-12-12 16:20:00.000 + \[11\] => +\) \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt index 5f1a4f43..6a30232c 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_fetchAll.phpt @@ -180,9 +180,9 @@ array(2) { [5]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." [7]=> @@ -395,7 +395,7 @@ object(stdClass)#%x (%x) { ["NVarCharCol"]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." } @@ -414,7 +414,7 @@ array(8) { [5]=> string(10) "STRINGCOL2" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [7]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." } @@ -425,7 +425,7 @@ string(10) "STRINGCOL2" string(23) "2000-11-11 11:11:11.223" string(10) "STRINGCOL2" string(10) "STRINGCOL2" -string(18) "222.22200000000001" +string(%d) "222.222%S" string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." Test_9 : FETCH_INVALID : diff --git a/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt b/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt index f235ed94..fa63c225 100644 --- a/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt +++ b/test/functional/pdo_sqlsrv/pdostatement_nextRowset.phpt @@ -31,7 +31,7 @@ try { var_dump($e); } ?> ---EXPECT-- +--EXPECTF-- array(1) { [0]=> array(62) { @@ -224,9 +224,9 @@ array(2) { [5]=> string(10) "STRINGCOL2" ["FloatCol"]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" [6]=> - string(18) "222.22200000000001" + string(%d) "222.222%S" ["XmlCol"]=> string(431) " 2 This is a really large string used to test certain large data types like xml data type. The length of this string is greater than 256 to correctly test a large data type. This is currently used by atleast varchar type and by xml type. The fetch tests are the primary consumer of this string to validate that fetch on large types work fine. The length of this string as counted in terms of number of characters is 417." [7]=> diff --git a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc index dd4614de..80ad95f0 100644 --- a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc +++ b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc @@ -3,34 +3,34 @@ // SQL Server, and a HGS server. The HGS server and SQL Server // are the same for testing purposes. -if (!extension_loaded("sqlsrv")) { +if (!extension_loaded("pdo_sqlsrv")) { die("skip Extension not loaded"); } -require_once("MsSetup.inc"); +require_once('MsSetup.inc'); -$connectionInfo = array("UID"=>$uid, "PWD"=>$pwd, "Driver" => $driver); - -$conn = sqlsrv_connect( $server, $connectionInfo ); -if ($conn === false) { - die( "skip Could not connect during SKIPIF." ); +$conn = new PDO("sqlsrv:server = $server", $uid, $pwd); +if (!$conn) { + die("skip Could not connect during SKIPIF."); } -$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; -$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; -$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; +$msodbcsqlVer = $conn->getAttribute(PDO::ATTR_CLIENT_VERSION)['DriverVer']; +$version = explode(".", $msodbcsqlVer); -if ($msodbcsql_maj < 17) { +$msodbcsqlMaj = $version[0]; +$msodbcsqlMin = $version[1]; + +if ($msodbcsqlMaj < 17) { die("skip Unsupported ODBC driver version"); } -if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { +if ($msodbcsqlMin < 4 and $msodbcsqlMaj == 17) { die("skip Unsupported ODBC driver version"); } // Get SQL Server -$server_info = sqlsrv_server_info($conn); -if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { +$serverInfo = $conn->getAttribute(PDO::ATTR_SERVER_INFO); +if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { die("skip Server is not HGS enabled"); } -?> +?> \ No newline at end of file diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index ead94a30..69844169 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -42,6 +42,8 @@ if __name__ == '__main__': parser.add_argument('-dbname', '--DBNAME', required=True) parser.add_argument('-azure', '--AZURE', required=False, default='no') args = parser.parse_args() + + print("Start\n") try: server = os.environ['TEST_PHP_SQL_SERVER'] @@ -61,10 +63,13 @@ if __name__ == '__main__': if (args.AZURE.lower() == 'no'): manageTestDB('create_db.sql', conn_options, args.DBNAME) + print("About to set up databases...\n") # create tables in the new database setupTestDatabase(conn_options, args.DBNAME, args.AZURE) + print("About to populate tables...\n") # populate these tables populateTables(conn_options, args.DBNAME) + print("About to set up encryption...\n") # setup AE (certificate, column master key and column encryption key) setupAE(conn_options, args.DBNAME) diff --git a/test/functional/sqlsrv/53_0021.phpt b/test/functional/sqlsrv/53_0021.phpt index 8d010299..c9bbf201 100644 --- a/test/functional/sqlsrv/53_0021.phpt +++ b/test/functional/sqlsrv/53_0021.phpt @@ -12,7 +12,12 @@ Test for integer, float, and datetime types vs various sql server types. require( 'MsCommon.inc' ); -function get_fields( $stmt ) { +$epsilon = 0.00001; +$decimals = ['9999999999999999999999999999999999999', '-10000000000000000000000000000000000001', '0']; + +function get_fields( $stmt, $round ) { + + global $epsilon, $decimals; // bigint $field = sqlsrv_get_field( $stmt, 0, SQLSRV_PHPTYPE_INT ); @@ -71,7 +76,22 @@ function get_fields( $stmt ) { } else { var_dump( sqlsrv_errors( SQLSRV_ERR_WARNINGS ) ); - echo "$field\n"; + // The size of a float is platform dependent, with a precision of roughly 14 digits + // http://php.net/manual/en/language.types.float.php + // For example, in Ubuntu 18.04 or macOS Mojave the returned value is 1.0E+37 or -1.0E+37 + // but in Alpine Linux it is 9.9999999999997E+36 or -9.9999999999997E+36 + if ($decimals[$round] == '0') { + if ($field != 0) { + echo "Expected 0 but got $field\n"; + } + } else { + $expected = floatval($decimals[$round]); + $diff = abs(($field - $expected) / $expected); + + if ($diff > $epsilon) { + echo "Expected $expected but got $field -- difference is $diff\n"; + } + } } // datetime @@ -147,7 +167,7 @@ function get_fields( $stmt ) { } // maximum values - get_fields( $stmt ); + get_fields( $stmt, 0 ); $success = sqlsrv_fetch( $stmt ); if( !$success ) { @@ -156,7 +176,7 @@ function get_fields( $stmt ) { } // minimum values - get_fields( $stmt ); + get_fields( $stmt, 1 ); $success = sqlsrv_fetch( $stmt ); if( !$success ) { @@ -165,7 +185,7 @@ function get_fields( $stmt ) { } // zero values - get_fields( $stmt ); + get_fields( $stmt, 2 ); $stmt = sqlsrv_query( $conn, "SELECT int_type, decimal_type, datetime_type, real_type FROM [test_types]" ); if( !$stmt ) { @@ -258,7 +278,6 @@ NULL NULL 1 NULL -1.0E+37 NULL 12/12/1968 04:20:00 NULL @@ -296,7 +315,6 @@ NULL NULL 0 NULL --1.0E+37 NULL 12/12/1968 04:20:00 NULL @@ -319,7 +337,6 @@ NULL NULL 0 NULL -0 NULL 12/12/1968 04:20:00 NULL diff --git a/test/functional/sqlsrv/AEData.inc b/test/functional/sqlsrv/AEData.inc index 3879efce..46dfc6c5 100644 --- a/test/functional/sqlsrv/AEData.inc +++ b/test/functional/sqlsrv/AEData.inc @@ -84,11 +84,9 @@ $sqlTypes = array( function is_incompatible_types_error( $dataType, $sqlType ) { $errors = sqlsrv_errors(); - foreach ( $errors as $error ) - { + foreach ($errors as $error) { // 22018 is the SQLSTATE for the operand crash error for incompatible types - if ( $error['SQLSTATE'] == 22018 ) - { + if ($error['SQLSTATE'] == '22018') { echo "Encrypted $sqlType is incompatible with encrypted $dataType\n"; } } @@ -109,7 +107,6 @@ function get_sqlType_constant( $sqlType ) { switch ( $sqlType ) { case 'SQLSRV_SQLTYPE_BIGINT': - case 'SQLSRV_SQLTYPE_BINARY': case 'SQLSRV_SQLTYPE_BIT': case 'SQLSRV_SQLTYPE_DATE': case 'SQLSRV_SQLTYPE_DATETIME': @@ -135,6 +132,10 @@ function get_sqlType_constant( $sqlType ) case 'SQLSRV_SQLTYPE_XML': return constant( $sqlType ); break; + case 'SQLSRV_SQLTYPE_BINARY': + // our tests always use precision 5 for SQLSRV_SQLTYPE_BINARY + return SQLSRV_SQLTYPE_BINARY(5); + break; case 'SQLSRV_SQLTYPE_CHAR': // our tests always use precision 5 for SQLSRV_SQLTYPE_CHAR return SQLSRV_SQLTYPE_CHAR(5); @@ -146,7 +147,7 @@ function get_sqlType_constant( $sqlType ) case 'SQLSRV_SQLTYPE_NCHAR': // our tests always use precision 5 for SQLSRV_SQLTYPE_NCHAR return SQLSRV_SQLTYPE_NCHAR(5); - break; + break; case 'SQLSRV_SQLTYPE_NUMERIC': // our tests always use precision 10 scale 5 for SQLSRV_SQLTYPE_NUMERIC return SQLSRV_SQLTYPE_NUMERIC(10, 5); @@ -157,7 +158,7 @@ function get_sqlType_constant( $sqlType ) } } -function isDateTimeType( $sqlType ) +function isDateTimeType($sqlType) { return ($sqlType == 'SQLSRV_SQLTYPE_DATE' || $sqlType == 'SQLSRV_SQLTYPE_DATETIME' || @@ -167,4 +168,20 @@ function isDateTimeType( $sqlType ) $sqlType == 'SQLSRV_SQLTYPE_TIME'); } +function isLOBType($sqlType) +{ + return ($sqlType == 'SQLSRV_SQLTYPE_TEXT' || $sqlType == 'SQLSRV_SQLTYPE_NTEXT' || $sqlType == 'SQLSRV_SQLTYPE_IMAGE'); +} + +function isCompatible($compatList, $dataType, $sqlType) +{ + foreach ($compatList[$dataType] as $compatType) { + if (stripos($compatType, $sqlType) !== false) { + return true; + } + } + + return false; +} + ?> diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index dec27c55..195430d5 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -84,6 +84,12 @@ function isDaasMode() return ($daasMode ? true : false); } +function isLocaleDisabled() +{ + global $daasMode, $localeDisabled; + return ($daasMode || $localeDisabled); +} + function isSQLAzure() { // 'SQL Azure' indicates SQL Database or SQL Data Warehouse @@ -491,10 +497,11 @@ function handleErrors() function setUSAnsiLocale() { - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return; } + if (!isWindows()) { // macOS the locale names are different in Linux or macOS $locale = strtoupper(PHP_OS) === 'LINUX' ? "en_US.ISO-8859-1" : "en_US.ISO8859-1"; @@ -505,8 +512,8 @@ function setUSAnsiLocale() function resetLocaleToDefault() { - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return; } // Like setUSAnsiLocale() above, this method is only needed in non-Windows environment @@ -522,8 +529,8 @@ function isLocaleSupported() if (isWindows()) { return true; } - // Do not run locale tests in Azure - if (isDaasMode()) { + // Do not run locale tests if locale disabled + if (isLocaleDisabled()) { return false; } if (AE\isDataEncrypted()) { diff --git a/test/functional/sqlsrv/MsSetup.inc b/test/functional/sqlsrv/MsSetup.inc index 8335c13b..f328e17c 100644 --- a/test/functional/sqlsrv/MsSetup.inc +++ b/test/functional/sqlsrv/MsSetup.inc @@ -25,6 +25,7 @@ $daasMode = false; $marsMode = true; $traceEnabled = false; +$localeDisabled = false; $adServer = 'TARGET_AD_SERVER'; $adDatabase = 'TARGET_AD_DATABASE'; diff --git a/test/functional/sqlsrv/break.php b/test/functional/sqlsrv/break.php index 97abcccf..b7bdc43b 100644 --- a/test/functional/sqlsrv/break.php +++ b/test/functional/sqlsrv/break.php @@ -12,79 +12,79 @@ $tableName2 = "test_connres2"; // Using generated tables will eventually allow us to put the // connection resiliency tests on Github, since the integrated testing // from AppVeyor does not have AdventureWorks. -function GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ) +function GenerateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2) { - $connectionInfo = array( "Database"=>$dbName, "uid"=>$uid, "pwd"=>$pwd ); + $connectionInfo = array("Database"=>$dbName, "uid"=>$uid, "pwd"=>$pwd); - $conn = sqlsrv_connect( $server, $connectionInfo ); - if ( $conn === false ) - { - die ( print_r( sqlsrv_errors() ) ); + $conn = sqlsrv_connect($server, $connectionInfo); + if ($conn === false) { + die (print_r(sqlsrv_errors())); } // Create table - $sql = "CREATE TABLE $tableName1 ( c1 INT, c2 VARCHAR(40) )"; - $stmt = sqlsrv_query( $conn, $sql ); + $sql = "CREATE TABLE $tableName1 (c1 INT, c2 VARCHAR(40))"; + $stmt = sqlsrv_query($conn, $sql); // Insert data - $sql = "INSERT INTO $tableName1 VALUES ( ?, ? )"; - for( $t = 100; $t < 116; $t++ ) - { - $ts = substr( sha1( $t ),0,5 ); - $params = array( $t,$ts ); - $stmt = sqlsrv_prepare( $conn, $sql, $params ); - sqlsrv_execute( $stmt ); + $sql = "INSERT INTO $tableName1 VALUES (?, ?)"; + for ($t = 100; $t < 116; $t++) { + $ts = substr(sha1($t), 0, 5); + $params = array($t, $ts); + $stmt = sqlsrv_prepare($conn, $sql, $params); + sqlsrv_execute($stmt); } // Create table - $sql = "CREATE TABLE $tableName2 ( c1 INT, c2 VARCHAR(40) )"; - $stmt = sqlsrv_query( $conn, $sql ); + $sql = "CREATE TABLE $tableName2 (c1 INT, c2 VARCHAR(40))"; + $stmt = sqlsrv_query($conn, $sql); // Insert data - $sql = "INSERT INTO $tableName2 VALUES ( ?, ? )"; - for( $t = 200; $t < 209; $t++ ) - { - $ts = substr( sha1( $t ),0,5 ); - $params = array( $t,$ts ); - $stmt = sqlsrv_prepare( $conn, $sql, $params ); - sqlsrv_execute( $stmt ); + $sql = "INSERT INTO $tableName2 VALUES (?, ?)"; + for ($t = 200; $t < 209; $t++) { + $ts = substr(sha1($t), 0, 5); + $params = array($t, $ts); + $stmt = sqlsrv_prepare($conn, $sql, $params); + sqlsrv_execute($stmt); } - sqlsrv_close( $conn ); + sqlsrv_close($conn); } // Break connection by getting the session ID and killing it. // Note that breaking a connection and testing reconnection requires a // TCP/IP protocol connection (as opposed to a Shared Memory protocol). -function BreakConnection( $conn, $conn_break ) +// Wait one second before and after breaking to ensure the break occurs +// in the correct order, otherwise there may be timing issues in Linux +// that can cause tests to fail intermittently and unpredictably. +function BreakConnection($conn, $conn_break) { - $stmt1 = sqlsrv_query( $conn, "SELECT @@SPID" ); - if ( sqlsrv_fetch( $stmt1 ) ) - { - $spid=sqlsrv_get_field( $stmt1, 0 ); + sleep(1); + $stmt1 = sqlsrv_query($conn, "SELECT @@SPID"); + if (sqlsrv_fetch($stmt1)) { + $spid=sqlsrv_get_field($stmt1, 0); } - $stmt2 = sqlsrv_prepare( $conn_break, "KILL ".$spid ); - sqlsrv_execute( $stmt2 ); + $stmt2 = sqlsrv_prepare($conn_break, "KILL ".$spid); + sqlsrv_execute($stmt2); sleep(1); } // Remove the tables generated by GenerateTables -function DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ) +function DropTables($server, $uid, $pwd, $tableName1, $tableName2) { global $dbName; - $connectionInfo = array( "Database"=>$dbName, "UID"=>$uid, "PWD"=>$pwd ); - $conn = sqlsrv_connect( $server, $connectionInfo ); + $connectionInfo = array("Database"=>$dbName, "UID"=>$uid, "PWD"=>$pwd); + $conn = sqlsrv_connect($server, $connectionInfo); - $query="IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; - $stmt=sqlsrv_query( $conn, $query ); + $query = "IF OBJECT_ID('$tableName1', 'U') IS NOT NULL DROP TABLE $tableName1"; + $stmt = sqlsrv_query($conn, $query); - $query="IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; - $stmt=sqlsrv_query( $conn, $query ); + $query = "IF OBJECT_ID('$tableName2', 'U') IS NOT NULL DROP TABLE $tableName2"; + $stmt = sqlsrv_query($conn, $query); } -DropTables( $server, $uid, $pwd, $tableName1, $tableName2 ); -GenerateTables( $server, $uid, $pwd, $dbName, $tableName1, $tableName2 ); +DropTables($server, $uid, $pwd, $tableName1, $tableName2); +GenerateTables($server, $uid, $pwd, $dbName, $tableName1, $tableName2); ?> diff --git a/test/functional/sqlsrv/skipif_not_hgs.inc b/test/functional/sqlsrv/skipif_not_hgs.inc index 7d7b3ca1..a25d8cbc 100644 --- a/test/functional/sqlsrv/skipif_not_hgs.inc +++ b/test/functional/sqlsrv/skipif_not_hgs.inc @@ -9,11 +9,11 @@ if (!extension_loaded("sqlsrv")) { require_once("MsSetup.inc"); -$connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword, "Driver" => $driver); +$connectionInfo = array("UID"=>$userName, "PWD"=>$userPassword); -$conn = sqlsrv_connect( $server, $connectionInfo ); +$conn = sqlsrv_connect($server, $connectionInfo); if ($conn === false) { - die( "skip Could not connect during SKIPIF." ); + die("skip Could not connect during SKIPIF."); } $msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt index cb7aaf66..6fd5c3be 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_sqltype_numeric.phpt @@ -52,7 +52,7 @@ foreach ($dataTypes as $dataType) { } } // 22018 is the SQLSTATE for any incompatible conversion errors - if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == 22018) { + if ($isCompatible && sqlsrv_errors()[0]['SQLSTATE'] == '22018') { echo "$sqlType should be compatible with $dataType\n"; $success = false; } diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt index 67c601b8..020dce75 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_datetime.phpt @@ -1,111 +1,154 @@ --TEST-- Test for inserting and retrieving encrypted data of datetime types --DESCRIPTION-- -Bind output params using sqlsrv_prepare with all sql_type +Bind output/inout params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetime2" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "smalldatetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "time" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), - "datetimeoffset" => array("SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIMEOFFSET") ); - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - + array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetime2" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "smalldatetime" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DATE", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "time" => array( "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_TIME", "SQLSRV_SQLTYPE_DATETIMEOFFSET", "SQLSRV_SQLTYPE_DATETIME2"), + "datetimeoffset" => array("SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIMEOFFSET") ); + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; + + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array( &$c_detOut, $direction, null, $sqlTypeConstant), + array(&$c_randOut, $direction, null, $sqlTypeConstant )) + ); + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + // With data encrypted, errors are totally expected + if (empty($errors)) { + echo "Encrypted data: $dataType should NOT be compatible with $sqlType\n"; + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible) { + if (!empty($errors)) { + echo "$dataType should be compatible with $sqlType.\n"; + } else { + $success = true; + } + } else { + $implicitConv = 'Implicit conversion from data type '; + + // 22018 is the SQLSTATE for any incompatible conversion errors + if ($errors[0]['SQLSTATE'] === '22018') { + $success = true; + } elseif (strpos($errors[0]['message'], $implicitConv) !== false) { + $success = true; + } else { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +$conn = AE\connect(); + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + // insert a row + // Take the second and third entres (some edge cases) from the various + // $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'date', use $date_params[1] and $date_params[2] + // to form an array, namely ["0001-01-01", "9999-12-31"] $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - $c_detOut = ''; - $c_randOut = ''; - $stmt = sqlsrv_prepare( $conn, $outSql, - array(array( &$c_detOut, SQLSRV_PARAM_OUT, null, $sqlTypeConstant), - array(&$c_randOut, SQLSRV_PARAM_OUT, null, $sqlTypeConstant ))); - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - if (empty($errors) && AE\IsDataEncrypted()) { - // SQLSRV_PHPTYPE_DATETIME not supported - echo "$dataType should not be compatible with any datetime type.\n"; - $success = false; - } - } - } - } - } - - // cleanup - sqlsrv_free_stmt($stmt); - sqlsrv_query($conn, "TRUNCATE TABLE $tbname"); - - if ($success) { - echo "Test successfully done.\n"; - } - - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $r; + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_* constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + // cleanup + sqlsrv_free_stmt($stmt); + sqlsrv_query($conn, "TRUNCATE TABLE $tbname"); + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt index ea5285d8..1fd65641 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_numeric.phpt @@ -5,143 +5,181 @@ Bind output params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "tinyint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "smallint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "int" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "bigint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP" ), - "decimal(18,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "numeric(10,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), - "float" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT"), - "real" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT")); -$epsilon = 0.0001; - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - // TODO: It's a good idea to test conversions between different datatypes when AE is off as well. - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - +$directions = array(SQLSRV_PARAM_OUT, SQLSRV_PARAM_INOUT); + +// this is a list of implicit datatype conversion that SQL Server allows (https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-type-conversion-database-engine) +$compatList = array("bit" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "tinyint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "smallint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "int" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "bigint" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP" ), + "decimal(18,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "numeric(10,5)" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT", "SQLSRV_SQLTYPE_TIMESTAMP"), + "float" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT"), + "real" => array( "SQLSRV_SQLTYPE_BINARY", "SQLSRV_SQLTYPE_VARBINARY", "SQLSRV_SQLTYPE_CHAR", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DATETIME", "SQLSRV_SQLTYPE_SMALLDATETIME", "SQLSRV_SQLTYPE_DECIMAL(18,5)", "SQLSRV_SQLTYPE_NUMERIC(10,5)", "SQLSRV_SQLTYPE_FLOAT", "SQLSRV_SQLTYPE_REAL", "SQLSRV_SQLTYPE_BIGINT", "SQLSRV_SQLTYPE_INT", "SQLSRV_SQLTYPE_SMALLINT", "SQLSRV_SQLTYPE_TINYINT", "SQLSRV_SQLTYPE_MONEY", "SQLSRV_SQLTYPE_SMALLMONEY", "SQLSRV_SQLTYPE_BIT")); + +function compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues) +{ + $epsilon = 0.0001; + $success = true; + + if ($dataType == "float" || $dataType == "real") { + if (abs($c_detOut - $inputValues[0]) > $epsilon || abs($c_randOut - $inputValues[1]) > $epsilon) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + $success = false; + } + } else { + if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + $success = false; + } + } + + return $success; +} + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; + + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array( &$c_detOut, $direction, null, $sqlTypeConstant), + array(&$c_randOut, $direction, null, $sqlTypeConstant )) + ); + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + if (empty($errors)) { + // With data encrypted, it's a lot stricter, so the results are expected + // to be numeric and comparable + $success = compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues); + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible && empty($errors)) { + $success = true; + } else { + // Even if $dataType is compatible with $sqlType sometimes + // we still get errors from the server -- if so, it might + // return either SQLSTATE '42000' or '22018' (operand type + // clash but only happens with some certain types) + // E.g. when converting a bigint to int or an int to numeric, + // SQLSTATE '42000' is returned, indicating an error when + // converting from one type to another. + // TODO 11559: investigate if SQLSTATE '42000' is indeed acceptable + $success = ($errors[0]['SQLSTATE'] === '42000' || ($errors[0]['SQLSTATE'] === '22018' && in_array($sqlType, ['SQLSRV_SQLTYPE_XML', 'SQLSRV_SQLTYPE_BINARY', 'SQLSRV_SQLTYPE_VARBINARY', 'SQLSRV_SQLTYPE_UNIQUEIDENTIFIER', 'SQLSRV_SQLTYPE_TIMESTAMP']))); + if (!$success) { + if ($compatible) { + echo "$dataType should be compatible with $sqlType.\n"; + } else { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + } + + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +$conn = AE\connect(); + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + // insert a row + // Take the second and third entres (some edge cases) from the various + // $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'decimal(18,5)', use $decimal_params[1] and $decimal_params[2] + // to form an array, namely [-9223372036854.80000, 9223372036854.80000] $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - // convert input values to strings for decimals and numerics - if ($dataTypes == "decimal(18,5)" || $dataTypes == "numeric(10,5)") { - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => (string) $inputValues[0], $colMetaArr[1]->colName => (string) $inputValues[1] ), $r); - } else { - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - } - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - if ($sqlType == 'SQLSRV_SQLTYPE_FLOAT' || $sqlType == 'SQLSRV_SQLTYPE_REAL') { - $c_detOut = 0.0; - $c_randOut = 0.0; - } else { - $c_detOut = 0; - $c_randOut = 0; - } - $stmt = sqlsrv_prepare($conn, $outSql, - array(array( &$c_detOut, constant($direction), null, $sqlTypeConstant), - array(&$c_randOut, constant($direction), null, $sqlTypeConstant))); - - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - - if (!empty($errors)) { - if (stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - var_dump(sqlsrv_errors()); - $success = false; - } - } - else { - if (AE\IsDataEncrypted() || stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - if ($dataType == "float" || $dataType == "real") { - if (abs($c_detOut - $inputValues[0]) > $epsilon || abs($c_randOut - $inputValues[1]) > $epsilon) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } else { - if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } - } - } - - sqlsrv_free_stmt($stmt); - } - } - } - } - - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - - if ($success) { - echo "Test successfully done.\n"; - } - - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $r; + // convert input values to strings for decimals and numerics + if ($dataTypes == "decimal(18,5)" || $dataTypes == "numeric(10,5)") { + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => (string) $inputValues[0], $colMetaArr[1]->colName => (string) $inputValues[1] ), $r); + } else { + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + } + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_ constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt index 8bbe9606..65919143 100755 --- a/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_output_param_sqltype_string.phpt @@ -5,117 +5,155 @@ Bind output params using sqlsrv_prepare with all sql_type --SKIPIF-- --FILE-- - array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "varchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "nchar(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), - "nvarchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML")); - -$conn = AE\connect(); - -foreach ($dataTypes as $dataType) { - echo "\nTesting $dataType:\n"; - $success = true; - - // create table - $tbname = GetTempTableName("", false); - $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); - AE\createTable($conn, $tbname, $colMetaArr); - - // TODO: It's a good idea to test conversions between different datatypes when AE is off as well. - if (AE\isColEncrypted()) { - // Create a Store Procedure - $spname = 'selectAllColumns'; - createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); - } - - // insert a row - $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); - $r; - $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); - if ($r === false) { - is_incompatible_types_error($dataType, "default type"); - } - - foreach($directions as $direction) { - echo "Testing as $direction:\n"; - - // test each SQLSRV_SQLTYPE_ constants - foreach ($sqlTypes as $sqlType) { - if (!AE\isColEncrypted()) { - $isCompatible = false; - foreach ($compatList[$dataType] as $compatType) { - if (stripos($compatType, $sqlType) !== false) { - $isCompatible = true; - } - } - // 22018 is the SQLSTATE for any incompatible conversion errors - $errors = sqlsrv_errors(); - if (!empty($errors) && $isCompatible && $errors[0]['SQLSTATE'] == 22018) { - echo "$sqlType should be compatible with $dataType\n"; - $success = false; - } - } else { - // skip unsupported datetime types - if (!isDateTimeType($sqlType)) { - $sqlTypeConstant = get_sqlType_constant($sqlType); - - // Call store procedure - $outSql = AE\getCallProcSqlPlaceholders($spname, 2); - $c_detOut = ''; - $c_randOut = ''; - $stmt = sqlsrv_prepare($conn, $outSql, - array(array(&$c_detOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant), - array(&$c_randOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant))); - - if (!$stmt) { - die(print_r(sqlsrv_errors(), true)); - } - - sqlsrv_execute($stmt); - $errors = sqlsrv_errors(); - - if (!empty($errors) ) { - if (stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - var_dump(sqlsrv_errors()); - $success = false; - } - } - else - { - if (AE\IsDataEncrypted() || stripos("SQLSRV_SQLTYPE_" . $dataType, $sqlType) !== false) { - if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { - echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; - print(" c_det: " . $c_detOut . "\n"); - print(" c_rand: " . $c_randOut . "\n"); - $success = false; - } - } - } - - sqlsrv_free_stmt($stmt); - } - } - } - } +$directions = array(SQLSRV_PARAM_OUT, SQLSRV_PARAM_INOUT); + +// this is a list of implicit datatype conversion that SQL Server allows (https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-type-conversion-database-engine) +$compatList = array("char(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "varchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "nchar(5)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML"), + "nvarchar(max)" => array( "SQLSRV_SQLTYPE_CHAR(5)", "SQLSRV_SQLTYPE_VARCHAR", "SQLSRV_SQLTYPE_NCHAR(5)", "SQLSRV_SQLTYPE_NVARCHAR", "SQLSRV_SQLTYPE_DECIMAL", "SQLSRV_SQLTYPE_NUMERIC", "SQLSRV_SQLTYPE_NTEXT", "SQLSRV_SQLTYPE_TEXT", "SQLSRV_SQLTYPE_XML")); + +$conn = AE\connect(); + +function compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues) +{ + $success = true; + if ($c_detOut != $inputValues[0] || $c_randOut != $inputValues[1]) { + echo "Incorrect output retrieved for datatype $dataType and sqlType $sqlType:\n"; + print(" c_det: " . $c_detOut . "\n"); + print(" c_rand: " . $c_randOut . "\n"); + + $success = false; + } + + return $success; +} + +function testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues) +{ + // The driver does not support these types as output params, simply return + if (isDateTimeType($sqlType) || isLOBType($sqlType)) { + return true; + } + + global $compatList; - if (AE\isColEncrypted()) { - dropProc($conn, $spname); - } - if ($success) { - echo "Test successfully done.\n"; - } - dropTable($conn, $tbname); -} - -sqlsrv_close($conn); + $sqlTypeConstant = get_sqlType_constant($sqlType); + + // Call store procedure + $outSql = AE\getCallProcSqlPlaceholders($spname, 2); + + // Set these to NULL such that the PHP type of each output parameter is inferred + // from the SQLSRV_SQLTYPE_* constant + $c_detOut = null; + $c_randOut = null; + + $stmt = sqlsrv_prepare( + $conn, + $outSql, + array(array(&$c_detOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant), + array(&$c_randOut, SQLSRV_PARAM_INOUT, null, $sqlTypeConstant)) + ); + + if (!$stmt) { + die(print_r(sqlsrv_errors(), true)); + } + sqlsrv_execute($stmt); + + $success = false; + $errors = sqlsrv_errors(); + if (AE\IsDataEncrypted()) { + if (empty($errors)) { + // With data encrypted, it's a lot stricter, so the results are expected + // to be comparable + $success = compareResults($dataType, $sqlType, $c_detOut, $c_randOut, $inputValues); + } else { + // This should return 22018, the SQLSTATE for any incompatible conversion, + // except the XML type + $success = ($errors[0]['SQLSTATE'] === '22018'); + if (!$success) { + if ($sqlType === 'SQLSRV_SQLTYPE_XML') { + $success = ($errors[0]['SQLSTATE'] === '42000'); + } else { + echo "Encrypted data: unexpected errors with SQL type: $sqlType\n"; + } + } + } + } else { + $compatible = isCompatible($compatList, $dataType, $sqlType); + if ($compatible && empty($errors)) { + $success = true; + } else { + // Even if $dataType is compatible with $sqlType sometimes + // we still get errors from the server -- if so, it should + // return SQLSTATE '42000', indicating an error when + // converting from one type to another + // With data NOT encrypted, converting string types to other + // types will not return '22018' + $success = ($errors[0]['SQLSTATE'] === '42000'); + if (!$success) { + echo "Failed with SQL type: $sqlType\n"; + } + } + } + + return $success; +} + +//////////////////////////////////////////////////////////////////////////////////////// + +foreach ($dataTypes as $dataType) { + echo "\nTesting $dataType:\n"; + $success = true; + + // create table + $tbname = GetTempTableName("", false); + $colMetaArr = array(new AE\ColumnMeta($dataType, "c_det"), new AE\ColumnMeta($dataType, "c_rand", null, false)); + AE\createTable($conn, $tbname, $colMetaArr); + + // Create a Store Procedure + $spname = 'selectAllColumns'; + createProc($conn, $spname, "@c_det $dataType OUTPUT, @c_rand $dataType OUTPUT", "SELECT @c_det = c_det, @c_rand = c_rand FROM $tbname"); + + // insert a row + // Take the second and third entres from the various $[$dataType]_params in AEData.inc + // e.g. with $dataType = 'varchar(max)', use $varchar_params[1] and $varchar_params[2] + // to form an array + $inputValues = array_slice(${explode("(", $dataType)[0] . "_params"}, 1, 2); + $r; + $stmt = AE\insertRow($conn, $tbname, array( $colMetaArr[0]->colName => $inputValues[0], $colMetaArr[1]->colName => $inputValues[1] ), $r); + if ($r === false) { + fatalError("Failed to insert data of type $dataType\n"); + } + + foreach ($directions as $direction) { + $dir = ($direction == SQLSRV_PARAM_OUT) ? 'SQLSRV_PARAM_OUT' : 'SQLSRV_PARAM_INOUT'; + echo "Testing as $dir:\n"; + + // test each SQLSRV_SQLTYPE_ constants + foreach ($sqlTypes as $sqlType) { + $success = testOutputParam($conn, $spname, $direction, $dataType, $sqlType, $inputValues); + if (!$success) { + // No point to continue looping + echo("Test failed: $dataType as $sqlType\n"); + die(print_r(sqlsrv_errors(), true)); + } + } + } + + dropProc($conn, $spname); + if ($success) { + echo "Test successfully done.\n"; + } + dropTable($conn, $tbname); +} + +sqlsrv_close($conn); ?> --EXPECT-- diff --git a/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt b/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt new file mode 100644 index 00000000..425d7f58 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_buffered_fetch_types.phpt @@ -0,0 +1,278 @@ +--TEST-- +Prepare with cursor buffered and fetch a variety of types converted to different types +--DESCRIPTION-- +Test various conversion functionalites for buffered queries with SQLSRV. +--SKIPIF-- + +--FILE-- +SQLSRV_CURSOR_CLIENT_BUFFERED)); + if (!$stmt) { + fatalError("In fetchAsUTF8: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsUTF8: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as UTF-8 strings + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_STRING('utf-8')); + if ($i == 0) { + if ($inputs[$i] !== hex2bin($f)) { + var_dump($f); + } + } else { + if ($f !== $inputs[$i]) { + var_dump($f); + } + } + } +} + +function fetchArray($conn, $tableName, $inputs) +{ + $query = "SELECT * FROM $tableName"; + + $stmt = sqlsrv_prepare($conn, $query, array(), array('Scrollable'=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchArray: failed to prepare query!"); + } + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("In fetchArray: failed to execute query!"); + } + + // Fetch fields as an array + $results = sqlsrv_fetch_array($stmt); + if ($results === false) { + fatalError("In fetchArray: failed to fetch the row from $tableName!"); + } + + for ($i = 0; $i < count($inputs); $i++) { + if ($i == 1) { + $expected = intval($inputs[$i]); + } elseif ($i == 2) { + $expected = floatval($inputs[$i]); + } else { + $expected = $inputs[$i]; + } + + if ($results[$i] !== $expected) { + echo "in fetchArray: for column $i expected $expected but got: "; + var_dump($results[$i]); + } + } +} + +function fetchAsFloats($conn, $tableName, $inputs) +{ + global $violation, $outOfRange, $epsilon; + + $query = "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $query, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchAsFloats: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsFloats: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as floats + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_FLOAT); + + if ($i == 0) { + // The varbinary field - expect the violation error + if (strpos(sqlsrv_errors()[0]['message'], $violation) === false) { + var_dump($f); + fatalError("in fetchAsFloats: expected $violation for column $i\n"); + } + } elseif ($i < 5) { + $expected = floatval($inputs[$i]); + $diff = abs(($f - $expected) / $expected); + + if ($diff > $epsilon) { + echo "in fetchAsFloats: for column $i expected $expected but got: "; + var_dump($f); + } + } else { + // The char fields will get errors too + // TODO 11297: fix this part outside Windows later + if (isWindows()) { + if (strpos(sqlsrv_errors()[0]['message'], $outOfRange) === false) { + var_dump($f); + fatalError("in fetchAsFloats: expected $outOfRange for column $i\n"); + } + } else { + if ($f != 0.0) { + var_dump($f); + } + } + } + } +} + +function fetchAsInts($conn, $tableName, $inputs) +{ + global $violation, $outOfRange, $truncation; + + $query = "SELECT * FROM $tableName"; + $stmt = sqlsrv_query($conn, $query, array(), array("Scrollable"=>SQLSRV_CURSOR_CLIENT_BUFFERED, 'ReturnDatesAsStrings' => true)); + if (!$stmt) { + fatalError("In fetchAsInts: failed to run query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsInts: failed to fetch the row from $tableName!"); + } + + // Fetch all fields as integers + for ($i = 0; $i < count($inputs); $i++) { + $f = sqlsrv_get_field($stmt, $i, SQLSRV_PHPTYPE_INT); + + if ($i == 0) { + // The varbinary field - expect the violation error + if (strpos(sqlsrv_errors()[0]['message'], $violation) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $violation for column $i\n"); + } + } elseif ($i == 2) { + // The float field - expect truncation + if (strpos(sqlsrv_errors()[0]['message'], $truncation) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $truncation for column $i\n"); + } + } elseif ($i >= 5) { + // The char fields will get errors too + // TODO 11297: fix this part outside Windows later + if (isWindows()) { + if (strpos(sqlsrv_errors()[0]['message'], $outOfRange) === false) { + var_dump($f); + fatalError("in fetchAsInts: expected $outOfRange for column $i\n"); + } + } else { + if ($f != 0) { + var_dump($f); + } + } + } else { + $expected = floor($inputs[$i]); + if ($f != $expected) { + echo "in fetchAsInts: for column $i expected $expected but got: "; + var_dump($f); + } + } + } +} + +function fetchAsBinary($conn, $tableName, $inputs) +{ + $query = "SELECT c_varbinary FROM $tableName"; + + $stmt = sqlsrv_prepare($conn, $query, array(), array('Scrollable'=>SQLSRV_CURSOR_CLIENT_BUFFERED)); + if (!$stmt) { + fatalError("In fetchAsBinary: failed to prepare query!"); + } + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("In fetchAsBinary: failed to execute query!"); + } + + if (sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC) === false) { + fatalError("In fetchAsInts: failed to fetch the row from $tableName!"); + } + + // Fetch the varbinary field as is + $f = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM("binary")); + if (gettype($f) !== 'resource') { + var_dump($f); + } + // Do not expect errors + $errs = sqlsrv_errors(); + if (!empty($errs)) { + var_dump($errs); + } + + // Check its value + while (!feof($f)) { + $str = fread($f, 80); + } + if (trim($str) !== $inputs[0]) { + echo "Fetched binary value unexpected: $str\n"; + } +} + +require_once('MsCommon.inc'); + +$conn = AE\connect(array('CharacterSet' => 'UTF-8')); +$tableName = 'srvFetchingClientBuffer'; + +// Create table +$names = array('c_varbinary', 'c_int', 'c_float', 'c_decimal', 'c_datetime2', 'c_varchar', 'c_nvarchar'); + +$columns = array(new AE\ColumnMeta('varbinary(10)', $names[0]), + new AE\ColumnMeta('int', $names[1]), + new AE\ColumnMeta('float(53)', $names[2]), + new AE\ColumnMeta('decimal(16, 6)', $names[3]), + new AE\ColumnMeta('datetime2', $names[4]), + new AE\ColumnMeta('varchar(50)', $names[5]), + new AE\ColumnMeta('nvarchar(50)', $names[6])); +$stmt = AE\createTable($conn, $tableName, $columns); +if (!$stmt) { + fatalError("Failed to create $tableName!"); +} + +// Prepare the input values +$inputs = array('abcdefghij', '34567', '9876.5432', '123456789.012340', '2020-02-02 20:20:20.2220000', 'This is a test', 'Şơмė śäოрŀề'); + +$params = array(array(bin2hex($inputs[0]), SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_BINARY(10)), + $inputs[1], $inputs[2], $inputs[3], $inputs[4], $inputs[5], + array($inputs[6], SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STRING('utf-8'))); + +// Form the insert query +$colStr = '('; +foreach ($names as $name) { + $colStr .= $name . ", "; +} +$colStr = rtrim($colStr, ", ") . ") "; +$insertSql = "INSERT INTO [$tableName] " . $colStr . 'VALUES (?,?,?,?,?,?,?)'; + +// Insert one row only +$stmt = sqlsrv_prepare($conn, $insertSql, $params); +if ($stmt) { + $res = sqlsrv_execute($stmt); + if (!$res) { + fatalError("Failed to execute insert statement to $tableName!"); + } +} else { + fatalError("Failed to prepare insert statement to $tableName!"); +} + +// Starting fetching using client buffers +fetchAsUTF8($conn, $tableName, $inputs); +fetchArray($conn, $tableName, $inputs); +fetchAsFloats($conn, $tableName, $inputs); +fetchAsInts($conn, $tableName, $inputs); +fetchAsBinary($conn, $tableName, $inputs); + +dropTable($conn, $tableName); + +echo "Done\n"; + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +?> +--EXPECT-- +Done diff --git a/test/functional/sqlsrv/sqlsrv_commit_logs.phpt b/test/functional/sqlsrv/sqlsrv_commit_logs.phpt new file mode 100644 index 00000000..ba17483e --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_commit_logs.phpt @@ -0,0 +1,63 @@ +--TEST-- +Test sqlsrv_commit method with logging +--DESCRIPTION-- +Similar to sqlsrv_commit.phpt but also test some basic logging activities +By adding integer values together, we can specify more than one logging option at a time. +SQLSRV_LOG_SYSTEM_CONN (2) Turns on logging of connection activity. +SQLSRV_LOG_SYSTEM_STMT (4) Turns on logging of statement activity. + +For example, sqlsrv.LogSubsystems = 6 +turns on logging of connection and statement activities +--SKIPIF-- + +--FILE-- + +--EXPECT-- +sqlsrv_connect: entering +sqlsrv_query: entering +sqlsrv_query: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_query: entering +sqlsrv_query: entering +sqlsrv_commit: entering +sqlsrv_query: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_free_stmt: entering +sqlsrv_stmt_dtor: entering +sqlsrv_close: entering diff --git a/test/functional/sqlsrv/sqlsrv_connect.phpt b/test/functional/sqlsrv/sqlsrv_connect.phpt index 083e74fa..2dcacb7d 100644 --- a/test/functional/sqlsrv/sqlsrv_connect.phpt +++ b/test/functional/sqlsrv/sqlsrv_connect.phpt @@ -14,7 +14,7 @@ functions return FALSE for errors. fatalError("sqlsrv_connect should have returned false."); } - $conn = sqlsrv_connect("_!@#$", array( "Driver" => "Danica Patrick" )); + $conn = sqlsrv_connect("_!@#$", array( "Driver" => "Wrong Driver" )); if ($conn !== false) { fatalError("sqlsrv_connect should have returned false."); } diff --git a/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt b/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt index e6a5bbb6..f65b5810 100644 --- a/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt +++ b/test/functional/sqlsrv/sqlsrv_connect_encrypted.phpt @@ -1,98 +1,101 @@ --TEST-- -Test new connection keyword ColumnEncryption +Test new connection keyword ColumnEncryption with different input values +--DESCRIPTION-- +Some test cases return errors as expected. For testing purposes, an enclave enabled +SQL Server and the HGS server are the same instance. If the server is HGS enabled, +the error message of one test case is not the same. --SKIPIF-- --FILE-- $database,"UID"=>$userName, "PWD"=>$userPassword); -test_ColumnEncryption($server, $connectionOptions); +testColumnEncryption($server, $connectionOptions); echo "Done"; -function test_ColumnEncryption($server ,$connectionOptions){ +function testColumnEncryption($server, $connectionOptions) +{ $conn = sqlsrv_connect($server, $connectionOptions); - if ($conn === false) - { + if ($conn === false) { print_r(sqlsrv_errors()); } $msodbcsql_ver = sqlsrv_client_info($conn)['DriverVer']; - $msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; + $msodbcsqlMaj = explode(".", $msodbcsql_ver)[0]; + // Next, check if the server is HGS enabled + $hgsEnabled = true; + $serverInfo = sqlsrv_server_info($conn); + if (strpos($serverInfo['SQLServerName'], 'PHPHGS') === false) { + $hgsEnabled = false; + } + // Only works for ODBC 17 - $connectionOptions['ColumnEncryption']='Enabled'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - if($msodbcsql_maj < 17){ + $connectionOptions['ColumnEncryption'] = 'Enabled'; + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if ($msodbcsqlMaj < 17) { $expected = "The Always Encrypted feature requires Microsoft ODBC Driver 17 for SQL Server."; - if( strcasecmp(sqlsrv_errors($conn)[0]['message'], $expected ) != 0 ) - { + if (strcasecmp(sqlsrv_errors($conn)[0]['message'], $expected) != 0) { print_r(sqlsrv_errors()); } - } - else - { + } else { + echo "Test case 1 failed:\n"; print_r(sqlsrv_errors()); } } - + // Works for ODBC 17, ODBC 13 $connectionOptions['ColumnEncryption']='Disabled'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - if($msodbcsql_maj < 13) - { - $expected_substr = "Invalid connection string attribute"; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if ($msodbcsqlMaj < 13) { + $expected = "Invalid connection string attribute"; + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { print_r(sqlsrv_errors()); } - } - else - { + } else { + echo "Test case 2 failed:\n"; print_r(sqlsrv_errors()); } - } - else - { + } else { sqlsrv_close($conn); } + + // Should fail for all ODBC drivers - but the error message returned depends on the server + $expected = "Invalid value specified for connection string attribute 'ColumnEncryption'"; + if ($hgsEnabled) { + $expected = "Requested attestation protocol is invalid."; + } - // should fail for all ODBC drivers $connectionOptions['ColumnEncryption']='false'; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value specified for connection string attribute 'ColumnEncryption'"; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 3 failed:\n"; print_r(sqlsrv_errors()); } } - // should fail for all ODBC drivers + $expected = "Invalid value type for option ColumnEncryption was specified. String type was expected."; + + // should fail for all ODBC drivers with the above error message $connectionOptions['ColumnEncryption']=true; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value type for option ColumnEncryption was specified. String type was expected."; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 4 failed:\n"; print_r(sqlsrv_errors()); } } - // should fail for all ODBC drivers + // should fail for all ODBC drivers with the above error message $connectionOptions['ColumnEncryption']=false; - $conn = sqlsrv_connect( $server, $connectionOptions ); - if( $conn === false ) - { - $expected_substr = "Invalid value type for option ColumnEncryption was specified. String type was expected."; - if( strpos(sqlsrv_errors($conn)[0]['message'], $expected_substr ) === false ) - { + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + if (strpos(sqlsrv_errors($conn)[0]['message'], $expected) === false) { + echo "Test case 5 failed:\n"; print_r(sqlsrv_errors()); } } diff --git a/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt b/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt new file mode 100644 index 00000000..4422294f --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_connect_log_to_file.phpt @@ -0,0 +1,56 @@ +--TEST-- +Test functions return FALSE for errors with logging +--DESCRIPTION-- +Similar to sqlsrv_connect_logs.phpt but this time test logging to a log file +--SKIPIF-- + +--FILE-- + "Wrong Driver" )); + if ($conn !== false) { + fatalError("sqlsrv_connect should have returned false."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_NOTICE); + $conn = sqlsrv_connect($server, array( "uid" => $uid , "pwd" => $pwd )); + + if ($conn === false) { + fatalError("sqlsrv_connect should have connected."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); + $stmt = sqlsrv_query($conn, "SELECT * FROM some_bogus_table"); + if ($stmt !== false) { + fatalError("sqlsrv_query should have returned false."); + } + + ini_set("sqlsrv.LogSeverity", SQLSRV_LOG_SEVERITY_ALL); + if (file_exists($logFilepath)) { + echo file_get_contents($logFilepath); + unlink($logFilepath); + } + + sqlsrv_close($conn); +?> +--EXPECTF-- +[%s UTC] sqlsrv_connect: entering +[%s UTC] sqlsrv_connect: SQLSTATE = IMSSP +[%s UTC] sqlsrv_connect: error code = -106 +[%s UTC] sqlsrv_connect: message = Invalid value Wrong Driver was specified for Driver option. +[%s UTC] sqlsrv_connect: entering +[%s UTC] sqlsrv_query: SQLSTATE = 42S02 +[%s UTC] sqlsrv_query: error code = 208 +[%s UTC] sqlsrv_query: message = %s[SQL Server]Invalid object name 'some_bogus_table'. \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_connect_logs.phpt b/test/functional/sqlsrv/sqlsrv_connect_logs.phpt new file mode 100644 index 00000000..1ba654bd --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_connect_logs.phpt @@ -0,0 +1,49 @@ +--TEST-- +Test functions return FALSE for errors with logging +--DESCRIPTION-- +Similar to sqlsrv_connect.phpt but also test different settings of logging activities +--SKIPIF-- + +--FILE-- + "Wrong Driver" )); + if ($conn !== false) { + fatalError("sqlsrv_connect should have returned false."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_NOTICE); + $conn = sqlsrv_connect($server, array( "uid" => $uid , "pwd" => $pwd )); + + if ($conn === false) { + fatalError("sqlsrv_connect should have connected."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ERROR); + $stmt = sqlsrv_query($conn, "SELECT * FROM some_bogus_table"); + if ($stmt !== false) { + fatalError("sqlsrv_query should have returned false."); + } + + sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_WARNING); + + sqlsrv_close($conn); +?> +--EXPECTF-- +sqlsrv.LogSubsystems = -1 +sqlsrv_connect: entering +sqlsrv_connect: SQLSTATE = IMSSP +sqlsrv_connect: error code = -106 +sqlsrv_connect: message = Invalid value Wrong Driver was specified for Driver option. +sqlsrv_configure: entering +sqlsrv.LogSeverity = 4 +sqlsrv_connect: entering +sqlsrv_configure: entering +sqlsrv_query: SQLSTATE = 42S02 +sqlsrv_query: error code = 208 +sqlsrv_query: message = %s[SQL Server]Invalid object name 'some_bogus_table'. diff --git a/test/functional/sqlsrv/sqlsrv_get_field.phpt b/test/functional/sqlsrv/sqlsrv_get_field.phpt index 9247d0ba..9d63a1f5 100644 --- a/test/functional/sqlsrv/sqlsrv_get_field.phpt +++ b/test/functional/sqlsrv/sqlsrv_get_field.phpt @@ -268,7 +268,7 @@ NULL NULL 1 NULL -1\.0E\+37 +(1\.0E\+37|9.9999999999997E\+36) NULL 12\/12\/1968 04\:20\:00 NULL @@ -306,7 +306,7 @@ NULL NULL 0 NULL -\-1\.0E\+37 +(\-1\.0E\+37|-9.9999999999997E\+36) NULL 12\/12\/1968 04\:20\:00 NULL diff --git a/test/functional/sqlsrv/srv_007_login_timeout.phpt b/test/functional/sqlsrv/srv_007_login_timeout.phpt index eda8cf3a..8145c370 100644 --- a/test/functional/sqlsrv/srv_007_login_timeout.phpt +++ b/test/functional/sqlsrv/srv_007_login_timeout.phpt @@ -7,21 +7,24 @@ Intentionally provide an invalid server name and set LoginTimeout. Verify the ti --FILE-- $timeout)); $numAttempts++; @@ -38,7 +41,7 @@ do { echo "Connection failed at $elapsed secs. Leeway is $leeway sec but the difference is $diff\n"; } else { // The test will fail but this helps us decide if this test should be redesigned - echo "$numAttempts\t"; + echo "Attempts: $numAttempts, Time difference: $diff\n"; sleep(5); } } diff --git a/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt b/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt index 99a915da..1725d3b3 100644 --- a/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt +++ b/test/functional/sqlsrv/srv_230_sqlsrv_buffered_numeric_types.phpt @@ -67,6 +67,10 @@ var_dump($array); $array = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); var_dump($array); +// The size of a float is platform dependent, with a precision of roughly 14 digits +// http://php.net/manual/en/language.types.float.php +$epsilon = 0.00001; + $numFields = sqlsrv_num_fields($stmt); $meta = sqlsrv_field_metadata($stmt); $rowcount = sqlsrv_num_rows($stmt); @@ -81,8 +85,21 @@ for ($i = 0; $i < $rowcount; $i++) { $field = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_INT); var_dump($field); } - $field = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_FLOAT); - var_dump($field); + $field1 = sqlsrv_get_field($stmt, $j, SQLSRV_PHPTYPE_FLOAT); + if ($j > 5) { + // these are the zero fields + $expected = 0.0; + if ($field1 !== $expected) { + echo "Expected $expected but got $field1\n"; + } + } else { + $expected = floatval($field); + $diff = abs(($field1 - $expected) / $expected); + + if ($diff > $epsilon) { + echo "Expected $expected but got $field1 -- difference is $diff\n"; + } + } } } @@ -136,78 +153,60 @@ array(9) { column: a string(15) "1234567890.1234" -float(1234567890.1234) column: neg_a string(16) "-1234567890.1234" -float(-1234567890.1234) column: b string(1) "1" int(1) -float(1) column: neg_b string(2) "-1" int(-1) -float(-1) column: c string(7) ".500000" -float(0.5) column: neg_c string(8) "-.550000" -float(-0.55) column: zero string(1) "0" int(0) -float(0) column: zerof string(1) "0" -float(0) column: zerod string(7) ".000000" -float(0) column: a string(3) "0.5" -float(0.5) column: neg_a string(5) "-0.55" -float(-0.55) column: b string(6) "100000" int(100000) -float(100000) column: neg_b string(8) "-1234567" int(-1234567) -float(-1234567) column: c string(17) "1234567890.123400" -float(1234567890.1234) column: neg_c string(18) "-1234567890.123400" -float(-1234567890.1234) column: zero string(1) "0" int(0) -float(0) column: zerof string(1) "0" -float(0) column: zerod -string(7) ".000000" -float(0) +string(7) ".000000" \ No newline at end of file