PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / trunk
Matomo Analytics – Powerful, Privacy-First Insights for WordPress vtrunk
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Db / BatchInsert.php
matomo / app / core / Db Last commit date
Adapter 1 month ago Schema 2 weeks ago Adapter.php 1 month ago AdapterInterface.php 1 month ago BatchInsert.php 6 months ago Schema.php 1 month ago SchemaInterface.php 1 month ago Settings.php 1 year ago TransactionLevel.php 1 year ago TransactionalDatabaseDynamicTrait.php 1 year ago TransactionalDatabaseInterface.php 1 year ago TransactionalDatabaseStaticTrait.php 1 year ago
BatchInsert.php
267 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik\Db;
10
11 use Exception;
12 use Piwik\Common;
13 use Piwik\Config;
14 use Piwik\Config\DatabaseConfig;
15 use Piwik\Container\StaticContainer;
16 use Piwik\Db;
17 use Piwik\ExceptionHandler;
18 use Piwik\Log;
19 use Piwik\SettingsServer;
20 use Piwik\SettingsPiwik;
21 class BatchInsert
22 {
23 /**
24 * Performs a batch insert into a specific table by iterating through the data
25 *
26 * NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available
27 *
28 * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
29 * @param array $fields array of unquoted field names
30 * @param array $values array of data to be inserted
31 * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows
32 */
33 public static function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = \true)
34 {
35 $tableName = preg_replace('/[^a-zA-Z\\d_-]/', '', $tableName);
36 $fieldList = '(' . join(',', $fields) . ')';
37 $ignore = $ignoreWhenDuplicate ? 'IGNORE' : '';
38 foreach ($values as $row) {
39 $row = array_values($row);
40 $query = "INSERT {$ignore} INTO `" . $tableName . "`\n\t\t\t\t\t {$fieldList}\n\t\t\t\t\t VALUES (" . Common::getSqlStringFieldsArray($row) . ")";
41 Db::query($query, $row);
42 }
43 }
44 /**
45 * Performs a batch insert into a specific table by sending all data in one SQL statement.
46 *
47 * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
48 * @param array $fields array of unquoted field names
49 * @param array $values array of data to be inserted
50 * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows
51 */
52 public static function tableInsertBatchSql($tableName, $fields, $values, $ignoreWhenDuplicate = \true)
53 {
54 $insertLines = array();
55 $bind = array();
56 foreach ($values as $row) {
57 $insertLines[] = "(" . Common::getSqlStringFieldsArray($row) . ")";
58 $bind = array_merge($bind, $row);
59 }
60 $fieldList = '(' . implode(',', $fields) . ')';
61 $insertLines = implode(',', $insertLines);
62 $ignore = $ignoreWhenDuplicate ? 'IGNORE' : '';
63 $query = "INSERT {$ignore} INTO {$tableName} {$fieldList} VALUES {$insertLines}";
64 Db::query($query, $bind);
65 }
66 /**
67 * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs,
68 * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs.
69 *
70 * @param string $tableName PREFIXED table name! you must call Common::prefixTable() before passing the table name
71 * @param array $fields array of unquoted field names
72 * @param array $values array of data to be inserted
73 * @param bool $throwException Whether to throw an exception that was caught while trying
74 * LOAD DATA INFILE, or not.
75 * @param string $charset The charset to use, defaults to utf8
76 * @throws Exception
77 * @return bool True if the bulk LOAD was used, false if we fallback to plain INSERTs
78 */
79 public static function tableInsertBatch($tableName, $fields, $values, $throwException = \false, $charset = 'utf8')
80 {
81 $loadDataInfileEnabled = Config::getInstance()->General['enable_load_data_infile'];
82 if ($loadDataInfileEnabled && Db::get()->hasBulkLoader()) {
83 $path = self::getBestPathForLoadData();
84 $instanceId = SettingsPiwik::getPiwikInstanceId();
85 if (empty($instanceId)) {
86 $instanceId = '';
87 }
88 $filePath = $path . $tableName . '-' . $instanceId . Common::generateUniqId() . '.csv';
89 // always use utf8 for TiDb, as TiDb has problems with latin1
90 if (DatabaseConfig::isTiDb()) {
91 $charset = 'utf8';
92 }
93 try {
94 $fileSpec = array(
95 'delim' => "\t",
96 'quote' => '"',
97 // chr(34)
98 'escape' => '\\\\',
99 // chr(92)
100 'escapespecial_cb' => function ($str) {
101 return str_replace(array(chr(92), chr(34)), array(chr(92) . chr(92), chr(92) . chr(34)), $str);
102 },
103 'eol' => "\r\n",
104 'null' => 'NULL',
105 'charset' => $charset,
106 );
107 self::createCSVFile($filePath, $fileSpec, $values);
108 if (!is_readable($filePath)) {
109 throw new Exception("File {$filePath} could not be read.");
110 }
111 $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec);
112 if ($rc) {
113 unlink($filePath);
114 return \true;
115 }
116 } catch (Exception $e) {
117 if ($throwException) {
118 throw $e;
119 }
120 }
121 // if all else fails, fallback to a series of INSERTs
122 if (file_exists($filePath)) {
123 @unlink($filePath);
124 }
125 }
126 self::tableInsertBatchIterate($tableName, $fields, $values);
127 return \false;
128 }
129 private static function getBestPathForLoadData()
130 {
131 try {
132 $path = Db::fetchOne('SELECT @@secure_file_priv');
133 // was introduced in 5.0.38
134 } catch (Exception $e) {
135 // we do not rethrow exception as an error is expected if MySQL is < 5.0.38
136 // in this case tableInsertBatch might still work
137 }
138 if (empty($path) || !@is_dir($path) || !@is_writable($path)) {
139 $path = StaticContainer::get('path.tmp') . '/assets/';
140 } elseif (!Common::stringEndsWith($path, '/')) {
141 $path .= '/';
142 }
143 return $path;
144 }
145 /**
146 * Batch insert into table from CSV (or other delimited) file.
147 *
148 * @param string $tableName Name of table
149 * @param array $fields Field names
150 * @param string $filePath Path name of a file.
151 * @param array $fileSpec File specifications (delimiter, line terminator, etc)
152 *
153 * @throws Exception
154 * @return bool True if successful; false otherwise
155 */
156 public static function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec)
157 {
158 // Chroot environment: prefix the path with the absolute chroot path
159 $chrootPath = Config::getInstance()->General['absolute_chroot_path'];
160 if (!empty($chrootPath)) {
161 $filePath = $chrootPath . $filePath;
162 }
163 // On Windows, MySQL expects forward slashes as directory separators
164 if (SettingsServer::isWindows()) {
165 $filePath = str_replace('\\', '/', $filePath);
166 }
167 $query = "\n\t\t\t\t'{$filePath}'\n\t\t\tREPLACE\n\t\t\tINTO TABLE\n\t\t\t\t`" . $tableName . "`";
168 if (isset($fileSpec['charset'])) {
169 $query .= ' CHARACTER SET ' . $fileSpec['charset'];
170 }
171 $fieldList = '(' . join(',', $fields) . ')';
172 $query .= "\n\t\t\tFIELDS TERMINATED BY\n\t\t\t\t'" . $fileSpec['delim'] . "'\n\t\t\tENCLOSED BY\n\t\t\t\t'" . $fileSpec['quote'] . "'\n\t\t";
173 if (isset($fileSpec['escape'])) {
174 $query .= " ESCAPED BY '" . $fileSpec['escape'] . "'";
175 }
176 $query .= "\n\t\t\tLINES TERMINATED BY\n\t\t\t\t'" . $fileSpec['eol'] . "'\n\t\t\t{$fieldList}\n\t\t";
177 /*
178 * First attempt: assume web server and MySQL server are on the same machine;
179 * this requires that the db user have the FILE privilege; however, since this is
180 * a global privilege, it may not be granted due to security concerns
181 */
182 if (Config::getInstance()->General['multi_server_environment']) {
183 $keywords = array();
184 // don't try 'LOAD DATA INFILE' if in a multi_server_environment
185 } else {
186 $keywords = array('');
187 }
188 /*
189 * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server;
190 * the LOCAL keyword may trigger a known PHP PDO\MYSQL bug when MySQL not built with --enable-local-infile
191 * @see https://bugs.php.net/bug.php?id=54158
192 */
193 $openBaseDir = ini_get('open_basedir');
194 $safeMode = ini_get('safe_mode');
195 if ((function_exists('mysqli_get_client_stats') || empty($openBaseDir)) && empty($safeMode)) {
196 // 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)
197 // and if safe mode is not enabled
198 $keywords[] = 'LOCAL ';
199 }
200 $exceptions = array();
201 foreach ($keywords as $keyword) {
202 $queryStart = 'LOAD DATA ' . $keyword . 'INFILE ';
203 $sql = $queryStart . $query;
204 try {
205 $result = @Db::exec($sql);
206 if (empty($result) || $result < 0) {
207 continue;
208 }
209 return \true;
210 } catch (Exception $e) {
211 $code = $e->getCode();
212 $message = $e->getMessage() . ($code ? "[{$code}]" : '');
213 if (ExceptionHandler::shouldPrintBackTraceWithMessage()) {
214 $message .= "\n" . $e->getTraceAsString();
215 }
216 $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message;
217 }
218 }
219 if (count($exceptions)) {
220 $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions);
221 Log::info($message);
222 throw new Exception($message);
223 }
224 return \false;
225 }
226 /**
227 * Create CSV (or other delimited) files
228 *
229 * @param string $filePath filename to create
230 * @param array $fileSpec File specifications (delimiter, line terminator, etc)
231 * @param array $rows Array of array corresponding to rows of values
232 * @throws Exception if unable to create or write to file
233 */
234 protected static function createCSVFile($filePath, $fileSpec, $rows)
235 {
236 // Set up CSV delimiters, quotes, etc
237 $delim = $fileSpec['delim'];
238 $quote = $fileSpec['quote'];
239 $eol = $fileSpec['eol'];
240 $null = $fileSpec['null'];
241 $escapespecial_cb = $fileSpec['escapespecial_cb'];
242 $fp = @fopen($filePath, 'wb');
243 if (!$fp) {
244 throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.');
245 }
246 foreach ($rows as $row) {
247 $output = '';
248 foreach ($row as $value) {
249 if (!isset($value) || is_null($value) || $value === \false) {
250 $output .= $null . $delim;
251 } else {
252 $output .= $quote . $escapespecial_cb($value) . $quote . $delim;
253 }
254 }
255 // Replace delim with eol
256 $output = substr_replace($output, $eol, -1);
257 $ret = fwrite($fp, $output);
258 if (!$ret) {
259 fclose($fp);
260 throw new Exception('Error writing to the tmp file ' . $filePath);
261 }
262 }
263 fclose($fp);
264 @chmod($filePath, 0777);
265 }
266 }
267