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