PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.10.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.10.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 / classes / WpMatomo / Db / WordPress.php
matomo / classes / WpMatomo / Db Last commit date
Settings.php 4 years ago WordPress.php 1 year ago WordPressDbStatement.php 4 years ago WordPressTracker.php 4 years ago
WordPress.php
561 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 // make sure CREATE TABLE statements includ IF NOT EXISTS
228 if ( strpos( $sql, 'CREATE TABLE' ) !== false
229 && strpos( $sql, 'CREATE TABLE IF NOT EXISTS' ) === false
230 ) {
231 $sql = preg_replace( '/CREATE TABLE (?!IF NOT EXISTS)/', 'CREATE TABLE IF NOT EXISTS ', $sql );
232 }
233
234 $sql = str_replace( '%', '%%', $sql ); // eg when "value like 'done%'"
235
236 if ( is_array( $bind ) && empty( $bind ) ) {
237 return $sql;
238 }
239 if ( ! is_array( $bind ) ) {
240 $bind = array( $bind );
241 }
242
243 $null_placeholder = '_#__###NULL###_' . rand(1, PHP_INT_MAX) . ' __#_';
244 // random number not really needed but may prevent random issues that someone could somehow inject easily something
245
246 $has_replaced_null = false;
247
248 foreach ($bind as $index => $val) {
249 if (is_object($val) && method_exists($val, '__toString')) {
250 $bind[$index] = $val->__toString();
251 }
252 if (is_null($val)) {
253 $bind[$index] = $null_placeholder;
254 $has_replaced_null = true;
255 } elseif (is_string($val) && strpos($val, $null_placeholder) !== false) {
256 throw new \Exception('unexpected bind param'); // preventing random injections or something
257 }
258 }
259
260 $sql = $this->replace_placeholders( $sql );
261
262 $query = $wpdb->prepare( $sql, $bind );
263
264 if ($has_replaced_null) {
265 $query = str_replace("'$null_placeholder'", 'NULL', $query);
266 }
267
268 return $query;
269 }
270
271 public function query( $sql, $bind = array() ) {
272 global $wpdb;
273
274 $test_sql = trim( $sql );
275 if ( strpos( $test_sql, '/*' ) === 0 ) {
276 // remove eg "/* trigger = CronArchive */"
277 $startPos = strpos( $test_sql, '*/' );
278 $test_sql = substr( $test_sql, $startPos + strlen( '*/' ) );
279 $test_sql = trim( $test_sql );
280 }
281
282 if (
283 preg_match( '/^\s*(select)\s/i', $test_sql )
284 || preg_match( '/\sfor select\s/i', $test_sql )
285 ) {
286 // WordPress does not fetch any result when doing a query w/ a select... it's only supposed to be used for things like
287 // insert / update / drop ...
288 $result = $this->fetchAll( $sql, $bind );
289 } else {
290 $prepare = $this->prepareWp( $sql, $bind );
291
292 $this->before_execute_query( $wpdb, $sql );
293
294 $result = $wpdb->query( $prepare );
295
296 $this->after_execute_query( $wpdb, $sql );
297 }
298
299 return new WordPressDbStatement( $this, $sql, $result );
300 }
301
302 public function exec( $sqlQuery ) {
303 global $wpdb;
304
305 $this->before_execute_query( $wpdb, $sqlQuery );
306
307 $exec = $wpdb->query( $sqlQuery );
308 $this->after_execute_query( $wpdb, $sqlQuery );
309
310 return $exec;
311 }
312
313 public function fetch( $query, $parameters = array() ) {
314 return $this->fetchRow( $query, $parameters );
315 }
316
317 public function fetchCol( $sql, $bind = array() ) {
318 global $wpdb;
319 $prepare = $this->prepareWp( $sql, $bind );
320
321 $this->before_execute_query( $wpdb, $sql );
322
323 $col = $wpdb->get_col( $prepare );
324
325 $this->after_execute_query( $wpdb, $sql );
326
327 return $col;
328 }
329
330 public function fetchAssoc( $sql, $bind = array() ) {
331 global $wpdb;
332 $prepare = $this->prepareWp( $sql, $bind );
333
334 $this->before_execute_query( $wpdb, $sql );
335
336 $assoc = $wpdb->get_results( $prepare, ARRAY_A );
337
338 $this->after_execute_query( $wpdb, $sql );
339
340 return $assoc;
341 }
342
343 /**
344 * @param \wpdb $wpdb
345 *
346 * @throws \Zend_Db_Statement_Exception
347 */
348 private function before_execute_query( $wpdb, $sql ) {
349 if ( ! $wpdb->suppress_errors ) {
350 if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) {
351 // allow users to always suppress or never suppress
352 if ( MATOMO_SUPPRESS_DB_ERRORS === true ) {
353 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
354 }
355
356 return;
357 }
358
359 if ( defined( 'WP_DEBUG' )
360 && WP_DEBUG
361 && defined( 'WP_DEBUG_DISPLAY' )
362 && WP_DEBUG_DISPLAY
363 && ! is_admin() ) {
364 // prevent showing some notices in frontend eg if cronjob runs there
365
366 $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
367 if ( ! $is_likely_dedicated_cron ) {
368 // if cron is triggered through wp-cron.php, then we should maybe not suppress!
369 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
370
371 return;
372 }
373 }
374
375 if ( ( stripos( $sql, '/* WP IGNORE ERROR */' ) !== false )
376 || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false
377 || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) {
378 // prevent notices for queries that are expected to fail
379 // 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
380 // SELECT @@TX_ISOLATION => not available in all mysql versions
381 // SELECT @@transaction_isolation => not available in all mysql versions
382 // we show notices only in admin...
383 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
384
385 return;
386 }
387 }
388 }
389
390 /**
391 * @param \wpdb $wpdb
392 *
393 * @throws \Zend_Db_Statement_Exception
394 */
395 private function after_execute_query( $wpdb, $sql ) {
396 $lastError = $wpdb->last_error;
397
398 if ( $lastError && !$this->getErrorNumberFromMessage($lastError) ) {
399 // see #174 mysqli message usually doesn't include the error code so we need to add it for isErrNo to work
400 // we want to execute this while errors are suppressed
401 $row = $wpdb->get_row('SHOW ERRORS', ARRAY_A);
402 if (!empty($row['Code'])) {
403 $lastError = '['.$row['Code'].'] ' . $lastError;
404 }
405 }
406
407 if ( isset( $this->old_suppress_errors_value ) ) {
408 $wpdb->suppress_errors( $this->old_suppress_errors_value );
409 $this->old_suppress_errors_value = null;
410 }
411
412 if ( $lastError ) {
413 $message = 'WP DB Error: ' . $lastError;
414 if ( $sql ) {
415 $message .= ' SQL: ' . $sql;
416 }
417 throw new \Zend_Db_Statement_Exception( $message );
418 }
419 }
420
421 public function fetchAll( $sql, $bind = array(), $fetchMode = null ) {
422 global $wpdb;
423 $prepare = $this->prepareWp( $sql, $bind );
424
425 $this->before_execute_query( $wpdb, $sql );
426
427 $results = $wpdb->get_results( $prepare, ARRAY_A );
428
429 $this->after_execute_query( $wpdb, $sql );
430
431 return $results;
432 }
433
434 public function fetchOne( $sql, $bind = array() ) {
435 global $wpdb;
436 $prepare = $this->prepareWp( $sql, $bind );
437
438 $this->before_execute_query( $wpdb, $sql );
439
440 $value = $wpdb->get_var( $prepare );
441
442 $this->after_execute_query( $wpdb, $sql );
443
444 if ( $value === null ) {
445 return false; // make sure to behave same way as matomo
446 }
447
448 return $value;
449 }
450
451 public function fetchRow( $sql, $bind = array(), $fetchMode = null ) {
452 global $wpdb;
453 $prepare = $this->prepareWp( $sql, $bind );
454
455 $this->before_execute_query( $wpdb, $sql );
456
457 $row = $wpdb->get_row( $prepare, ARRAY_A );
458
459 $this->after_execute_query( $wpdb, $sql );
460
461 return $row;
462 }
463
464 public function insert( $table, array $bind ) {
465 global $wpdb;
466
467 $this->before_execute_query( $wpdb, '' );
468
469 $insert = $wpdb->insert( $table, $bind );
470
471 $this->after_execute_query( $wpdb, '' );
472
473 return $insert;
474 }
475
476 public function update( $table, array $bind, $where = '' ) {
477 global $wpdb;
478
479 $fields = array();
480 foreach ( $bind as $field => $val ) {
481 // wpdb's prepare doesn't seem to handle null values correctly. they are set to `''`
482 // in some cases, so we handle this explicitly here.
483 if ($val === null) {
484 unset($bind[$field]);
485 $fields[] = "`$field` = NULL";
486 } else {
487 $fields[] = "`$field` = %s";
488 }
489 }
490 $fields = implode( ', ', $fields );
491
492 $sql = "UPDATE `$table` SET $fields " . ( ( $where ) ? " WHERE $where" : '' );
493
494 if ( empty( $bind ) ) {
495 $prepared = $sql;
496 } else {
497 $prepared = $wpdb->prepare( $sql, $bind );
498 }
499
500 $this->before_execute_query( $wpdb, '' );
501
502 $update = $wpdb->query( $prepared );
503
504 $this->after_execute_query( $wpdb, '' );
505
506 return $update;
507 }
508
509 // public for tests
510 public function replace_placeholders( $sql ) {
511 $replaced = '';
512
513 $i = 0;
514 while ( $i < strlen( $sql ) ) {
515 if ( $this->is_string_literal_start( $sql[$i] ) ) {
516 $quote = $sql[$i];
517
518 $segment_end = $i + 1;
519 while ( $segment_end < strlen( $sql ) ) {
520 if ( $sql[ $segment_end ] === $quote ) {
521 // '' or ""
522 $is_double_quote = $segment_end + 1 < strlen( $sql )
523 && $sql[ $segment_end + 1 ] === $quote;
524
525 if ( $is_double_quote ) {
526 ++$segment_end;
527 } else {
528 break; // not double quote, end of string literal
529 }
530 }
531
532 ++$segment_end;
533 }
534
535 ++$segment_end; // advance past end quote
536
537 $replaced .= substr( $sql, $i, $segment_end - $i );
538 } else {
539 // advance until string literal or end of string
540 $segment_end = $i + 1;
541 while ( $segment_end < strlen( $sql ) && ! $this->is_string_literal_start( $sql[$segment_end] ) ) {
542 ++$segment_end;
543 }
544
545 $segment = substr( $sql, $i, $segment_end - $i );
546 $segment = str_replace( '?', '%s', $segment );
547
548 $replaced .= $segment;
549 }
550
551 $i = $segment_end;
552 }
553
554 return $replaced;
555 }
556
557 private function is_string_literal_start( $s ) {
558 return $s === '\'' || $s === '"';
559 }
560 }
561