PluginProbe
SQLite Database Integration / 2.2.3
SQLite Database Integration v2.2.3
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 2.2.3 All 32 releases
sqlite-database-integration / wp-includes / sqlite / class-wp-sqlite-db.php

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

659 lines 19.2 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 * @package wp-sqlite-integration
6 * @since 1.0.0
7 */
8
9 /**
10 * This class extends wpdb and replaces it.
11 *
12 * It also rewrites some methods that use mysql specific functions.
13 */
14 class WP_SQLite_DB extends wpdb {
15
16 /**
17 * Database Handle
18 *
19 * @var WP_SQLite_Translator
20 */
21 protected $dbh;
22
23 /**
24 * Backward compatibility, see wpdb::$allow_unsafe_unquoted_parameters.
25 *
26 * This property is mirroring "wpdb::$allow_unsafe_unquoted_parameters",
27 * because some tests are accessing it externally using PHP reflection.
28 *
29 * @var
30 */
31 private $allow_unsafe_unquoted_parameters = true;
32
33 /**
34 * Connects to the SQLite database.
35 *
36 * Unlike for MySQL, no credentials and host are needed.
37 *
38 * @param string $dbname Database name.
39 */
40 public function __construct( $dbname ) {
41 /**
42 * We need to initialize the "$wpdb" global early, so that the SQLite
43 * driver can configure the database. The call stack goes like this:
44 *
45 * 1. The "parent::__construct()" call executes "$this->db_connect()".
46 * 2. The database connection call initializes the SQLite driver.
47 * 3. The SQLite driver initializes and runs "WP_SQLite_Configurator".
48 * 4. The configurator uses "WP_SQLite_Information_Schema_Reconstructor",
49 * which requires "wp-admin/includes/schema.php" when in WordPress.
50 * 5. The "wp-admin/includes/schema.php" requires the "$wpdb" global,
51 * which creates a circular dependency.
52 */
53 $GLOBALS['wpdb'] = $this;
54
55 parent::__construct( '', '', $dbname, '' );
56 $this->charset = 'utf8mb4';
57 }
58
59 /**
60 * Method to set character set for the database.
61 *
62 * This overrides wpdb::set_charset(), only to dummy out the MySQL function.
63 *
64 * @see wpdb::set_charset()
65 *
66 * @param resource $dbh The resource given by mysql_connect.
67 * @param string $charset Optional. The character set. Default null.
68 * @param string $collate Optional. The collation. Default null.
69 */
70 public function set_charset( $dbh, $charset = null, $collate = null ) {
71 }
72
73 /**
74 * Method to get the character set for the database.
75 * Hardcoded to utf8mb4 for now.
76 *
77 * @param string $table The table name.
78 * @param string $column The column name.
79 *
80 * @return string The character set.
81 */
82 public function get_col_charset( $table, $column ) {
83 // Hardcoded for now.
84 return 'utf8mb4';
85 }
86
87 /**
88 * Changes the current SQL mode, and ensures its WordPress compatibility.
89 *
90 * If no modes are passed, it will ensure the current MySQL server modes are compatible.
91 *
92 * This overrides wpdb::set_sql_mode() while closely mirroring its implementation.
93 *
94 * @param array $modes Optional. A list of SQL modes to set. Default empty array.
95 */
96 public function set_sql_mode( $modes = array() ) {
97 if ( ! $this->dbh instanceof WP_SQLite_Driver ) {
98 return;
99 }
100
101 if ( empty( $modes ) ) {
102 $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' );
103 if ( ! isset( $result[0] ) ) {
104 return;
105 }
106
107 $modes_str = $result[0]->{'@@SESSION.sql_mode'};
108 if ( empty( $modes_str ) ) {
109 return;
110 }
111 $modes = explode( ',', $modes_str );
112 }
113
114 $modes = array_change_key_case( $modes, CASE_UPPER );
115
116 /**
117 * Filters the list of incompatible SQL modes to exclude.
118 *
119 * @since 3.9.0
120 *
121 * @param array $incompatible_modes An array of incompatible modes.
122 */
123 $incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
124
125 foreach ( $modes as $i => $mode ) {
126 if ( in_array( $mode, $incompatible_modes, true ) ) {
127 unset( $modes[ $i ] );
128 }
129 }
130 $modes_str = implode( ',', $modes );
131
132 $this->dbh->query( "SET SESSION sql_mode='$modes_str'" );
133 }
134
135 /**
136 * Closes the current database connection.
137 * Noop in SQLite.
138 *
139 * @return bool True to indicate the connection was successfully closed.
140 */
141 public function close() {
142 return true;
143 }
144
145 /**
146 * Method to select the database connection.
147 *
148 * This overrides wpdb::select(), only to dummy out the MySQL function.
149 *
150 * @see wpdb::select()
151 *
152 * @param string $db MySQL database name. Not used.
153 * @param resource|null $dbh Optional link identifier.
154 */
155 public function select( $db, $dbh = null ) {
156 $this->ready = true;
157 }
158
159 /**
160 * Method to escape characters.
161 *
162 * This overrides wpdb::_real_escape() to avoid using mysql_real_escape_string().
163 *
164 * @see wpdb::_real_escape()
165 *
166 * @param string $data The string to escape.
167 *
168 * @return string escaped
169 */
170 public function _real_escape( $data ) {
171 if ( ! is_scalar( $data ) ) {
172 return '';
173 }
174 $escaped = addslashes( $data );
175 return $this->add_placeholder_escape( $escaped );
176 }
177
178 /**
179 * Method to dummy out wpdb::esc_like() function.
180 *
181 * WordPress 4.0.0 introduced esc_like() function that adds backslashes to %,
182 * underscore and backslash, which is not interpreted as escape character
183 * by SQLite. So we override it and dummy out this function.
184 *
185 * @param string $text The raw text to be escaped. The input typed by the user should have no
186 * extra or deleted slashes.
187 *
188 * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare()
189 * or real_escape next.
190 */
191 public function esc_like( $text ) {
192 // The new driver adds "ESCAPE '\\'" to every LIKE expression by default.
193 // We only need to overload this function to a no-op for the old driver.
194 if ( $this->dbh instanceof WP_SQLite_Driver ) {
195 return parent::esc_like( $text );
196 }
197 return $text;
198 }
199
200 /**
201 * Prints SQL/DB error.
202 *
203 * This overrides wpdb::print_error() while closely mirroring its implementation.
204 *
205 * @global array $EZSQL_ERROR Stores error information of query and error string.
206 *
207 * @param string $str The error to display.
208 * @return void|false Void if the showing of errors is enabled, false if disabled.
209 */
210 public function print_error( $str = '' ) {
211 global $EZSQL_ERROR;
212
213 if ( ! $str ) {
214 $str = $this->last_error;
215 }
216
217 $EZSQL_ERROR[] = array(
218 'query' => $this->last_query,
219 'error_str' => $str,
220 );
221
222 if ( $this->suppress_errors ) {
223 return false;
224 }
225
226 $caller = $this->get_caller();
227 if ( $caller ) {
228 // Not translated, as this will only appear in the error log.
229 $error_str = sprintf( 'WordPress database error %1$s for query %2$s made by %3$s', $str, $this->last_query, $caller );
230 } else {
231 $error_str = sprintf( 'WordPress database error %1$s for query %2$s', $str, $this->last_query );
232 }
233
234 error_log( $error_str );
235
236 // Are we showing errors?
237 if ( ! $this->show_errors ) {
238 return false;
239 }
240
241 wp_load_translations_early();
242
243 // If there is an error then take note of it.
244 if ( is_multisite() ) {
245 $msg = sprintf(
246 "%s [%s]\n%s\n",
247 __( 'WordPress database error:' ),
248 $str,
249 $this->last_query
250 );
251
252 if ( defined( 'ERRORLOGFILE' ) ) {
253 error_log( $msg, 3, ERRORLOGFILE );
254 }
255 if ( defined( 'DIEONDBERROR' ) ) {
256 wp_die( $msg );
257 }
258 } else {
259 $str = htmlspecialchars( $str, ENT_QUOTES );
260 $query = htmlspecialchars( $this->last_query, ENT_QUOTES );
261
262 printf(
263 '<div id="error"><p class="wpdberror"><strong>%s</strong> [%s]<br /><code>%s</code></p></div>',
264 __( 'WordPress database error:' ),
265 $str,
266 $query
267 );
268 }
269 }
270
271 /**
272 * Method to flush cached data.
273 *
274 * This overrides wpdb::flush(). This is not necessarily overridden, because
275 * $result will never be resource.
276 *
277 * @see wpdb::flush
278 */
279 public function flush() {
280 $this->last_result = array();
281 $this->col_info = null;
282 $this->last_query = null;
283 $this->rows_affected = 0;
284 $this->num_rows = 0;
285 $this->last_error = '';
286 $this->result = null;
287 }
288
289 /**
290 * Method to do the database connection.
291 *
292 * This overrides wpdb::db_connect() to avoid using MySQL function.
293 *
294 * @see wpdb::db_connect()
295 *
296 * @param bool $allow_bail Not used.
297 * @return void
298 */
299 public function db_connect( $allow_bail = true ) {
300 if ( $this->dbh ) {
301 return;
302 }
303 $this->init_charset();
304
305 $pdo = null;
306 if ( isset( $GLOBALS['@pdo'] ) ) {
307 $pdo = $GLOBALS['@pdo'];
308 }
309 if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) {
310 if ( null === $this->dbname || '' === $this->dbname ) {
311 $this->bail(
312 'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
313 'db_connect_fail'
314 );
315 return false;
316 }
317
318 require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
319 require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
320 require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
321 require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
322 require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
323 require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
324 require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
325 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php';
326 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php';
327 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
328 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
329 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
330 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php';
331 require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php';
332 $this->ensure_database_directory( FQDB );
333
334 try {
335 $connection = new WP_SQLite_Connection(
336 array(
337 'pdo' => $pdo,
338 'path' => FQDB,
339 'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
340 )
341 );
342 $this->dbh = new WP_SQLite_Driver( $connection, $this->dbname );
343 $GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo();
344 } catch ( Throwable $e ) {
345 $this->last_error = $this->format_error_message( $e );
346 }
347 } else {
348 $this->dbh = new WP_SQLite_Translator( $pdo );
349 $this->last_error = $this->dbh->get_error_message();
350 $GLOBALS['@pdo'] = $this->dbh->get_pdo();
351 }
352 if ( $this->last_error ) {
353 return false;
354 }
355 $this->ready = true;
356 $this->set_sql_mode();
357 }
358
359 /**
360 * Method to dummy out wpdb::check_connection()
361 *
362 * @param bool $allow_bail Not used.
363 *
364 * @return bool
365 */
366 public function check_connection( $allow_bail = true ) {
367 return true;
368 }
369
370 /**
371 * Prepares a SQL query for safe execution.
372 *
373 * See "wpdb::prepare()". This override only fixes a WPDB test issue.
374 *
375 * @param string $query Query statement with `sprintf()`-like placeholders.
376 * @param array|mixed $args The array of variables or the first variable to substitute.
377 * @param mixed ...$args Further variables to substitute when using individual arguments.
378 * @return string|void Sanitized query string, if there is a query to prepare.
379 */
380 public function prepare( $query, ...$args ) {
381 /*
382 * Sync "$allow_unsafe_unquoted_parameters" with the WPDB parent property.
383 * This is only needed because some WPDB tests are accessing the private
384 * property externally via PHP reflection. This should be fixed WP tests.
385 */
386 $wpdb_allow_unsafe_unquoted_parameters = $this->__get( 'allow_unsafe_unquoted_parameters' );
387 if ( $wpdb_allow_unsafe_unquoted_parameters !== $this->allow_unsafe_unquoted_parameters ) {
388 $property = new ReflectionProperty( 'wpdb', 'allow_unsafe_unquoted_parameters' );
389 $property->setAccessible( true );
390 $property->setValue( $this, $this->allow_unsafe_unquoted_parameters );
391 $property->setAccessible( false );
392 }
393
394 return parent::prepare( $query, ...$args );
395 }
396
397 /**
398 * Performs a database query.
399 *
400 * This overrides wpdb::query() while closely mirroring its implementation.
401 *
402 * @see wpdb::query()
403 *
404 * @param string $query Database query.
405 *
406 * @param string $query Database query.
407 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
408 * affected/selected for all other queries. Boolean false on error.
409 */
410 public function query( $query ) {
411 if ( ! $this->ready ) {
412 return false;
413 }
414
415 $query = apply_filters( 'query', $query );
416
417 if ( ! $query ) {
418 $this->insert_id = 0;
419 return false;
420 }
421
422 $this->flush();
423
424 // Log how the function was called.
425 $this->func_call = "\$db->query(\"$query\")";
426
427 // Keep track of the last query for debug.
428 $this->last_query = $query;
429
430 /*
431 * @TODO: WPDB uses "$this->check_current_query" to check table/column
432 * charset and strip all invalid characters from the query.
433 * This is an involved process that we can bypass for SQLite,
434 * if we simply strip all invalid UTF-8 characters from the query.
435 *
436 * To do so, mb_convert_encoding can be used with an optional
437 * fallback to a htmlspecialchars method. E.g.:
438 * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
439 */
440 $this->_do_query( $query );
441
442 if ( $this->last_error ) {
443 // Clear insert_id on a subsequent failed insert.
444 if ( $this->insert_id && preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
445 $this->insert_id = 0;
446 }
447
448 $this->print_error();
449 return false;
450 }
451
452 if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
453 $return_val = true;
454 } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
455 if ( $this->dbh instanceof WP_SQLite_Driver ) {
456 $this->rows_affected = $this->dbh->get_last_return_value();
457 } else {
458 $this->rows_affected = $this->dbh->get_affected_rows();
459 }
460
461 // Take note of the insert_id.
462 if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
463 $this->insert_id = $this->dbh->get_insert_id();
464 }
465
466 // Return number of rows affected.
467 $return_val = $this->rows_affected;
468 } else {
469 $num_rows = 0;
470
471 if ( is_array( $this->result ) ) {
472 $this->last_result = $this->result;
473 $num_rows = count( $this->result );
474 }
475
476 // Log and return the number of rows selected.
477 $this->num_rows = $num_rows;
478 $return_val = $num_rows;
479 }
480
481 return $return_val;
482 }
483
484 /**
485 * Internal function to perform the SQLite query call.
486 *
487 * This closely mirrors wpdb::_do_query().
488 *
489 * @see wpdb::_do_query()
490 *
491 * @param string $query The query to run.
492 */
493 private function _do_query( $query ) {
494 if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
495 $this->timer_start();
496 }
497
498 try {
499 $this->result = $this->dbh->query( $query );
500 } catch ( Throwable $e ) {
501 $this->last_error = $this->format_error_message( $e );
502 }
503
504 if ( $this->dbh instanceof WP_SQLite_Translator ) {
505 $this->last_error = $this->dbh->get_error_message();
506 }
507
508 ++$this->num_queries;
509
510 if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
511 $this->log_query(
512 $query,
513 $this->timer_stop(),
514 $this->get_caller(),
515 $this->time_start,
516 array()
517 );
518 }
519 }
520
521 /**
522 * Method to set the class variable $col_info.
523 *
524 * This overrides wpdb::load_col_info(), which uses a mysql function.
525 *
526 * @see wpdb::load_col_info()
527 */
528 protected function load_col_info() {
529 if ( $this->col_info ) {
530 return;
531 }
532 $this->col_info = $this->dbh->get_columns();
533 }
534
535 /**
536 * Method to return what the database can do.
537 *
538 * This overrides wpdb::has_cap() to avoid using MySQL functions.
539 * SQLite supports subqueries, but not support collation, group_concat and set_charset.
540 *
541 * @see wpdb::has_cap()
542 *
543 * @param string $db_cap The feature to check for. Accepts 'collation',
544 * 'group_concat', 'subqueries', 'set_charset',
545 * 'utf8mb4', or 'utf8mb4_520'.
546 *
547 * @return bool Whether the database feature is supported, false otherwise.
548 */
549 public function has_cap( $db_cap ) {
550 return 'subqueries' === strtolower( $db_cap );
551 }
552
553 /**
554 * Method to return database version number.
555 *
556 * This overrides wpdb::db_version() to avoid using MySQL function.
557 * It returns mysql version number, but it means nothing for SQLite.
558 * So it return the newest mysql version.
559 *
560 * @see wpdb::db_version()
561 */
562 public function db_version() {
563 return '8.0';
564 }
565
566 /**
567 * Returns the version of the SQLite engine.
568 *
569 * @return string SQLite engine version as a string.
570 */
571 public function db_server_info() {
572 return $this->dbh->get_sqlite_version();
573 }
574
575 /**
576 * Make sure the SQLite database directory exists and is writable.
577 * Create .htaccess and index.php files to prevent direct access.
578 *
579 * @param string $database_path The path to the SQLite database file.
580 */
581 private function ensure_database_directory( string $database_path ) {
582 $dir = dirname( $database_path );
583
584 // Set the umask to 0000 to apply permissions exactly as specified.
585 // A non-zero umask affects new file and directory permissions.
586 $umask = umask( 0 );
587
588 // Ensure database directory.
589 if ( ! is_dir( $dir ) ) {
590 if ( ! @mkdir( $dir, 0700, true ) ) {
591 wp_die( sprintf( 'Failed to create database directory: %s', $dir ), 'Error!' );
592 }
593 }
594 if ( ! is_writable( $dir ) ) {
595 wp_die( sprintf( 'Database directory is not writable: %s', $dir ), 'Error!' );
596 }
597
598 // Ensure .htaccess file to prevent direct access.
599 $path = $dir . DIRECTORY_SEPARATOR . '.htaccess';
600 if ( ! is_file( $path ) ) {
601 $result = file_put_contents( $path, 'DENY FROM ALL', LOCK_EX );
602 if ( false === $result ) {
603 wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
604 }
605 chmod( $path, 0600 );
606 }
607
608 // Ensure index.php file to prevent direct access.
609 $path = $dir . DIRECTORY_SEPARATOR . 'index.php';
610 if ( ! is_file( $path ) ) {
611 $result = file_put_contents( $path, '<?php // Silence is gold. ?>', LOCK_EX );
612 if ( false === $result ) {
613 wp_die( sprintf( 'Failed to create file: %s', $path ), 'Error!' );
614 }
615 chmod( $path, 0600 );
616 }
617
618 // Restore the original umask value.
619 umask( $umask );
620 }
621
622
623 /**
624 * Format SQLite driver error message.
625 *
626 * @return string
627 */
628 private function format_error_message( Throwable $e ) {
629 $output = '<div style="clear:both">&nbsp;</div>' . PHP_EOL;
630
631 // Queries.
632 if ( $e instanceof WP_SQLite_Driver_Exception ) {
633 $driver = $e->getDriver();
634
635 $output .= '<div class="queries" style="clear:both;margin-bottom:2px;border:red dotted thin;">' . PHP_EOL;
636 $output .= '<p>MySQL query:</p>' . PHP_EOL;
637 $output .= '<p>' . $driver->get_last_mysql_query() . '</p>' . PHP_EOL;
638 $output .= '<p>Queries made or created this session were:</p>' . PHP_EOL;
639 $output .= '<ol>' . PHP_EOL;
640 foreach ( $driver->get_last_sqlite_queries() as $q ) {
641 $message = "Executing: {$q['sql']} | " . ( $q['params'] ? 'parameters: ' . implode( ', ', $q['params'] ) : '(no parameters)' );
642 $output .= '<li>' . htmlspecialchars( $message ) . '</li>' . PHP_EOL;
643 }
644 $output .= '</ol>' . PHP_EOL;
645 $output .= '</div>' . PHP_EOL;
646 }
647
648 // Message.
649 $output .= '<div style="clear:both;margin-bottom:2px;border:red dotted thin;" class="error_message" style="border-bottom:dotted blue thin;">' . PHP_EOL;
650 $output .= $e->getMessage() . PHP_EOL;
651 $output .= '</div>' . PHP_EOL;
652
653 // Backtrace.
654 $output .= '<p>Backtrace:</p>' . PHP_EOL;
655 $output .= '<pre>' . $e->getTraceAsString() . '</pre>' . PHP_EOL;
656 return $output;
657 }
658 }
659