General['enable_load_data_infile']; if ($loadDataInfileEnabled && Db::get()->hasBulkLoader()) { $path = self::getBestPathForLoadData(); $instanceId = SettingsPiwik::getPiwikInstanceId(); if (empty($instanceId)) { $instanceId = ''; } $filePath = $path . $tableName . '-' . $instanceId . Common::generateUniqId() . '.csv'; try { $fileSpec = array( 'delim' => "\t", 'quote' => '"', // chr(34) 'escape' => '\\\\', // chr(92) 'escapespecial_cb' => function ($str) { return str_replace(array(chr(92), chr(34)), array(chr(92) . chr(92), chr(92) . chr(34)), $str); }, 'eol' => "\r\n", 'null' => 'NULL', 'charset' => $charset ); self::createCSVFile($filePath, $fileSpec, $values); if (!is_readable($filePath)) { throw new Exception("File $filePath could not be read."); } $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec); if ($rc) { unlink($filePath); return true; } } catch (Exception $e) { if ($throwException) { throw $e; } } // if all else fails, fallback to a series of INSERTs if (file_exists($filePath)) { @unlink($filePath); } } self::tableInsertBatchIterate($tableName, $fields, $values); return false; } private static function getBestPathForLoadData() { try { $path = Db::fetchOne('SELECT @@secure_file_priv'); // was introduced in 5.0.38 } catch (Exception $e) { // we do not rethrow exception as an error is expected if MySQL is < 5.0.38 // in this case tableInsertBatch might still work } if (empty($path) || !@is_dir($path) || !@is_writable($path)) { $path = StaticContainer::get('path.tmp') . '/assets/'; } elseif (!Common::stringEndsWith($path, '/')) { $path .= '/'; } return $path; } /** * Batch insert into table from CSV (or other delimited) file. * * @param string $tableName Name of table * @param array $fields Field names * @param string $filePath Path name of a file. * @param array $fileSpec File specifications (delimiter, line terminator, etc) * * @throws Exception * @return bool True if successful; false otherwise */ public static function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec) { // Chroot environment: prefix the path with the absolute chroot path $chrootPath = Config::getInstance()->General['absolute_chroot_path']; if (!empty($chrootPath)) { $filePath = $chrootPath . $filePath; } // On Windows, MySQL expects forward slashes as directory separators if (SettingsServer::isWindows()) { $filePath = str_replace('\\', '/', $filePath); } $query = " '$filePath' REPLACE INTO TABLE `" . $tableName . "`"; if (isset($fileSpec['charset'])) { $query .= ' CHARACTER SET ' . $fileSpec['charset']; } $fieldList = '(' . join(',', $fields) . ')'; $query .= " FIELDS TERMINATED BY '" . $fileSpec['delim'] . "' ENCLOSED BY '" . $fileSpec['quote'] . "' "; if (isset($fileSpec['escape'])) { $query .= " ESCAPED BY '" . $fileSpec['escape'] . "'"; } $query .= " LINES TERMINATED BY '" . $fileSpec['eol'] . "' $fieldList "; /* * First attempt: assume web server and MySQL server are on the same machine; * this requires that the db user have the FILE privilege; however, since this is * a global privilege, it may not be granted due to security concerns */ if (Config::getInstance()->General['multi_server_environment']) { $keywords = array(); // don't try 'LOAD DATA INFILE' if in a multi_server_environment } else { $keywords = array(''); } /* * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server; * the LOCAL keyword may trigger a known PHP PDO\MYSQL bug when MySQL not built with --enable-local-infile * @see http://bugs.php.net/bug.php?id=54158 */ $openBaseDir = ini_get('open_basedir'); $safeMode = ini_get('safe_mode'); if ((function_exists('mysqli_get_client_stats') || empty($openBaseDir)) && empty($safeMode)) { // php 5.x - LOAD DATA LOCAL INFILE only used if open_basedir is not set (or we're using a non-buggy version of mysqlnd) // and if safe mode is not enabled $keywords[] = 'LOCAL '; } $exceptions = array(); foreach ($keywords as $keyword) { $queryStart = 'LOAD DATA ' . $keyword . 'INFILE '; $sql = $queryStart . $query; try { $result = @Db::exec($sql); if (empty($result) || $result < 0) { continue; } return true; } catch (Exception $e) { $code = $e->getCode(); $message = $e->getMessage() . ($code ? "[$code]" : ''); $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message; } } if (count($exceptions)) { $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions); Log::info($message); throw new Exception($message); } return false; } /** * Create CSV (or other delimited) files * * @param string $filePath filename to create * @param array $fileSpec File specifications (delimiter, line terminator, etc) * @param array $rows Array of array corresponding to rows of values * @throws Exception if unable to create or write to file */ protected static function createCSVFile($filePath, $fileSpec, $rows) { // Set up CSV delimiters, quotes, etc $delim = $fileSpec['delim']; $quote = $fileSpec['quote']; $eol = $fileSpec['eol']; $null = $fileSpec['null']; $escapespecial_cb = $fileSpec['escapespecial_cb']; $fp = @fopen($filePath, 'wb'); if (!$fp) { throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.'); } foreach ($rows as $row) { $output = ''; foreach ($row as $value) { if (!isset($value) || is_null($value) || $value === false) { $output .= $null . $delim; } else { $output .= $quote . $escapespecial_cb($value) . $quote . $delim; } } // Replace delim with eol $output = substr_replace($output, $eol, -1); $ret = fwrite($fp, $output); if (!$ret) { fclose($fp); throw new Exception('Error writing to the tmp file ' . $filePath); } } fclose($fp); @chmod($filePath, 0777); } }