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