diff --git a/.travis.yml b/.travis.yml index 0618bebfd..e368b84e5 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 382717716..c18663be0 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 2a96c89c5..fb3175157 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 842a99b17..c89e8a249 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 ae779335e..ef9870ef5 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 3f048788b..23b9d72c5 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 5bd527901..6f2bd8611 100644 Binary files a/media/os_development.PNG and b/media/os_development.PNG differ diff --git a/media/os_production.PNG b/media/os_production.PNG index d15fbbdb2..4ee4e9a99 100644 Binary files a/media/os_production.PNG and b/media/os_production.PNG differ diff --git a/media/php_versions.PNG b/media/php_versions.PNG index 98416fe81..b241f3a3f 100644 Binary files a/media/php_versions.PNG and b/media/php_versions.PNG differ diff --git a/media/sql_server.PNG b/media/sql_server.PNG index 938f30b77..25224fe2e 100644 Binary files a/media/sql_server.PNG and b/media/sql_server.PNG differ diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 2a0ea4791..eedd07b11 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 71c426909..448eee04a 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 6cfb43acf..cff7add5c 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 6a616da05..2b7269c0c 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 cea5a3ddc..10545e42c 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 a9e281c47..be6ead07b 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 13bb2e5e1..090322a5f 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 6bc7e7969..c875410da 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 3ebb179fe..42148b03d 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 bbb011909..b67af51a7 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 000000000..721295b40 --- /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 823f283cc..1895f5aad 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 000000000..02d37c204 --- /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 000000000..5b39f9f15 --- /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 000000000..9ee0dcceb --- /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 000000000..703ae7612 --- /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 000000000..d41175a24 --- /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 000000000..393554d95 --- /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 cc49693bd..2e22a203a 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 000000000..2b603fe14 --- /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 000000000..59d545d38 --- /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 000000000..c2fa226e3 --- /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 000000000..c962ba3e6 --- /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 000000000..da6708f20 --- /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 000000000..dd4614de8 --- /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 000000000..19f97bc56 --- /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 000000000..4a9fc5bb8 Binary files /dev/null and b/test/functional/setup/AEV2Cert.pfx differ diff --git a/test/functional/setup/ae_keys.sql b/test/functional/setup/ae_keys.sql index 35c877209..d352a6f83 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 + +/* 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 \ No newline at end of file +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 58900526d..ead94a30d 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 000000000..721295b40 --- /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 aec2b4bb1..8335c13b7 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 3e4f56729..ae9365cae 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 a59f336cd..99e973455 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 000000000..f7ef4e89f --- /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 3734e0be0..e6f03d279 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 59e184478..c492a961b 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 000000000..186e93492 --- /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 000000000..b8daaacf3 --- /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 000000000..d236a2a4f --- /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 000000000..002f61ac7 --- /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 000000000..a42cabaf4 --- /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 000000000..a22fe3a1d --- /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 000000000..4aca5a590 --- /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 53a2c0fa6..d2e3850d7 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 909e3e691..eacf853a1 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--