PluginProbe
SQLite Database Integration / 3.1.0
SQLite Database Integration v3.1.0
3.1.0 3.0.2 3.0.1 trunk 2.1.13 2.1.14 2.1.15 2.1.16 2.2.0 2.2.1 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.2.2 2.2.20 2.2.21 2.2.22 2.2.23 All 33 releases
sqlite-database-integration / wp-includes / sqlite / class-wp-sqlite-db.php

class-wp-sqlite-db.php in SQLite Database Integration 3.1.0, at wp-includes/sqlite/class-wp-sqlite-db.php

852 lines 24.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Extend and replace the wpdb class.
4 */
5
6 /**
7 * This class extends wpdb and replaces it.
8 *
9 * It also rewrites some methods that use mysql specific functions.
10 */
11 class WP_SQLite_DB extends wpdb {
12
13 /**
14 * Database Handle
15 *
16 * @var WP_MySQL_On_SQLite
17 */
18 protected $dbh;
19
20 /**
21 * Backward compatibility, see wpdb::$allow_unsafe_unquoted_parameters.
22 *
23 * This property is mirroring "wpdb::$allow_unsafe_unquoted_parameters",
24 * because some tests are accessing it externally using PHP reflection.
25 *
26 * @var
27 */
28 private $allow_unsafe_unquoted_parameters = true;
29
30 /**
31 * Connects to the SQLite database.
32 *
33 * Unlike for MySQL, no credentials and host are needed.
34 *
35 * @param string $dbname Database name.
36 */
37 public function __construct( $dbname ) {
38 /**
39 * We need to initialize the "$wpdb" global early, so that the SQLite
40 * driver can configure the database. The call stack goes like this:
41 *
42 * 1. The "parent::__construct()" call executes "$this->db_connect()".
43 * 2. The database connection call initializes the SQLite driver.
44 * 3. The SQLite driver initializes and runs "WP_SQLite_Configurator".
45 * 4. The configurator uses "WP_SQLite_Information_Schema_Reconstructor",
46 * which requires "wp-admin/includes/schema.php" when in WordPress.
47 * 5. The "wp-admin/includes/schema.php" requires the "$wpdb" global,
48 * which creates a circular dependency.
49 */
50 $GLOBALS['wpdb'] = $this;
51
52 // The SQLite driver may use the charset and collation while connecting.
53 $this->init_charset();
54
55 parent::__construct( '', '', $dbname, '' );
56 }
57
58 /**
59 * Returns the active MySQL-on-SQLite driver.
60 *
61 * @return WP_MySQL_On_SQLite The active driver.
62 * @throws RuntimeException When there is no active database connection.
63 */
64 public function get_driver(): WP_MySQL_On_SQLite {
65 if ( ! $this->dbh ) {
66 throw new RuntimeException( 'Cannot access the driver without an active database connection.' );
67 }
68
69 return $this->dbh;
70 }
71
72 /**
73 * Method to set character set for the database.
74 *
75 * This overrides wpdb::set_charset(), only to dummy out the MySQL function.
76 *
77 * @see wpdb::set_charset()
78 *
79 * @param resource $dbh The resource given by mysql_connect.
80 * @param string $charset Optional. The character set. Default null.
81 * @param string $collate Optional. The collation. Default null.
82 */
83 public function set_charset( $dbh, $charset = null, $collate = null ) {
84 }
85
86 /**
87 * Retrieves the character set for the given column.
88 *
89 * This overrides wpdb::get_col_charset() to enable the parent's implementation
90 * for SQLite by temporarily setting the is_mysql flag.
91 *
92 * @see wpdb::get_col_charset()
93 *
94 * @param string $table Table name.
95 * @param string $column Column name.
96 * @return string|false|WP_Error Column character set as a string. False if the column has
97 * no character set. WP_Error object on failure.
98 */
99 public function get_col_charset( $table, $column ) {
100 $original_is_mysql = $this->is_mysql ?? null;
101
102 /*
103 * The parent method returns early when `$this->is_mysql` is falsy.
104 * Since SQLite doesn't set this flag, we enable it temporarily so
105 * the parent can run its full logic — querying column metadata via
106 * SHOW FULL COLUMNS (which the SQLite driver translates) and
107 * populating the `$this->col_meta` cache.
108 */
109 try {
110 $this->is_mysql = true;
111 return parent::get_col_charset( $table, $column );
112 } finally {
113 $this->is_mysql = $original_is_mysql;
114 }
115 }
116
117 /**
118 * Retrieves the maximum string length allowed in a given column.
119 *
120 * This overrides wpdb::get_col_length() to enable the parent's implementation
121 * for SQLite by temporarily setting the is_mysql flag.
122 *
123 * @see wpdb::get_col_length()
124 *
125 * @param string $table Table name.
126 * @param string $column Column name.
127 * @return array|false|WP_Error Column length information, false if the column has
128 * no length. WP_Error object on failure.
129 */
130 public function get_col_length( $table, $column ) {
131 $original_is_mysql = $this->is_mysql ?? null;
132
133 // See get_col_charset() for an explanation of the is_mysql flag.
134 try {
135 $this->is_mysql = true;
136 return parent::get_col_length( $table, $column );
137 } finally {
138 $this->is_mysql = $original_is_mysql;
139 }
140 }
141
142 /**
143 * Changes the current SQL mode, and ensures its WordPress compatibility.
144 *
145 * If no modes are passed, it will ensure the current MySQL server modes are compatible.
146 *
147 * This overrides wpdb::set_sql_mode() while closely mirroring its implementation.
148 *
149 * @param array $modes Optional. A list of SQL modes to set. Default empty array.
150 */
151 public function set_sql_mode( $modes = array() ) {
152 if ( empty( $modes ) ) {
153 $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' )->fetchAll( PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
154 if ( ! isset( $result[0] ) ) {
155 return;
156 }
157
158 $modes_str = $result[0]->{'@@SESSION.sql_mode'};
159 if ( empty( $modes_str ) ) {
160 return;
161 }
162 $modes = explode( ',', $modes_str );
163 }
164
165 $modes = array_change_key_case( $modes, CASE_UPPER );
166
167 /**
168 * Filters the list of incompatible SQL modes to exclude.
169 *
170 * @since 3.9.0
171 *
172 * @param array $incompatible_modes An array of incompatible modes.
173 */
174 $incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
175
176 foreach ( $modes as $i => $mode ) {
177 if ( in_array( $mode, $incompatible_modes, true ) ) {
178 unset( $modes[ $i ] );
179 }
180 }
181 $modes_str = implode( ',', $modes );
182
183 $this->dbh->query( "SET SESSION sql_mode='$modes_str'" );
184 }
185
186 /**
187 * Closes the current database connection.
188 *
189 * This overrides wpdb::close() while closely mirroring its implementation.
190 *
191 * @see wpdb::close()
192 *
193 * @return bool True if the connection was successfully closed,
194 * false if it wasn't, or if the connection doesn't exist.
195 */
196 public function close() {
197 if ( ! $this->dbh ) {
198 return false;
199 }
200
201 $connection = $this->dbh->get_connection();
202 $pdo = $connection->get_pdo();
203
204 try {
205 if ( $this->dbh->inTransaction() ) {
206 $this->dbh->rollBack();
207 } elseif ( $pdo->inTransaction() ) {
208 $pdo->rollBack();
209 } else {
210 /*
211 * On PHP < 8.4, PDO cannot detect transactions started via SQL.
212 * A savepoint ensures ROLLBACK succeeds with or without one.
213 */
214 $pdo->exec( 'SAVEPOINT wp_sqlite_db_close' );
215 $pdo->exec( 'ROLLBACK' );
216 }
217 } catch ( Throwable $e ) {
218 return false;
219 }
220
221 if (
222 isset( $GLOBALS['@pdo'] )
223 && $GLOBALS['@pdo'] === $pdo
224 ) {
225 unset( $GLOBALS['@pdo'] );
226 }
227
228 $connection->set_query_logger( null );
229 $this->result = null;
230 $this->dbh = null;
231 $this->ready = false;
232 $this->has_connected = false;
233
234 return true;
235 }
236
237 /**
238 * Sets $this->charset and $this->collate.
239 *
240 * This overrides wpdb::init_charset(). SQLite stores all text as UTF-8, and
241 * the emulated MySQL connection always uses utf8mb4 (see set_charset()), so
242 * the charset is always utf8mb4, and only a UTF-8 DB_COLLATE applies.
243 *
244 * Unlike in wpdb, the result doesn't depend on the database connection.
245 * This is important, as the SQLite driver may use the charset and collation
246 * while connecting (see the constructor).
247 *
248 * @see wpdb::init_charset()
249 */
250 public function init_charset() {
251 $collate = defined( 'DB_COLLATE' ) ? strtolower( (string) DB_COLLATE ) : '';
252
253 // MySQL 8.0.30 and newer name the utf8 collations "utf8mb3_*".
254 $collate = preg_replace( '/^utf8mb3_/', 'utf8_', $collate );
255
256 // Collations of other charsets don't apply to the utf8mb4 connection.
257 if ( ! preg_match( '/^utf8(mb4)?_/', $collate ) ) {
258 $collate = '';
259 }
260
261 $charset_collate = $this->resolve_charset( 'utf8mb4', $collate );
262
263 $this->charset = $charset_collate['charset'];
264 $this->collate = $charset_collate['collate'];
265 }
266
267 /**
268 * Determines the best charset and collation to use given a charset and collation.
269 *
270 * For example, when able, utf8mb4 should be used instead of utf8.
271 *
272 * This overrides wpdb::determine_charset() while closely mirroring its implementation.
273 * The override is needed because the parent checks for a mysqli connection object.
274 *
275 * @param string $charset The character set to check.
276 * @param string $collate The collation to check.
277 * @return array {
278 * The most appropriate character set and collation to use.
279 *
280 * @type string $charset Character set.
281 * @type string $collate Collation.
282 * }
283 */
284 public function determine_charset( $charset, $collate ) {
285 if ( ! $this->dbh ) {
286 return compact( 'charset', 'collate' );
287 }
288
289 return $this->resolve_charset( $charset, $collate );
290 }
291
292 /**
293 * Method to select the database connection.
294 *
295 * This overrides wpdb::select(), only to dummy out the MySQL function.
296 *
297 * @see wpdb::select()
298 *
299 * @param string $db MySQL database name. Not used.
300 * @param resource|null $dbh Optional link identifier.
301 */
302 public function select( $db, $dbh = null ) {
303 $this->ready = true;
304 }
305
306 /**
307 * Method to escape characters.
308 *
309 * This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string().
310 *
311 * @see wpdb::_real_escape()
312 *
313 * @param string $data The string to escape.
314 *
315 * @return string escaped
316 * @throws RuntimeException When the database connection is not initialized.
317 */
318 public function _real_escape( $data ) {
319 if ( ! is_scalar( $data ) ) {
320 return '';
321 }
322
323 if ( ! $this->dbh ) {
324 throw new RuntimeException( 'Cannot escape data without an active database connection.' );
325 }
326
327 // Escape the string without bounding quotes to mirror mysqli_real_escape_string().
328 $quoted = $this->dbh->quote( (string) $data );
329 $escaped = substr( $quoted, 1, -1 );
330 return $this->add_placeholder_escape( $escaped );
331 }
332
333 /**
334 * Prints SQL/DB error.
335 *
336 * This overrides wpdb::print_error() while closely mirroring its implementation.
337 *
338 * @global array $EZSQL_ERROR Stores error information of query and error string.
339 *
340 * @param string $str The error to display.
341 * @return void|false Void if the showing of errors is enabled, false if disabled.
342 */
343 public function print_error( $str = '' ) {
344 global $EZSQL_ERROR;
345
346 if ( ! $str ) {
347 $str = $this->last_error;
348 }
349
350 $EZSQL_ERROR[] = array(
351 'query' => $this->last_query,
352 'error_str' => $str,
353 );
354
355 if ( $this->suppress_errors ) {
356 return false;
357 }
358
359 $caller = $this->get_caller();
360 if ( $caller ) {
361 // Not translated, as this will only appear in the error log.
362 $error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller );
363 } else {
364 $error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query );
365 }
366
367 error_log( $error_str );
368
369 // Are we showing errors?
370 if ( ! $this->show_errors ) {
371 return false;
372 }
373
374 wp_load_translations_early();
375
376 // If there is an error then take note of it.
377 if ( is_multisite() ) {
378 $msg = sprintf(
379 "%s [%s]\n%s\n",
380 __( 'WordPress database error:' ),
381 $str,
382 $this->last_query
383 );
384
385 if ( defined( 'ERRORLOGFILE' ) ) {
386 error_log( $msg, 3, ERRORLOGFILE );
387 }
388 if ( defined( 'DIEONDBERROR' ) ) {
389 wp_die( $msg );
390 }
391 } else {
392 $str = htmlspecialchars( $str, ENT_QUOTES );
393 $query = htmlspecialchars( $this->last_query, ENT_QUOTES );
394
395 printf(
396 '<div id="error"><p class="wpdberror"><strong>%s</strong> [%s]<br /><code>%s</code></p></div>',
397 __( 'WordPress database error:' ),
398 $str,
399 $query
400 );
401 }
402 }
403
404 /**
405 * Method to flush cached data.
406 *
407 * This overrides wpdb::flush(). This is not necessarily overridden, because
408 * $result will never be resource.
409 *
410 * @see wpdb::flush
411 */
412 public function flush() {
413 $this->last_result = array();
414 $this->col_info = null;
415 $this->last_query = null;
416 $this->rows_affected = 0;
417 $this->num_rows = 0;
418 $this->last_error = '';
419 $this->result = null;
420 }
421
422 /**
423 * Method to do the database connection.
424 *
425 * This overrides wpdb::db_connect() to avoid using MySQL function.
426 *
427 * @see wpdb::db_connect()
428 *
429 * @param bool $allow_bail Not used.
430 * @return bool True on a successful connection, false on failure.
431 */
432 public function db_connect( $allow_bail = true ) {
433 if ( $this->dbh ) {
434 return $this->ready;
435 }
436
437 $this->last_error = '';
438 if ( isset( $GLOBALS['@pdo'] ) ) {
439 trigger_error(
440 'PDO injection via $GLOBALS[\'@pdo\'] is no longer supported. The existing PDO will be ignored and a new connection will be created.',
441 E_USER_WARNING
442 );
443 }
444
445 if ( null === $this->dbname || '' === $this->dbname ) {
446 $this->bail(
447 'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
448 'db_connect_fail'
449 );
450 return false;
451 }
452
453 try {
454 $options = array(
455 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
456 );
457 $dbh = new WP_MySQL_On_SQLite(
458 sprintf(
459 'mysql-on-sqlite:path=%s;dbname=%s',
460 str_replace( ';', ';;', DB_PATH ),
461 str_replace( ';', ';;', $this->dbname )
462 ),
463 null,
464 null,
465 $options
466 );
467 $dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
468 $this->dbh = $dbh;
469
470 /**
471 * Exposes the underlying PDO SQLite connection for backward compatibility.
472 *
473 * @deprecated 3.0.0 Use WP_SQLite_DB::get_driver() with
474 * WP_MySQL_On_SQLite::get_sqlite_pdo() instead.
475 */
476 $GLOBALS['@pdo'] = $dbh->get_sqlite_pdo();
477 } catch ( Throwable $e ) {
478 $this->last_error = $this->format_error_message( $e );
479 }
480 if ( $this->last_error ) {
481 return false;
482 }
483
484 $this->has_connected = true;
485 $this->set_charset( $this->dbh );
486
487 $this->ready = true;
488 $this->set_sql_mode();
489 return true;
490 }
491
492 /**
493 * Checks that the database connection is available.
494 *
495 * @param bool $allow_bail Not used.
496 *
497 * @return bool True when the connection is available, false otherwise.
498 */
499 public function check_connection( $allow_bail = true ) {
500 if ( $this->dbh ) {
501 return true;
502 }
503
504 return $this->db_connect( $allow_bail );
505 }
506
507 /**
508 * Prepares a SQL query for safe execution.
509 *
510 * See "wpdb::prepare()". This override only fixes a WPDB test issue.
511 *
512 * @param string $query Query statement with `sprintf()`-like placeholders.
513 * @param array|mixed $args The array of variables or the first variable to substitute.
514 * @param mixed ...$args Further variables to substitute when using individual arguments.
515 * @return string|void Sanitized query string, if there is a query to prepare.
516 */
517 public function prepare( $query, ...$args ) {
518 /*
519 * Sync "$allow_unsafe_unquoted_parameters" with the WPDB parent property.
520 * This is only needed because some WPDB tests are accessing the private
521 * property externally via PHP reflection. This should be fixed WP tests.
522 */
523 $wpdb_allow_unsafe_unquoted_parameters = $this->__get( 'allow_unsafe_unquoted_parameters' );
524 if ( $wpdb_allow_unsafe_unquoted_parameters !== $this->allow_unsafe_unquoted_parameters ) {
525 $property = new ReflectionProperty( 'wpdb', 'allow_unsafe_unquoted_parameters' );
526 $property->setAccessible( true );
527 $property->setValue( $this, $this->allow_unsafe_unquoted_parameters );
528 $property->setAccessible( false );
529 }
530
531 return parent::prepare( $query, ...$args );
532 }
533
534 /**
535 * Performs a database query.
536 *
537 * This overrides wpdb::query() while closely mirroring its implementation.
538 *
539 * @see wpdb::query()
540 *
541 * @param string $query Database query.
542 *
543 * @param string $query Database query.
544 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
545 * affected/selected for all other queries. Boolean false on error.
546 */
547 public function query( $query ) {
548 // Query Monitor integration:
549 $query_monitor_active = defined( 'SQLITE_QUERY_MONITOR_LOADED' ) && SQLITE_QUERY_MONITOR_LOADED;
550 if ( $query_monitor_active && $this->show_errors ) {
551 $this->hide_errors();
552 }
553
554 if ( ! $this->ready ) {
555 $this->check_current_query = true;
556 return false;
557 }
558
559 $query = apply_filters( 'query', $query );
560
561 if ( ! $query ) {
562 $this->insert_id = 0;
563 return false;
564 }
565
566 $this->flush();
567
568 // Log how the function was called.
569 $this->func_call = "\$db->query(\"$query\")";
570
571 /*
572 * Mirror wpdb's query text validation.
573 * TODO: Add full charset enforcement to MySQL on SQLite, where column
574 * types and SQL mode are known, so all callers are protected.
575 */
576 if ( $this->check_current_query && ! $this->check_ascii( $query ) ) {
577 $stripped_query = $this->strip_invalid_text_from_query( $query );
578 // Charset discovery can run queries, so clear their results.
579 $this->flush();
580 if ( $stripped_query !== $query ) {
581 $this->insert_id = 0;
582 $this->last_query = $query;
583 wp_load_translations_early();
584 $this->last_error = __( 'WordPress database error: Could not perform query because it contains invalid data.' );
585 return false;
586 }
587 }
588 $this->check_current_query = true;
589
590 // Keep track of the last query for debug.
591 $this->last_query = $query;
592
593 // Save the query count after any charset discovery queries.
594 $last_query_count = count( $this->queries ?? array() );
595 $this->_do_query( $query );
596
597 if ( $this->last_error ) {
598 // Clear insert_id on a subsequent failed insert.
599 if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
600 $this->insert_id = 0;
601 }
602
603 $this->print_error();
604 return false;
605 }
606
607 if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
608 $return_val = true;
609 } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
610 $this->rows_affected = $this->result->rowCount();
611
612 // Take note of the insert_id.
613 if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
614 $this->insert_id = (int) $this->dbh->lastInsertId();
615 }
616
617 // Return number of rows affected.
618 $return_val = $this->rows_affected;
619 } else {
620 $num_rows = 0;
621
622 if ( $this->result->columnCount() > 0 ) {
623 $this->last_result = $this->result->fetchAll();
624 $num_rows = count( $this->last_result );
625 }
626
627 // Log and return the number of rows selected.
628 $this->num_rows = $num_rows;
629 $return_val = $num_rows;
630 }
631
632 // Query monitor integration:
633 if ( $query_monitor_active && class_exists( 'QM_Backtrace' ) ) {
634 if ( did_action( 'qm/cease' ) ) {
635 $this->queries = array();
636 }
637
638 $i = $last_query_count;
639 if ( ! isset( $this->queries[ $i ] ) ) {
640 return $return_val;
641 }
642
643 $this->queries[ $i ]['trace'] = new QM_Backtrace();
644 if ( ! isset( $this->queries[ $i ][3] ) ) {
645 $this->queries[ $i ][3] = $this->time_start;
646 }
647
648 if ( $this->last_error && ! $this->suppress_errors ) {
649 $this->queries[ $i ]['result'] = new WP_Error( 'qmdb', $this->last_error );
650 } else {
651 $this->queries[ $i ]['result'] = (int) $return_val;
652 }
653
654 // Add SQLite query data.
655 $this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries();
656 }
657 return $return_val;
658 }
659
660 /**
661 * Internal function to perform the SQLite query call.
662 *
663 * This closely mirrors wpdb::_do_query().
664 *
665 * @see wpdb::_do_query()
666 *
667 * @param string $query The query to run.
668 */
669 private function _do_query( $query ) {
670 if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
671 $this->timer_start();
672 }
673
674 try {
675 $this->result = $this->dbh->query( $query, PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
676 } catch ( Throwable $e ) {
677 $this->last_error = $this->format_error_message( $e );
678 }
679
680 ++$this->num_queries;
681
682 if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
683 $this->log_query(
684 $query,
685 $this->timer_stop(),
686 $this->get_caller(),
687 $this->time_start,
688 array()
689 );
690 }
691 }
692
693 /**
694 * Method to set the class variable $col_info.
695 *
696 * This overrides wpdb::load_col_info(), which uses a mysql function.
697 *
698 * @see wpdb::load_col_info()
699 */
700 protected function load_col_info() {
701 if ( $this->col_info ) {
702 return;
703 }
704 $this->col_info = array();
705 if ( null === $this->result ) {
706 return;
707 }
708 for ( $i = 0; $i < $this->result->columnCount(); $i++ ) {
709 $column = $this->result->getColumnMeta( $i );
710 $this->col_info[] = (object) array(
711 'name' => $column['name'],
712 'orgname' => $column['mysqli:orgname'],
713 'table' => $column['table'],
714 'orgtable' => $column['mysqli:orgtable'],
715 'def' => '', // Unused, always ''.
716 'db' => $column['mysqli:db'],
717 'catalog' => 'def', // Unused, always 'def'.
718 'max_length' => 0, // As of PHP 8.1, this is always 0.
719 'length' => $column['len'],
720 'charsetnr' => $column['mysqli:charsetnr'],
721 'flags' => $column['mysqli:flags'],
722 'type' => $column['mysqli:type'],
723 'decimals' => $column['precision'],
724 );
725 }
726 }
727
728 /**
729 * Determines whether the database supports a given feature.
730 *
731 * The utf8mb4 check is handled here because older WordPress versions inspect
732 * the MySQL client library. All other capabilities use the parent logic.
733 *
734 * @see wpdb::has_cap()
735 *
736 * @param string $db_cap The feature to check for.
737 * @return bool True when the database feature is supported, false otherwise.
738 */
739 public function has_cap( $db_cap ) {
740 if ( 'utf8mb4' === strtolower( $db_cap ) ) {
741 return true;
742 }
743
744 return parent::has_cap( $db_cap );
745 }
746
747 /**
748 * Retrieves the emulated database server version number.
749 *
750 * This mirrors wpdb::db_version(), but must also be defined here because
751 * WordPress 5.4 and older fetch server information directly from the MySQL
752 * extension instead of delegating to wpdb::db_server_info().
753 *
754 * @see wpdb::db_version()
755 *
756 * @return string Version number on success, or an empty string while disconnected.
757 */
758 public function db_version() {
759 return preg_replace( '/[^0-9.].*/', '', $this->db_server_info() );
760 }
761
762 /**
763 * Returns the raw version string of the emulated MySQL server.
764 *
765 * @see wpdb::db_server_info()
766 *
767 * @return string Emulated MySQL server version, or an empty string while disconnected.
768 */
769 public function db_server_info() {
770 if ( ! $this->dbh ) {
771 return '';
772 }
773
774 return $this->dbh->getAttribute( PDO::ATTR_SERVER_VERSION ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
775 }
776
777 /**
778 * Resolves the charset and collation as wpdb::determine_charset() does.
779 *
780 * Unlike in wpdb, this doesn't need a database connection. The emulated
781 * MySQL server (5.7 or newer) always supports utf8mb4_unicode_520_ci.
782 *
783 * @see wpdb::determine_charset()
784 *
785 * @param string $charset The character set to check.
786 * @param string $collate The collation to check.
787 * @return array {
788 * The most appropriate character set and collation to use.
789 *
790 * @type string $charset Character set.
791 * @type string $collate Collation.
792 * }
793 */
794 private function resolve_charset( $charset, $collate ) {
795 if ( 'utf8' === $charset ) {
796 $charset = 'utf8mb4';
797 }
798
799 if ( 'utf8mb4' === $charset ) {
800 // _general_ is outdated, so we can upgrade it to _unicode_, instead.
801 if ( ! $collate || 'utf8_general_ci' === $collate ) {
802 $collate = 'utf8mb4_unicode_ci';
803 } else {
804 $collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
805 }
806 }
807
808 // _unicode_520_ is a better collation, we should use that when it's available.
809 if ( 'utf8mb4_unicode_ci' === $collate ) {
810 $collate = 'utf8mb4_unicode_520_ci';
811 }
812
813 return compact( 'charset', 'collate' );
814 }
815
816 /**
817 * Format MySQL-on-SQLite driver error message.
818 *
819 * @return string
820 */
821 private function format_error_message( Throwable $e ) {
822 $output = '<div style="clear:both">&nbsp;</div>' . PHP_EOL;
823
824 // Queries.
825 if ( $e instanceof WP_MySQL_On_SQLite_Exception ) {
826 $driver = $e->get_driver();
827
828 $output .= '<div class="queries" style="clear:both;margin-bottom:2px;border:red dotted thin;">' . PHP_EOL;
829 $output .= '<p>MySQL query:</p>' . PHP_EOL;
830 $output .= '<p>' . $driver->get_last_mysql_query() . '</p>' . PHP_EOL;
831 $output .= '<p>Queries made or created this session were:</p>' . PHP_EOL;
832 $output .= '<ol>' . PHP_EOL;
833 foreach ( $driver->get_last_sqlite_queries() as $q ) {
834 $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
835 $output .= '<li>' . htmlspecialchars( $message ) . '</li>' . PHP_EOL;
836 }
837 $output .= '</ol>' . PHP_EOL;
838 $output .= '</div>' . PHP_EOL;
839 }
840
841 // Message.
842 $output .= '<div style="clear:both;margin-bottom:2px;border:red dotted thin;" class="error_message" style="border-bottom:dotted blue thin;">' . PHP_EOL;
843 $output .= $e->getMessage() . PHP_EOL;
844 $output .= '</div>' . PHP_EOL;
845
846 // Backtrace.
847 $output .= '<p>Backtrace:</p>' . PHP_EOL;
848 $output .= '<pre>' . $e->getTraceAsString() . '</pre>' . PHP_EOL;
849 return $output;
850 }
851 }
852