PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.14.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.14.2
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 / classes / WpMatomo / Db / WordPress.php
matomo / classes / WpMatomo / Db Last commit date
Settings.php 4 years ago WordPress.php 4 years ago WordPressDbStatement.php 4 years ago WordPressTracker.php 4 years ago
WordPress.php
487 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 * @package matomo
8 */
9
10 namespace Piwik\Db\Adapter;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // if accessed directly
14 }
15
16 require_once 'WordPressDbStatement.php';
17 require_once 'WordPressTracker.php';
18
19 class WordPress extends Mysqli {
20
21 // needed to be compatbile with mysqli class when `getConnection()` is called and we cannot return the
22 // actual connection but return an instance of this.
23 public $error = '';
24
25 private $old_suppress_errors_value = null;
26
27 /**
28 * Return default port.
29 *
30 * @return int
31 */
32 public static function getDefaultPort() {
33 return 3306;
34 }
35
36 /**
37 * Returns true if this adapter supports blobs as fields
38 *
39 * @return bool
40 */
41 public function hasBlobDataType() {
42 return true;
43 }
44
45 /**
46 * Returns true if this adapter supports bulk loading
47 *
48 * @return bool
49 */
50 public function hasBulkLoader() {
51 return false;
52 }
53
54
55 public static function isEnabled() {
56 return true;
57 }
58
59 public function getConnection() {
60 return $this;
61 }
62
63 /**
64 * Is the connection character set equal to utf8?
65 *
66 * @return bool
67 */
68 public function isConnectionUTF8() {
69 $value = $this->fetchOne( 'SELECT @@character_set_client;' );
70
71 return ! empty( $value ) && strpos(strtolower( $value ), 'utf8') === 0;
72 }
73
74 public function checkClientVersion() {
75 // not implemented as we don't need to check that
76 }
77
78 public function getClientVersion() {
79 $value = $this->fetchOne( 'SELECT @@version;' );
80
81 return $value;
82 }
83
84 public function closeConnection() {
85 // we do not want to disconnect WordPress DB ever as it breaks eg the tests where it loses all
86 // temporary tables... also we should leave it up to WordPress whether it wants to close db or not
87 // global $wpdb;
88 // $wpdb->close();
89 // if ($this->_connection) {
90 // parent::closeConnection();
91 // }
92 }
93
94 public function lastInsertId( $tableName = null, $primaryKey = null ) {
95 global $wpdb;
96
97 if ( empty( $wpdb->insert_id ) ) {
98 return $this->fetchOne( 'SELECT LAST_INSERT_ID()' );
99 }
100
101 return $wpdb->insert_id;
102 }
103
104 public function listTables() {
105 global $wpdb;
106 $sql = 'SHOW TABLES';
107
108 $tables = $wpdb->get_results( $sql, ARRAY_N );
109 $result = [];
110 foreach ($tables as $table) {
111 $result[] = $table[0];
112 }
113 return $result;
114 }
115
116 public function describeTable($tableName, $schemaName = null)
117 {
118 global $wpdb;
119
120 if ($schemaName) {
121 $sql = 'DESCRIBE ' . $this->quoteIdentifier("$schemaName.$tableName", true);
122 } else {
123 $sql = 'DESCRIBE ' . $this->quoteIdentifier($tableName, true);
124 }
125
126 $result = $wpdb->get_results( $sql, ARRAY_A );
127
128 $desc = array();
129
130 $row_defaults = array(
131 'Length' => null,
132 'Scale' => null,
133 'Precision' => null,
134 'Unsigned' => null,
135 'Primary' => false,
136 'PrimaryPosition' => null,
137 'Identity' => false
138 );
139 $i = 1;
140 $p = 1;
141 foreach ($result as $key => $row) {
142 $row = array_merge($row_defaults, $row);
143 if (preg_match('/unsigned/', $row['Type'])) {
144 $row['Unsigned'] = true;
145 }
146 if (preg_match('/^((?:var)?char)\((\d+)\)/', $row['Type'], $matches)) {
147 $row['Type'] = $matches[1];
148 $row['Length'] = $matches[2];
149 } else if (preg_match('/^decimal\((\d+),(\d+)\)/', $row['Type'], $matches)) {
150 $row['Type'] = 'decimal';
151 $row['Precision'] = $matches[1];
152 $row['Scale'] = $matches[2];
153 } else if (preg_match('/^float\((\d+),(\d+)\)/', $row['Type'], $matches)) {
154 $row['Type'] = 'float';
155 $row['Precision'] = $matches[1];
156 $row['Scale'] = $matches[2];
157 } else if (preg_match('/^((?:big|medium|small|tiny)?int)\((\d+)\)/', $row['Type'], $matches)) {
158 $row['Type'] = $matches[1];
159 /**
160 * The optional argument of a MySQL int type is not precision
161 * or length; it is only a hint for display width.
162 */
163 }
164 if (strtoupper($row['Key']) == 'PRI') {
165 $row['Primary'] = true;
166 $row['PrimaryPosition'] = $p;
167 if ($row['Extra'] == 'auto_increment') {
168 $row['Identity'] = true;
169 } else {
170 $row['Identity'] = false;
171 }
172 ++$p;
173 }
174 $desc[$this->foldCase($row['Field'])] = array(
175 'SCHEMA_NAME' => null, // @todo
176 'TABLE_NAME' => $this->foldCase($tableName),
177 'COLUMN_NAME' => $this->foldCase($row['Field']),
178 'COLUMN_POSITION' => $i,
179 'DATA_TYPE' => $row['Type'],
180 'DEFAULT' => $row['Default'],
181 'NULLABLE' => (bool) ($row['Null'] == 'YES'),
182 'LENGTH' => $row['Length'],
183 'SCALE' => $row['Scale'],
184 'PRECISION' => $row['Precision'],
185 'UNSIGNED' => $row['Unsigned'],
186 'PRIMARY' => $row['Primary'],
187 'PRIMARY_POSITION' => $row['PrimaryPosition'],
188 'IDENTITY' => $row['Identity']
189 );
190 ++$i;
191 }
192 return $desc;
193 }
194
195 public function getServerVersion() {
196 global $wpdb;
197
198 return $wpdb->db_version();
199 }
200
201 private function getErrorNumberFromMessage( $message ) {
202 if ( preg_match( '/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $message, $match ) ) {
203 return $match[1];
204 }
205 }
206
207 /**
208 * Test error number
209 *
210 * @param \Exception $e
211 * @param string $errno
212 *
213 * @return bool
214 */
215 public function isErrNo( $e, $errno ) {
216 $errorCode = $this->getErrorNumberFromMessage($e->getMessage());
217 return !empty($errorCode) && $errorCode == $errno;
218 }
219
220 public function rowCount( $queryResult ) {
221 return $queryResult->rowCount();
222 }
223
224 private function prepareWp( $sql, $bind = array() ) {
225 global $wpdb;
226
227 $sql = str_replace( '%', '%%', $sql ); // eg when "value like 'done%'"
228
229 if ( is_array( $bind ) && empty( $bind ) ) {
230 return $sql;
231 }
232 if ( ! is_array( $bind ) ) {
233 $bind = array( $bind );
234 }
235
236 $null_placeholder = '_#__###NULL###_' . rand(1, PHP_INT_MAX) . ' __#_';
237 // random number not really needed but may prevent random issues that someone could somehow inject easily something
238
239 $has_replaced_null = false;
240
241 foreach ($bind as $index => $val) {
242 if (is_object($val) && method_exists($val, '__toString')) {
243 $bind[$index] = $val->__toString();
244 }
245 if (is_null($val)) {
246 $bind[$index] = $null_placeholder;
247 $has_replaced_null = true;
248 } elseif (is_string($val) && strpos($val, $null_placeholder) !== false) {
249 throw new \Exception('unexpected bind param'); // preventing random injections or something
250 }
251 }
252
253 $sql = str_replace( '?', '%s', $sql );
254
255 $query = $wpdb->prepare( $sql, $bind );
256
257 if ($has_replaced_null) {
258 $query = str_replace("'$null_placeholder'", 'NULL', $query);
259 }
260
261 return $query;
262 }
263
264 public function query( $sql, $bind = array() ) {
265 global $wpdb;
266
267 $test_sql = trim( $sql );
268 if ( strpos( $test_sql, '/*' ) === 0 ) {
269 // remove eg "/* trigger = CronArchive */"
270 $startPos = strpos( $test_sql, '*/' );
271 $test_sql = substr( $test_sql, $startPos + strlen( '*/' ) );
272 $test_sql = trim( $test_sql );
273 }
274
275 if ( preg_match( '/^\s*(select)\s/i', $test_sql ) ) {
276 // WordPress does not fetch any result when doing a select... it's only supposed to be used for things like
277 // insert / update / drop ...
278 $result = $this->fetchAll( $sql, $bind );
279 } else {
280 $prepare = $this->prepareWp( $sql, $bind );
281
282 $this->before_execute_query( $wpdb, $sql );
283
284 $result = $wpdb->query( $prepare );
285
286 $this->after_execute_query( $wpdb, $sql );
287 }
288
289 return new WordPressDbStatement( $this, $sql, $result );
290 }
291
292 public function exec( $sqlQuery ) {
293 global $wpdb;
294
295 $this->before_execute_query( $wpdb, $sqlQuery );
296
297 $exec = $wpdb->query( $sqlQuery );
298 $this->after_execute_query( $wpdb, $sqlQuery );
299
300 return $exec;
301 }
302
303 public function fetch( $query, $parameters = array() ) {
304 return $this->fetchRow( $query, $parameters );
305 }
306
307 public function fetchCol( $sql, $bind = array() ) {
308 global $wpdb;
309 $prepare = $this->prepareWp( $sql, $bind );
310
311 $this->before_execute_query( $wpdb, $sql );
312
313 $col = $wpdb->get_col( $prepare );
314
315 $this->after_execute_query( $wpdb, $sql );
316
317 return $col;
318 }
319
320 public function fetchAssoc( $sql, $bind = array() ) {
321 global $wpdb;
322 $prepare = $this->prepareWp( $sql, $bind );
323
324 $this->before_execute_query( $wpdb, $sql );
325
326 $assoc = $wpdb->get_results( $prepare, ARRAY_A );
327
328 $this->after_execute_query( $wpdb, $sql );
329
330 return $assoc;
331 }
332
333 /**
334 * @param \wpdb $wpdb
335 *
336 * @throws \Zend_Db_Statement_Exception
337 */
338 private function before_execute_query( $wpdb, $sql ) {
339 if ( ! $wpdb->suppress_errors ) {
340 if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) {
341 // allow users to always suppress or never suppress
342 if ( MATOMO_SUPPRESS_DB_ERRORS === true ) {
343 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
344 }
345
346 return;
347 }
348
349 if ( defined( 'WP_DEBUG' )
350 && WP_DEBUG
351 && defined( 'WP_DEBUG_DISPLAY' )
352 && WP_DEBUG_DISPLAY
353 && ! is_admin() ) {
354 // prevent showing some notices in frontend eg if cronjob runs there
355
356 $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
357 if ( ! $is_likely_dedicated_cron ) {
358 // if cron is triggered through wp-cron.php, then we should maybe not suppress!
359 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
360
361 return;
362 }
363 }
364
365 if ( ( stripos( $sql, '/* WP IGNORE ERROR */' ) !== false )
366 || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false
367 || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) {
368 // prevent notices for queries that are expected to fail
369 // SELECT 1 FROM wp_matomo_logtmpsegment1cc77bce7a13181081e44ea6ffc0a9fd LIMIT 1 => runs to detect if temp table exists or not and regularly the query fails which is expected
370 // SELECT @@TX_ISOLATION => not available in all mysql versions
371 // SELECT @@transaction_isolation => not available in all mysql versions
372 // we show notices only in admin...
373 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
374
375 return;
376 }
377 }
378 }
379
380 /**
381 * @param \wpdb $wpdb
382 *
383 * @throws \Zend_Db_Statement_Exception
384 */
385 private function after_execute_query( $wpdb, $sql ) {
386 $lastError = $wpdb->last_error;
387
388 if ( $lastError && !$this->getErrorNumberFromMessage($lastError) ) {
389 // see #174 mysqli message usually doesn't include the error code so we need to add it for isErrNo to work
390 // we want to execute this while errors are suppressed
391 $row = $wpdb->get_row('SHOW ERRORS', ARRAY_A);
392 if (!empty($row['Code'])) {
393 $lastError = '['.$row['Code'].'] ' . $lastError;
394 }
395 }
396
397 if ( isset( $this->old_suppress_errors_value ) ) {
398 $wpdb->suppress_errors( $this->old_suppress_errors_value );
399 $this->old_suppress_errors_value = null;
400 }
401
402 if ( $lastError ) {
403 $message = 'WP DB Error: ' . $lastError;
404 if ( $sql ) {
405 $message .= ' SQL: ' . $sql;
406 }
407 throw new \Zend_Db_Statement_Exception( $message );
408 }
409 }
410
411 public function fetchAll( $sql, $bind = array(), $fetchMode = null ) {
412 global $wpdb;
413 $prepare = $this->prepareWp( $sql, $bind );
414
415 $this->before_execute_query( $wpdb, $sql );
416
417 $results = $wpdb->get_results( $prepare, ARRAY_A );
418
419 $this->after_execute_query( $wpdb, $sql );
420
421 return $results;
422 }
423
424 public function fetchOne( $sql, $bind = array() ) {
425 global $wpdb;
426 $prepare = $this->prepareWp( $sql, $bind );
427
428 $this->before_execute_query( $wpdb, $sql );
429
430 $value = $wpdb->get_var( $prepare );
431
432 $this->after_execute_query( $wpdb, $sql );
433
434 if ( $value === null ) {
435 return false; // make sure to behave same way as matomo
436 }
437
438 return $value;
439 }
440
441 public function fetchRow( $sql, $bind = array(), $fetchMode = null ) {
442 global $wpdb;
443 $prepare = $this->prepareWp( $sql, $bind );
444
445 $this->before_execute_query( $wpdb, $sql );
446
447 $row = $wpdb->get_row( $prepare, ARRAY_A );
448
449 $this->after_execute_query( $wpdb, $sql );
450
451 return $row;
452 }
453
454 public function insert( $table, array $bind ) {
455 global $wpdb;
456
457 $this->before_execute_query( $wpdb, '' );
458
459 $insert = $wpdb->insert( $table, $bind );
460
461 $this->after_execute_query( $wpdb, '' );
462
463 return $insert;
464 }
465
466 public function update( $table, array $bind, $where = '' ) {
467 global $wpdb;
468
469 $fields = array();
470 foreach ( $bind as $field => $val ) {
471 $fields[] = "`$field` = %s";
472 }
473 $fields = implode( ', ', $fields );
474
475 $sql = "UPDATE `$table` SET $fields " . ( ( $where ) ? " WHERE $where" : '' );
476 $prepared = $wpdb->prepare( $sql, $bind );
477
478 $this->before_execute_query( $wpdb, '' );
479
480 $update = $wpdb->query( $prepared );
481
482 $this->after_execute_query( $wpdb, '' );
483
484 return $update;
485 }
486 }
487