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 / classes / WpMatomo / Db / WordPress.php
matomo / classes / WpMatomo / Db Last commit date
Settings.php 6 years ago WordPress.php 6 years ago WordPressDbStatement.php 6 years ago WordPressTracker.php 6 years ago
WordPress.php
484 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_null($val)) {
243 $bind[$index] = $null_placeholder;
244 $has_replaced_null = true;
245 } elseif (is_string($val) && strpos($val, $null_placeholder) !== false) {
246 throw new \Exception('unexpected bind param'); // preventing random injections or something
247 }
248 }
249
250 $sql = str_replace( '?', '%s', $sql );
251
252 $query = $wpdb->prepare( $sql, $bind );
253
254 if ($has_replaced_null) {
255 $query = str_replace("'$null_placeholder'", 'NULL', $query);
256 }
257
258 return $query;
259 }
260
261 public function query( $sql, $bind = array() ) {
262 global $wpdb;
263
264 $test_sql = trim( $sql );
265 if ( strpos( $test_sql, '/*' ) === 0 ) {
266 // remove eg "/* trigger = CronArchive */"
267 $startPos = strpos( $test_sql, '*/' );
268 $test_sql = substr( $test_sql, $startPos + strlen( '*/' ) );
269 $test_sql = trim( $test_sql );
270 }
271
272 if ( preg_match( '/^\s*(select)\s/i', $test_sql ) ) {
273 // WordPress does not fetch any result when doing a select... it's only supposed to be used for things like
274 // insert / update / drop ...
275 $result = $this->fetchAll( $sql, $bind );
276 } else {
277 $prepare = $this->prepareWp( $sql, $bind );
278
279 $this->before_execute_query( $wpdb, $sql );
280
281 $result = $wpdb->query( $prepare );
282
283 $this->after_execute_query( $wpdb, $sql );
284 }
285
286 return new WordPressDbStatement( $this, $sql, $result );
287 }
288
289 public function exec( $sqlQuery ) {
290 global $wpdb;
291
292 $this->before_execute_query( $wpdb, $sqlQuery );
293
294 $exec = $wpdb->query( $sqlQuery );
295 $this->after_execute_query( $wpdb, $sqlQuery );
296
297 return $exec;
298 }
299
300 public function fetch( $query, $parameters = array() ) {
301 return $this->fetchRow( $query, $parameters );
302 }
303
304 public function fetchCol( $sql, $bind = array() ) {
305 global $wpdb;
306 $prepare = $this->prepareWp( $sql, $bind );
307
308 $this->before_execute_query( $wpdb, $sql );
309
310 $col = $wpdb->get_col( $prepare );
311
312 $this->after_execute_query( $wpdb, $sql );
313
314 return $col;
315 }
316
317 public function fetchAssoc( $sql, $bind = array() ) {
318 global $wpdb;
319 $prepare = $this->prepareWp( $sql, $bind );
320
321 $this->before_execute_query( $wpdb, $sql );
322
323 $assoc = $wpdb->get_results( $prepare, ARRAY_A );
324
325 $this->after_execute_query( $wpdb, $sql );
326
327 return $assoc;
328 }
329
330 /**
331 * @param \wpdb $wpdb
332 *
333 * @throws \Zend_Db_Statement_Exception
334 */
335 private function before_execute_query( $wpdb, $sql ) {
336 if ( ! $wpdb->suppress_errors ) {
337 if ( defined( 'MATOMO_SUPPRESS_DB_ERRORS' ) ) {
338 // allow users to always suppress or never suppress
339 if ( MATOMO_SUPPRESS_DB_ERRORS === true ) {
340 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
341 }
342
343 return;
344 }
345
346 if ( defined( 'WP_DEBUG' )
347 && WP_DEBUG
348 && defined( 'WP_DEBUG_DISPLAY' )
349 && WP_DEBUG_DISPLAY
350 && ! is_admin() ) {
351 // prevent showing some notices in frontend eg if cronjob runs there
352
353 $is_likely_dedicated_cron = defined( 'DOING_CRON' ) && DOING_CRON && defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
354 if ( ! $is_likely_dedicated_cron ) {
355 // if cron is triggered through wp-cron.php, then we should maybe not suppress!
356 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
357
358 return;
359 }
360 }
361
362 if ( ( stripos( $sql, 'SELECT 1 FROM' ) !== false && stripos( $sql, 'matomo_logtmpsegment' ) !== false )
363 || stripos( $sql, 'SELECT @@TX_ISOLATION' ) !== false
364 || stripos( $sql, 'SELECT @@transaction_isolation' ) !== false ) {
365 // prevent notices for queries that are expected to fail
366 // 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
367 // SELECT @@TX_ISOLATION => not available in all mysql versions
368 // SELECT @@transaction_isolation => not available in all mysql versions
369 // we show notices only in admin...
370 $this->old_suppress_errors_value = $wpdb->suppress_errors( true );
371
372 return;
373 }
374 }
375 }
376
377 /**
378 * @param \wpdb $wpdb
379 *
380 * @throws \Zend_Db_Statement_Exception
381 */
382 private function after_execute_query( $wpdb, $sql ) {
383 $lastError = $wpdb->last_error;
384
385 if ( $lastError && !$this->getErrorNumberFromMessage($lastError) ) {
386 // see #174 mysqli message usually doesn't include the error code so we need to add it for isErrNo to work
387 // we want to execute this while errors are suppressed
388 $row = $wpdb->get_row('SHOW ERRORS', ARRAY_A);
389 if (!empty($row['Code'])) {
390 $lastError = '['.$row['Code'].'] ' . $lastError;
391 }
392 }
393
394 if ( isset( $this->old_suppress_errors_value ) ) {
395 $wpdb->suppress_errors( $this->old_suppress_errors_value );
396 $this->old_suppress_errors_value = null;
397 }
398
399 if ( $lastError ) {
400 $message = 'WP DB Error: ' . $lastError;
401 if ( $sql ) {
402 $message .= ' SQL: ' . $sql;
403 }
404 throw new \Zend_Db_Statement_Exception( $message );
405 }
406 }
407
408 public function fetchAll( $sql, $bind = array(), $fetchMode = null ) {
409 global $wpdb;
410 $prepare = $this->prepareWp( $sql, $bind );
411
412 $this->before_execute_query( $wpdb, $sql );
413
414 $results = $wpdb->get_results( $prepare, ARRAY_A );
415
416 $this->after_execute_query( $wpdb, $sql );
417
418 return $results;
419 }
420
421 public function fetchOne( $sql, $bind = array() ) {
422 global $wpdb;
423 $prepare = $this->prepareWp( $sql, $bind );
424
425 $this->before_execute_query( $wpdb, $sql );
426
427 $value = $wpdb->get_var( $prepare );
428
429 $this->after_execute_query( $wpdb, $sql );
430
431 if ( $value === null ) {
432 return false; // make sure to behave same way as matomo
433 }
434
435 return $value;
436 }
437
438 public function fetchRow( $sql, $bind = array(), $fetchMode = null ) {
439 global $wpdb;
440 $prepare = $this->prepareWp( $sql, $bind );
441
442 $this->before_execute_query( $wpdb, $sql );
443
444 $row = $wpdb->get_row( $prepare, ARRAY_A );
445
446 $this->after_execute_query( $wpdb, $sql );
447
448 return $row;
449 }
450
451 public function insert( $table, array $bind ) {
452 global $wpdb;
453
454 $this->before_execute_query( $wpdb, '' );
455
456 $insert = $wpdb->insert( $table, $bind );
457
458 $this->after_execute_query( $wpdb, '' );
459
460 return $insert;
461 }
462
463 public function update( $table, array $bind, $where = '' ) {
464 global $wpdb;
465
466 $fields = array();
467 foreach ( $bind as $field => $val ) {
468 $fields[] = "`$field` = %s";
469 }
470 $fields = implode( ', ', $fields );
471
472 $sql = "UPDATE `$table` SET $fields " . ( ( $where ) ? " WHERE $where" : '' );
473 $prepared = $wpdb->prepare( $sql, $bind );
474
475 $this->before_execute_query( $wpdb, '' );
476
477 $update = $wpdb->query( $prepared );
478
479 $this->after_execute_query( $wpdb, '' );
480
481 return $update;
482 }
483 }
484