summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/mysqli/mysqli.c7
-rw-r--r--ext/mysqli/mysqli_api.c5
-rw-r--r--ext/mysqli/mysqli_nonapi.c6
-rw-r--r--ext/mysqli/mysqli_prop.c2
-rw-r--r--ext/mysqli/php_mysqli_structs.h2
-rw-r--r--ext/mysqli/tests/bug77956.phpt5
-rw-r--r--ext/mysqli/tests/foo/bar/bar.data3
-rw-r--r--ext/mysqli/tests/foo/foo.data3
-rw-r--r--ext/mysqli/tests/local_infile_tools.inc12
-rw-r--r--ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt75
-rw-r--r--ext/mysqli/tests/mysqli_constants.phpt4
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_default_off.phpt4
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt80
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt65
-rw-r--r--ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt65
-rw-r--r--ext/mysqli/tests/mysqli_phpinfo.phpt2
-rw-r--r--ext/mysqlnd/mysqlnd_connection.c17
-rw-r--r--ext/mysqlnd/mysqlnd_enum_n_def.h2
-rw-r--r--ext/mysqlnd/mysqlnd_loaddata.c45
-rw-r--r--ext/mysqlnd/mysqlnd_structs.h2
-rw-r--r--ext/pdo_mysql/config.w325
-rw-r--r--ext/pdo_mysql/mysql_driver.c29
-rw-r--r--ext/pdo_mysql/pdo_mysql.c3
-rw-r--r--ext/pdo_mysql/php_pdo_mysql_int.h3
-rw-r--r--ext/pdo_mysql/tests/bug70389.phpt4
-rw-r--r--ext/pdo_mysql/tests/foo/bar/bar.data3
-rw-r--r--ext/pdo_mysql/tests/foo/foo.data3
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt5
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt25
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt5
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt85
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt76
-rw-r--r--ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt85
-rw-r--r--ext/pdo_mysql/tests/skipifinfilenotallowed.inc6
34 files changed, 717 insertions, 26 deletions
diff --git a/ext/mysqli/mysqli.c b/ext/mysqli/mysqli.c
index d4143e5564..bc29c74ce0 100644
--- a/ext/mysqli/mysqli.c
+++ b/ext/mysqli/mysqli.c
@@ -499,6 +499,7 @@ PHP_INI_BEGIN()
#endif
STD_PHP_INI_BOOLEAN("mysqli.reconnect", "0", PHP_INI_SYSTEM, OnUpdateLong, reconnect, zend_mysqli_globals, mysqli_globals)
STD_PHP_INI_BOOLEAN("mysqli.allow_local_infile", "0", PHP_INI_SYSTEM, OnUpdateLong, allow_local_infile, zend_mysqli_globals, mysqli_globals)
+ STD_PHP_INI_ENTRY("mysqli.local_infile_directory", NULL, PHP_INI_SYSTEM, OnUpdateString, local_infile_directory, zend_mysqli_globals, mysqli_globals)
PHP_INI_END()
/* }}} */
@@ -523,6 +524,7 @@ static PHP_GINIT_FUNCTION(mysqli)
mysqli_globals->report_mode = 0;
mysqli_globals->report_ht = 0;
mysqli_globals->allow_local_infile = 0;
+ mysqli_globals->local_infile_directory = NULL;
mysqli_globals->rollback_on_cached_plink = FALSE;
}
/* }}} */
@@ -600,6 +602,9 @@ PHP_MINIT_FUNCTION(mysqli)
REGISTER_LONG_CONSTANT("MYSQLI_READ_DEFAULT_FILE", MYSQL_READ_DEFAULT_FILE, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("MYSQLI_OPT_CONNECT_TIMEOUT", MYSQL_OPT_CONNECT_TIMEOUT, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOCAL_INFILE", MYSQL_OPT_LOCAL_INFILE, CONST_CS | CONST_PERSISTENT);
+#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
+ REGISTER_LONG_CONSTANT("MYSQLI_OPT_LOAD_DATA_LOCAL_DIR", MYSQL_OPT_LOAD_DATA_LOCAL_DIR, CONST_CS | CONST_PERSISTENT);
+#endif
REGISTER_LONG_CONSTANT("MYSQLI_INIT_COMMAND", MYSQL_INIT_COMMAND, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("MYSQLI_OPT_READ_TIMEOUT", MYSQL_OPT_READ_TIMEOUT, CONST_CS | CONST_PERSISTENT);
#ifdef MYSQLI_USE_MYSQLND
@@ -1021,7 +1026,7 @@ void php_mysqli_fetch_into_hash_aux(zval *return_value, MYSQL_RES * result, zend
MYSQL_ROW row;
unsigned int i, num_fields;
MYSQL_FIELD *fields;
- zend_ulong *field_len;
+ unsigned long *field_len;
if (!(row = mysql_fetch_row(result))) {
RETURN_NULL();
diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c
index b968f73567..e1abd07135 100644
--- a/ext/mysqli/mysqli_api.c
+++ b/ext/mysqli/mysqli_api.c
@@ -1210,7 +1210,7 @@ PHP_FUNCTION(mysqli_fetch_lengths)
#ifdef MYSQLI_USE_MYSQLND
const size_t *ret;
#else
- const zend_ulong *ret;
+ const unsigned long *ret;
#endif
if (zend_parse_method_parameters(ZEND_NUM_ARGS(), getThis(), "O", &mysql_result, mysqli_result_class_entry) == FAILURE) {
@@ -1674,6 +1674,9 @@ static int mysqli_options_get_option_zval_type(int option)
#if MYSQL_VERSION_ID > 50605 || defined(MYSQLI_USE_MYSQLND)
case MYSQL_SERVER_PUBLIC_KEY:
#endif
+#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
+ case MYSQL_OPT_LOAD_DATA_LOCAL_DIR:
+#endif
return IS_STRING;
default:
diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c
index 4870a4319a..907b1e3fbc 100644
--- a/ext/mysqli/mysqli_nonapi.c
+++ b/ext/mysqli/mysqli_nonapi.c
@@ -332,6 +332,12 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b
unsigned int allow_local_infile = MyG(allow_local_infile);
mysql_options(mysql->mysql, MYSQL_OPT_LOCAL_INFILE, (char *)&allow_local_infile);
+#if MYSQL_VERSION_ID >= 80021 || defined(MYSQLI_USE_MYSQLND)
+ if (MyG(local_infile_directory) && !php_check_open_basedir(MyG(local_infile_directory))) {
+ mysql_options(mysql->mysql, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, MyG(local_infile_directory));
+ }
+#endif
+
end:
if (!mysqli_resource) {
mysqli_resource = (MYSQLI_RESOURCE *)ecalloc (1, sizeof(MYSQLI_RESOURCE));
diff --git a/ext/mysqli/mysqli_prop.c b/ext/mysqli/mysqli_prop.c
index 9749fe5632..f1ed103001 100644
--- a/ext/mysqli/mysqli_prop.c
+++ b/ext/mysqli/mysqli_prop.c
@@ -267,7 +267,7 @@ static int result_lengths_read(mysqli_object *obj, zval *retval, bool quiet)
#ifdef MYSQLI_USE_MYSQLND
const size_t *ret;
#else
- const zend_ulong *ret;
+ const unsigned long *ret;
#endif
uint32_t field_count;
diff --git a/ext/mysqli/php_mysqli_structs.h b/ext/mysqli/php_mysqli_structs.h
index a39e68b276..b86b58c944 100644
--- a/ext/mysqli/php_mysqli_structs.h
+++ b/ext/mysqli/php_mysqli_structs.h
@@ -46,6 +46,7 @@ typedef _Bool my_bool;
#include <errmsg.h>
#include <mysqld_error.h>
#include "mysqli_libmysql.h"
+
#endif /* MYSQLI_USE_MYSQLND */
@@ -276,6 +277,7 @@ ZEND_BEGIN_MODULE_GLOBALS(mysqli)
char *default_pw;
zend_long reconnect;
zend_long allow_local_infile;
+ char *local_infile_directory;
zend_long strict;
zend_long error_no;
char *error_msg;
diff --git a/ext/mysqli/tests/bug77956.phpt b/ext/mysqli/tests/bug77956.phpt
index c76e1021e1..19df063951 100644
--- a/ext/mysqli/tests/bug77956.phpt
+++ b/ext/mysqli/tests/bug77956.phpt
@@ -55,6 +55,5 @@ $link->close();
unlink('bug77956.data');
?>
--EXPECTF--
-Warning: mysqli::query(): LOAD DATA LOCAL INFILE forbidden in %s on line %d
-[006] [2000] LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile
-done
+[006] [2000] LOAD DATA LOCAL INFILE is forbidden, check related settings like mysqli.allow_local_infile|mysqli.local_infile_directory or PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY
+done \ No newline at end of file
diff --git a/ext/mysqli/tests/foo/bar/bar.data b/ext/mysqli/tests/foo/bar/bar.data
new file mode 100644
index 0000000000..56e5e8cdce
--- /dev/null
+++ b/ext/mysqli/tests/foo/bar/bar.data
@@ -0,0 +1,3 @@
+97
+98
+99
diff --git a/ext/mysqli/tests/foo/foo.data b/ext/mysqli/tests/foo/foo.data
new file mode 100644
index 0000000000..01e79c32a8
--- /dev/null
+++ b/ext/mysqli/tests/foo/foo.data
@@ -0,0 +1,3 @@
+1
+2
+3
diff --git a/ext/mysqli/tests/local_infile_tools.inc b/ext/mysqli/tests/local_infile_tools.inc
index fef400d0a9..d45d15e6ac 100644
--- a/ext/mysqli/tests/local_infile_tools.inc
+++ b/ext/mysqli/tests/local_infile_tools.inc
@@ -6,8 +6,7 @@
}
}
- function check_local_infile_support($link, $engine, $table_name = 'test') {
-
+ function check_local_infile_allowed_by_server($link) {
if (!$res = mysqli_query($link, 'SHOW VARIABLES LIKE "local_infile"'))
return "Cannot check if Server variable 'local_infile' is set to 'ON'";
@@ -16,6 +15,15 @@
if ('ON' != $row['Value'])
return sprintf("Server variable 'local_infile' seems not set to 'ON', found '%s'", $row['Value']);
+ return "";
+ }
+
+ function check_local_infile_support($link, $engine, $table_name = 'test') {
+ $res = check_local_infile_allowed_by_server($link);
+ if ($res) {
+ return $res;
+ }
+
if (!mysqli_query($link, sprintf('DROP TABLE IF EXISTS %s', $table_name))) {
return "Failed to drop old test table";
}
diff --git a/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt b/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt
new file mode 100644
index 0000000000..187701ff8c
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_allow_local_infile_overrides_local_infile_directory.phpt
@@ -0,0 +1,75 @@
+--TEST--
+mysqli.allow_local_infile overrides mysqli.local_infile_directory
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=1
+mysqli.local_infile_directory={PWD}/foo/bar
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->query("DROP TABLE IF EXISTS test")) {
+ printf("[002] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
+ printf("[003] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+ if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
+ printf("[004] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) {
+ $row = mysqli_fetch_assoc($res);
+ mysqli_free_result($res);
+
+ $row_count = $row['num'];
+ $expected_row_count = 3;
+ if ($row_count != $expected_row_count) {
+ printf("[005] %d != %d\n", $row_count, $expected_row_count);
+ }
+ } else {
+ printf("[006] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $link->close();
+ echo "done";
+?>
+--CLEAN--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
+ printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
+}
+
+$link->close();
+?>
+--EXPECT--
+done
diff --git a/ext/mysqli/tests/mysqli_constants.phpt b/ext/mysqli/tests/mysqli_constants.phpt
index 6297d56b34..8538fafa4f 100644
--- a/ext/mysqli/tests/mysqli_constants.phpt
+++ b/ext/mysqli/tests/mysqli_constants.phpt
@@ -202,6 +202,10 @@ mysqli.allow_local_infile=1
$expected_constants["MYSQLI_TYPE_JSON"] = true;
}
+ if ($version > 80210 || $IS_MYSQLND) {
+ $expected_constants['MYSQLI_OPT_LOAD_DATA_LOCAL_DIR'] = true;
+ }
+
$unexpected_constants = array();
foreach ($constants as $group => $consts) {
diff --git a/ext/mysqli/tests/mysqli_local_infile_default_off.phpt b/ext/mysqli/tests/mysqli_local_infile_default_off.phpt
index c2e8aa2dc8..65f4012925 100644
--- a/ext/mysqli/tests/mysqli_local_infile_default_off.phpt
+++ b/ext/mysqli/tests/mysqli_local_infile_default_off.phpt
@@ -16,11 +16,11 @@ echo "server: ", $row['Value'], "\n";
mysqli_free_result($res);
mysqli_close($link);
-echo "connector: ", ini_get("mysqli.allow_local_infile"), "\n";
+echo 'connector: ', ini_get('mysqli.allow_local_infile'), ' ', var_export(ini_get('mysqli.local_infile_directory')), "\n";
print "done!\n";
?>
--EXPECTF--
server: %s
-connector: 0
+connector: 0 ''
done!
diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt
new file mode 100644
index 0000000000..a9c0c82e99
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_allowed.phpt
@@ -0,0 +1,80 @@
+--TEST--
+mysqli.local_infile_directory vs access allowed
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/foo
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->query("DROP TABLE IF EXISTS test")) {
+ printf("[002] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
+ printf("[003] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+ if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
+ printf("[004] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/bar/bar.data');
+ if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
+ printf("[005] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if ($res = mysqli_query($link, 'SELECT COUNT(id) AS num FROM test')) {
+ $row = mysqli_fetch_assoc($res);
+ mysqli_free_result($res);
+
+ $row_count = $row['num'];
+ $expected_row_count = 6;
+ if ($row_count != $expected_row_count) {
+ printf("[006] %d != %d\n", $row_count, $expected_row_count);
+ }
+ } else {
+ printf("[007] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $link->close();
+ echo "done";
+?>
+--CLEAN--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
+ printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
+}
+
+$link->close();
+?>
+--EXPECT--
+done
diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt
new file mode 100644
index 0000000000..8f50e03220
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_local_infile_directory_access_denied.phpt
@@ -0,0 +1,65 @@
+--TEST--
+mysqli.local_infile_directory access denied
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/foo/bar
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->query("DROP TABLE IF EXISTS test")) {
+ printf("[002] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
+ printf("[003] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+ if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
+ printf("[004] [%d] %s\n", $link->errno, $link->error);
+ } else {
+ printf("[005] bug! should not happen - access denied expected\n");
+ }
+
+ $link->close();
+ echo "done";
+?>
+--CLEAN--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
+ printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
+}
+
+$link->close();
+?>
+--EXPECTF--
+[004] [2068] LOAD DATA LOCAL INFILE %s
+done
diff --git a/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt
new file mode 100644
index 0000000000..a48606ad66
--- /dev/null
+++ b/ext/mysqli/tests/mysqli_local_infile_directory_vs_open_basedir.phpt
@@ -0,0 +1,65 @@
+--TEST--
+mysqli.local_infile_directory vs open_basedir
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket))
+ die("skip Cannot connect to MySQL");
+
+include_once("local_infile_tools.inc");
+if ($msg = check_local_infile_allowed_by_server($link))
+ die(sprintf("skip %s, [%d] %s", $msg, $link->errno, $link->error));
+
+mysqli_close($link);
+
+?>
+--INI--
+open_basedir={PWD}
+mysqli.allow_local_infile=0
+mysqli.local_infile_directory={PWD}/../
+--FILE--
+<?php
+ require_once("connect.inc");
+
+ if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[001] Connect failed, [%d] %s\n", mysqli_connect_errno(), mysqli_connect_error());
+ }
+
+ if (!$link->query("DROP TABLE IF EXISTS test")) {
+ printf("[002] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ if (!$link->query("CREATE TABLE test (id INT UNSIGNED NOT NULL PRIMARY KEY) ENGINE=" . $engine)) {
+ printf("[003] [%d] %s\n", $link->errno, $link->error);
+ }
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+ if (!$link->query("LOAD DATA LOCAL INFILE '".$filepath."' INTO TABLE test")) {
+ printf("[004] [%d] %s\n", $link->errno, $link->error);
+ } else {
+ printf("[005] bug! should not happen - operation not permitted expected\n");
+ }
+
+ echo "done";
+?>
+--CLEAN--
+<?php
+require_once('connect.inc');
+
+if (!$link = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket)) {
+ printf("[clean] Cannot connect to the server using host=%s, user=%s, passwd=***, dbname=%s, port=%s, socket=%s\n",
+ $host, $user, $db, $port, $socket);
+}
+
+if (!$link->query($link, 'DROP TABLE IF EXISTS test')) {
+ printf("[clean] Failed to drop old test table: [%d] %s\n", mysqli_errno($link), mysqli_error($link));
+}
+
+$link->close();
+?>
+--EXPECTF--
+Warning: mysqli_connect(): open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s) in %s on line %d
+[004] [2068] LOAD DATA LOCAL INFILE %s
+done
diff --git a/ext/mysqli/tests/mysqli_phpinfo.phpt b/ext/mysqli/tests/mysqli_phpinfo.phpt
index fd0edd4463..ea78a6bb0c 100644
--- a/ext/mysqli/tests/mysqli_phpinfo.phpt
+++ b/ext/mysqli/tests/mysqli_phpinfo.phpt
@@ -46,7 +46,7 @@ require_once('skipifconnectfailure.inc');
if ($IS_MYSQLND) {
$expected = array(
'size',
- 'mysqli.allow_local_infile',
+ 'mysqli.allow_local_infile', 'mysqli.local_infile_directory',
'mysqli.allow_persistent', 'mysqli.max_persistent'
);
foreach ($expected as $k => $entry)
diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c
index 168e161d1b..8730de04b1 100644
--- a/ext/mysqlnd/mysqlnd_connection.c
+++ b/ext/mysqlnd/mysqlnd_connection.c
@@ -251,6 +251,10 @@ MYSQLND_METHOD(mysqlnd_conn_data, free_options)(MYSQLND_CONN_DATA * conn)
mnd_pefree(conn->options->connect_attr, pers);
conn->options->connect_attr = NULL;
}
+ if (conn->options->local_infile_directory) {
+ mnd_pefree(conn->options->local_infile_directory, pers);
+ conn->options->local_infile_directory = NULL;
+ }
}
/* }}} */
@@ -1648,6 +1652,19 @@ MYSQLND_METHOD(mysqlnd_conn_data, set_client_option)(MYSQLND_CONN_DATA * const c
conn->options->flags &= ~CLIENT_LOCAL_FILES;
}
break;
+ case MYSQL_OPT_LOAD_DATA_LOCAL_DIR:
+ {
+ if (conn->options->local_infile_directory) {
+ mnd_pefree(conn->options->local_infile_directory, conn->persistent);
+ }
+
+ if (!value || (*value == '\0')) {
+ conn->options->local_infile_directory = NULL;
+ } else {
+ conn->options->local_infile_directory = mnd_pestrdup(value, conn->persistent);
+ }
+ break;
+ }
case MYSQL_INIT_COMMAND:
{
char ** new_init_commands;
diff --git a/ext/mysqlnd/mysqlnd_enum_n_def.h b/ext/mysqlnd/mysqlnd_enum_n_def.h
index b65e8523b2..80be26e622 100644
--- a/ext/mysqlnd/mysqlnd_enum_n_def.h
+++ b/ext/mysqlnd/mysqlnd_enum_n_def.h
@@ -129,6 +129,7 @@
#define CR_PARAMS_NOT_BOUND 2031
#define CR_INVALID_PARAMETER_NO 2034
#define CR_INVALID_BUFFER_USE 2035
+#define CR_LOAD_DATA_LOCAL_INFILE_REJECTED 2068
#define MYSQLND_EE_FILENOTFOUND 7890
@@ -247,6 +248,7 @@ typedef enum mysqlnd_client_option
MYSQL_OPT_NET_BUFFER_LENGTH,
MYSQL_OPT_TLS_VERSION,
MYSQL_OPT_SSL_MODE,
+ MYSQL_OPT_LOAD_DATA_LOCAL_DIR,
MYSQLND_DEPRECATED_ENUM1 = 200,
MYSQLND_OPT_INT_AND_FLOAT_NATIVE = 201,
MYSQLND_OPT_NET_CMD_BUFFER_SIZE = 202,
diff --git a/ext/mysqlnd/mysqlnd_loaddata.c b/ext/mysqlnd/mysqlnd_loaddata.c
index 4cd0433877..c00800c451 100644
--- a/ext/mysqlnd/mysqlnd_loaddata.c
+++ b/ext/mysqlnd/mysqlnd_loaddata.c
@@ -149,12 +149,51 @@ mysqlnd_handle_local_infile(MYSQLND_CONN_DATA * conn, const char * const filenam
MYSQLND_INFILE infile;
MYSQLND_PFC * net = conn->protocol_frame_codec;
MYSQLND_VIO * vio = conn->vio;
+ bool is_local_infile_enabled = (conn->options->flags & CLIENT_LOCAL_FILES) == CLIENT_LOCAL_FILES;
+ const char* local_infile_directory = conn->options->local_infile_directory;
+ bool is_local_infile_dir_set = local_infile_directory != NULL;
+ bool prerequisities_ok = TRUE;
DBG_ENTER("mysqlnd_handle_local_infile");
- if (!(conn->options->flags & CLIENT_LOCAL_FILES)) {
- SET_CLIENT_ERROR(conn->error_info, CR_UNKNOWN_ERROR, UNKNOWN_SQLSTATE,
- "LOAD DATA LOCAL INFILE is forbidden, check mysqli.allow_local_infile");
+ /*
+ if local_infile is disabled, and local_infile_dir is not set, then operation is forbidden
+ */
+ if (!is_local_infile_enabled && !is_local_infile_dir_set) {
+ SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE,
+ "LOAD DATA LOCAL INFILE is forbidden, check related settings like "
+ "mysqli.allow_local_infile|mysqli.local_infile_directory or "
+ "PDO::MYSQL_ATTR_LOCAL_INFILE|PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY");
+ prerequisities_ok = FALSE;
+ }
+
+ /*
+ if local_infile_dir is set, then check whether it actually exists, and is accessible
+ */
+ if (is_local_infile_dir_set) {
+ php_stream *stream = php_stream_opendir(local_infile_directory, REPORT_ERRORS, NULL);
+ if (stream) {
+ php_stream_closedir(stream);
+ } else {
+ SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE, "cannot open local_infile_directory");
+ prerequisities_ok = FALSE;
+ }
+ }
+
+ /*
+ if local_infile is disabled and local_infile_dir is set, then we have to check whether
+ filename is located inside its subtree
+ but only in such a case, because when local_infile is enabled, then local_infile_dir is ignored
+ */
+ if (prerequisities_ok && !is_local_infile_enabled && is_local_infile_dir_set) {
+ if (php_check_specific_open_basedir(local_infile_directory, filename) == -1) {
+ SET_CLIENT_ERROR(conn->error_info, CR_LOAD_DATA_LOCAL_INFILE_REJECTED, UNKNOWN_SQLSTATE,
+ "LOAD DATA LOCAL INFILE DIRECTORY restriction in effect. Unable to open file");
+ prerequisities_ok = FALSE;
+ }
+ }
+
+ if (!prerequisities_ok) {
/* write empty packet to server */
ret = net->data->m.send(net, vio, empty_packet, 0, conn->stats, conn->error_info);
*is_warning = TRUE;
diff --git a/ext/mysqlnd/mysqlnd_structs.h b/ext/mysqlnd/mysqlnd_structs.h
index 75d8af9acd..6ee057fc72 100644
--- a/ext/mysqlnd/mysqlnd_structs.h
+++ b/ext/mysqlnd/mysqlnd_structs.h
@@ -231,6 +231,8 @@ typedef struct st_mysqlnd_session_options
unsigned int max_allowed_packet;
bool int_and_float_native;
+
+ char *local_infile_directory;
} MYSQLND_SESSION_OPTIONS;
diff --git a/ext/pdo_mysql/config.w32 b/ext/pdo_mysql/config.w32
index 8b5577273d..48e47f7871 100644
--- a/ext/pdo_mysql/config.w32
+++ b/ext/pdo_mysql/config.w32
@@ -10,7 +10,10 @@ if (PHP_PDO_MYSQL != "no") {
ADD_EXTENSION_DEP('pdo_mysql', 'pdo');
} else {
if (CHECK_LIB("libmysql.lib", "pdo_mysql", PHP_PDO_MYSQL) &&
- CHECK_HEADER_ADD_INCLUDE("mysql.h", "CFLAGS_PDO_MYSQL", PHP_PHP_BUILD + "\\include\\mysql;" + PHP_PDO_MYSQL)) {
+ CHECK_HEADER_ADD_INCLUDE("mysql.h", "CFLAGS_PDO_MYSQL",
+ PHP_PDO_MYSQL + "\\include;" +
+ PHP_PHP_BUILD + "\\include\\mysql;" +
+ PHP_PDO_MYSQL)) {
EXTENSION("pdo_mysql", "pdo_mysql.c mysql_driver.c mysql_statement.c", null, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
} else {
WARNING("pdo_mysql not enabled; libraries and headers not found");
diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c
index 6b42335878..94fa2411d6 100644
--- a/ext/pdo_mysql/mysql_driver.c
+++ b/ext/pdo_mysql/mysql_driver.c
@@ -521,6 +521,24 @@ static int pdo_mysql_get_attribute(pdo_dbh_t *dbh, zend_long attr, zval *return_
ZVAL_BOOL(return_value, H->local_infile);
break;
+#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND)
+ case PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY:
+ {
+ const char* local_infile_directory = NULL;
+#ifdef PDO_USE_MYSQLND
+ local_infile_directory = H->server->data->options->local_infile_directory;
+#else
+ mysql_get_option(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, &local_infile_directory);
+#endif
+ if (local_infile_directory) {
+ ZVAL_STRING(return_value, local_infile_directory);
+ } else {
+ ZVAL_NULL(return_value);
+ }
+ break;
+ }
+#endif
+
default:
PDO_DBG_RETURN(0);
}
@@ -724,6 +742,17 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options)
#endif
}
+#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND)
+ zend_string *local_infile_directory = pdo_attr_strval(driver_options, PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, NULL);
+ if (local_infile_directory && !php_check_open_basedir(ZSTR_VAL(local_infile_directory))) {
+ if (mysql_options(H->server, MYSQL_OPT_LOAD_DATA_LOCAL_DIR, (const char *)ZSTR_VAL(local_infile_directory))) {
+ zend_string_release(local_infile_directory);
+ pdo_mysql_error(dbh);
+ goto cleanup;
+ }
+ zend_string_release(local_infile_directory);
+ }
+#endif
#ifdef MYSQL_OPT_RECONNECT
/* since 5.0.3, the default for this option is 0 if not specified.
* we want the old behaviour
diff --git a/ext/pdo_mysql/pdo_mysql.c b/ext/pdo_mysql/pdo_mysql.c
index 84828edaa4..1bfc5ff874 100644
--- a/ext/pdo_mysql/pdo_mysql.c
+++ b/ext/pdo_mysql/pdo_mysql.c
@@ -124,6 +124,9 @@ static PHP_MINIT_FUNCTION(pdo_mysql)
#ifdef PDO_USE_MYSQLND
REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_SSL_VERIFY_SERVER_CERT", (zend_long)PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT);
#endif
+#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND)
+ REGISTER_PDO_CLASS_CONST_LONG("MYSQL_ATTR_LOCAL_INFILE_DIRECTORY", (zend_long)PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY);
+#endif
#ifdef PDO_USE_MYSQLND
mysqlnd_reverse_api_register_api(&pdo_mysql_reverse_api);
diff --git a/ext/pdo_mysql/php_pdo_mysql_int.h b/ext/pdo_mysql/php_pdo_mysql_int.h
index 75287e7904..6077ce8245 100644
--- a/ext/pdo_mysql/php_pdo_mysql_int.h
+++ b/ext/pdo_mysql/php_pdo_mysql_int.h
@@ -178,6 +178,9 @@ enum {
#ifdef PDO_USE_MYSQLND
PDO_MYSQL_ATTR_SSL_VERIFY_SERVER_CERT,
#endif
+#if MYSQL_VERSION_ID >= 80021 || defined(PDO_USE_MYSQLND)
+ PDO_MYSQL_ATTR_LOCAL_INFILE_DIRECTORY,
+#endif
};
#endif
diff --git a/ext/pdo_mysql/tests/bug70389.phpt b/ext/pdo_mysql/tests/bug70389.phpt
index 7815b21255..adfd65f5ab 100644
--- a/ext/pdo_mysql/tests/bug70389.phpt
+++ b/ext/pdo_mysql/tests/bug70389.phpt
@@ -26,8 +26,8 @@ var_dump($flags);
array(3) {
[%d]=>
bool(true)
- [1001]=>
+ [%d]=>
bool(true)
- [12]=>
+ [%d]=>
bool(true)
}
diff --git a/ext/pdo_mysql/tests/foo/bar/bar.data b/ext/pdo_mysql/tests/foo/bar/bar.data
new file mode 100644
index 0000000000..3fa90ba016
--- /dev/null
+++ b/ext/pdo_mysql/tests/foo/bar/bar.data
@@ -0,0 +1,3 @@
+97;first
+98;second
+99;third
diff --git a/ext/pdo_mysql/tests/foo/foo.data b/ext/pdo_mysql/tests/foo/foo.data
new file mode 100644
index 0000000000..70d8d301e9
--- /dev/null
+++ b/ext/pdo_mysql/tests/foo/foo.data
@@ -0,0 +1,3 @@
+1;one
+2;two
+3;three
diff --git a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
index efbf3c51c8..76db58dff2 100644
--- a/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
+++ b/ext/pdo_mysql/tests/pdo_mysql___construct_options.phpt
@@ -156,6 +156,11 @@ MySQLPDOTest::skip();
set_option_and_check(33, PDO::MYSQL_ATTR_DIRECT_QUERY, 1, 'PDO::MYSQL_ATTR_DIRECT_QUERY');
set_option_and_check(34, PDO::MYSQL_ATTR_DIRECT_QUERY, 0, 'PDO::MYSQL_ATTR_DIRECT_QUERY');
+ if (defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ set_option_and_check(35, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, null, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY');
+ // libmysqlclient returns the directory with a trailing slash.
+ // set_option_and_check(36, PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY, __DIR__, 'PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY');
+ }
} catch (PDOException $e) {
printf("[001] %s, [%s] %s Line: %s\n",
$e->getMessage(),
diff --git a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt
index c9877f3ac1..205e059b54 100644
--- a/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt
+++ b/ext/pdo_mysql/tests/pdo_mysql_class_constants.phpt
@@ -13,6 +13,16 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) {
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+ function get_client_version() {
+ if (extension_loaded('mysqli')) {
+ return mysqli_get_client_version();
+ }
+ /* XXX the MySQL client library version isn't exposed with any
+ constants, the single possibility is to use the PDO::getAttribute().
+ This however will fail with no connection. */
+ return MySQLPDOTest::getClientVersion(MySQLPDOTest::factory());
+ }
+
$expected = array(
'MYSQL_ATTR_USE_BUFFERED_QUERY' => true,
'MYSQL_ATTR_LOCAL_INFILE' => true,
@@ -38,15 +48,12 @@ if (!extension_loaded('mysqli') && !extension_loaded('mysqlnd')) {
if (extension_loaded('mysqlnd')) {
$expected['MYSQL_ATTR_SSL_VERIFY_SERVER_CERT'] = true;
$expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true;
- } else if (extension_loaded('mysqli')) {
- if (mysqli_get_client_version() > 50605) {
- $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true;
- }
- } else if (MySQLPDOTest::getClientVersion(MySQLPDOTest::factory()) > 50605) {
- /* XXX the MySQL client library version isn't exposed with any
- constants, the single possibility is to use the PDO::getAttribute().
- This however will fail with no connection. */
- $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true;
+ } else if (get_client_version() > 50605) {
+ $expected['MYSQL_ATTR_SERVER_PUBLIC_KEY'] = true;
+ }
+
+ if (MySQLPDOTest::isPDOMySQLnd() || get_client_version() >= 80021) {
+ $expected['MYSQL_ATTR_LOCAL_INFILE_DIRECTORY'] = true;
}
/*
diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt
index 810adce7e7..9a12f837fb 100644
--- a/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt
+++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_default_off.phpt
@@ -5,6 +5,9 @@ ensure default for local infile is off
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
MySQLPDOTest::skip();
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
?>
--FILE--
<?php
@@ -17,8 +20,10 @@ $pass = PDO_MYSQL_TEST_PASS;
$db = new PDO($dsn, $user, $pass);
echo var_export($db->getAttribute(PDO::MYSQL_ATTR_LOCAL_INFILE)), "\n";
+echo var_export($db->getAttribute(PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY)), "\n";
echo "done!\n";
?>
--EXPECT--
false
+NULL
done!
diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt
new file mode 100644
index 0000000000..edabfbc07f
--- /dev/null
+++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_allowed.phpt
@@ -0,0 +1,85 @@
+--TEST--
+PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY vs access allowed
+--SKIPIF--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->exec($sql);
+ if ($ret !== $exp) {
+ printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n",
+ $offset, $exp, gettype($exp), $ret, gettype($ret), $sql,
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+ } catch (PDOException $e) {
+ printf("[%03d] '%s' has failed, [%s] %s\n",
+ $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+
+ return true;
+ }
+
+ require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+ putenv('PDOTEST_ATTR='.serialize([
+ PDO::MYSQL_ATTR_LOCAL_INFILE=>false,
+ PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo"
+ ]));
+ $db = MySQLPDOTest::factory();
+ MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db));
+
+ try {
+ exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0);
+ exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0);
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/bar/bar.data');
+
+ $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath));
+ if (exec_and_count(3, $db, $sql, 3)) {
+ $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC');
+ $expected = array(
+ array("id" => 97, "col1" => "first"),
+ array("id" => 98, "col1" => "second"),
+ array("id" => 99, "col1" => "third"),
+ );
+ $ret = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($expected as $offset => $exp) {
+ foreach ($exp as $key => $value) {
+ $actual_value = trim(strval($ret[$offset][$key]));
+ if ($actual_value != $value) {
+ printf("Results seem wrong, check manually\n");
+ echo "------ EXPECTED OUTPUT ------\n";
+ var_dump($expected);
+ echo "------ ACTUAL OUTPUT ------\n";
+ var_dump($ret);
+ break 2;
+ }
+ }
+ }
+ }
+ } catch (PDOException $e) {
+ printf("[001] %s, [%s] %s\n",
+ $e->getMessage(),
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ }
+
+ print "done!";
+?>
+--CLEAN--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->exec('DROP TABLE IF EXISTS test');
+?>
+--EXPECT--
+done!
diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt
new file mode 100644
index 0000000000..c955c1daad
--- /dev/null
+++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_directory_denied.phpt
@@ -0,0 +1,76 @@
+--TEST--
+PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY vs access denied
+--SKIPIF--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->exec($sql);
+ if ($ret !== $exp) {
+ printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n",
+ $offset, $exp, gettype($exp), $ret, gettype($ret), $sql,
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+ } catch (PDOException $e) {
+ printf("[%03d] '%s' has failed, [%s] %s\n",
+ $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+
+ return true;
+ }
+
+ require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+ putenv('PDOTEST_ATTR='.serialize([
+ PDO::MYSQL_ATTR_LOCAL_INFILE=>false,
+ PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo/bar"
+ ]));
+ $db = MySQLPDOTest::factory();
+ MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db));
+
+ try {
+ exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0);
+ exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0);
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+
+ $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath));
+ if (exec_and_count(3, $db, $sql, false)) {
+ $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC');
+ $expected = array();
+ $ret = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if ($ret != $expected) {
+ printf("Results seem wrong, check manually\n");
+ echo "------ EXPECTED OUTPUT ------\n";
+ var_dump($expected);
+ echo "------ ACTUAL OUTPUT ------\n";
+ var_dump($ret);
+ }
+ }
+ } catch (PDOException $e) {
+ printf("[001] %s, [%s] %s\n",
+ $e->getMessage(),
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ }
+
+ print "done!";
+?>
+--CLEAN--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->exec('DROP TABLE IF EXISTS test');
+?>
+--EXPECTF--
+Warning: PDO::exec(): SQLSTATE[HY000]: General error: 2068 LOAD DATA LOCAL INFILE %s in %s on line %d
+done!
diff --git a/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt b/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt
new file mode 100644
index 0000000000..c6d60fd1ab
--- /dev/null
+++ b/ext/pdo_mysql/tests/pdo_mysql_local_infile_overrides_local_infile_directory.phpt
@@ -0,0 +1,85 @@
+--TEST--
+PDO::MYSQL_ATTR_LOCAL_INFILE overrides PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY
+--SKIPIF--
+<?php
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipif.inc');
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+MySQLPDOTest::skip();
+require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'skipifinfilenotallowed.inc');
+if (!defined('PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY')) {
+ die("skip No MYSQL_ATTR_LOCAL_INFILE_DIRECTORY support");
+}
+?>
+--FILE--
+<?php
+ function exec_and_count($offset, &$db, $sql, $exp) {
+ try {
+ $ret = $db->exec($sql);
+ if ($ret !== $exp) {
+ printf("[%03d] Expecting '%s'/%s got '%s'/%s when running '%s', [%s] %s\n",
+ $offset, $exp, gettype($exp), $ret, gettype($ret), $sql,
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+ } catch (PDOException $e) {
+ printf("[%03d] '%s' has failed, [%s] %s\n",
+ $offset, $sql, $db->errorCode(), implode(' ', $db->errorInfo()));
+ return false;
+ }
+
+ return true;
+ }
+
+ require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
+ putenv('PDOTEST_ATTR='.serialize([
+ PDO::MYSQL_ATTR_LOCAL_INFILE=>true,
+ PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY=>__DIR__."/foo/bar"
+ ]));
+ $db = MySQLPDOTest::factory();
+ MySQLPDOTest::createTestTable($db, MySQLPDOTest::detect_transactional_mysql_engine($db));
+
+ try {
+ exec_and_count(1, $db, 'DROP TABLE IF EXISTS test', 0);
+ exec_and_count(2, $db, sprintf('CREATE TABLE test(id INT NOT NULL PRIMARY KEY, col1 CHAR(10)) ENGINE=%s', PDO_MYSQL_TEST_ENGINE), 0);
+
+ $filepath = str_replace('\\', '/', __DIR__.'/foo/foo.data');
+
+ $sql = sprintf("LOAD DATA LOCAL INFILE %s INTO TABLE test FIELDS TERMINATED BY ';' LINES TERMINATED BY '\n'", $db->quote($filepath));
+ if (exec_and_count(3, $db, $sql, 3)) {
+ $stmt = $db->query('SELECT id, col1 FROM test ORDER BY id ASC');
+ $expected = array(
+ array("id" => 1, "col1" => "one"),
+ array("id" => 2, "col1" => "two"),
+ array("id" => 3, "col1" => "three"),
+ );
+ $ret = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($expected as $offset => $exp) {
+ foreach ($exp as $key => $value) {
+ $actual_value = trim(strval($ret[$offset][$key]));
+ if ($actual_value != $value) {
+ printf("Results seem wrong, check manually\n");
+ echo "------ EXPECTED OUTPUT ------\n";
+ var_dump($expected);
+ echo "------ ACTUAL OUTPUT ------\n";
+ var_dump($ret);
+ break 2;
+ }
+ }
+ }
+ }
+ } catch (PDOException $e) {
+ printf("[001] %s, [%s] %s\n",
+ $e->getMessage(),
+ $db->errorCode(), implode(' ', $db->errorInfo()));
+ }
+
+ print "done!";
+?>
+--CLEAN--
+<?php
+require dirname(__FILE__) . '/mysql_pdo_test.inc';
+$db = MySQLPDOTest::factory();
+$db->exec('DROP TABLE IF EXISTS test');
+?>
+--EXPECT--
+done!
diff --git a/ext/pdo_mysql/tests/skipifinfilenotallowed.inc b/ext/pdo_mysql/tests/skipifinfilenotallowed.inc
new file mode 100644
index 0000000000..abfea299b7
--- /dev/null
+++ b/ext/pdo_mysql/tests/skipifinfilenotallowed.inc
@@ -0,0 +1,6 @@
+<?php
+$db = MySQLPDOTest::factory();
+$stmt = $db->query("SHOW VARIABLES LIKE 'local_infile'");
+if (($row = $stmt->fetch(PDO::FETCH_ASSOC)) && ($row['value'] != 'ON'))
+ die("skip Server variable 'local_infile' seems not set to 'ON', found '". $row['value'] ."'");
+?>