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