PluginProbe
SQLite Object Cache / trunk
SQLite Object Cache vtrunk
1.6.5 trunk 0.1.7 1.0.0 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.4.0 1.4.1 1.5.1 1.5.4 1.5.5 1.5.6 1.5.7 All 30 releases
sqlite-object-cache / assets / drop-in / object-cache.php

object-cache.php in SQLite Object Cache trunk, at assets/drop-in/object-cache.php

3,276 lines 101.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: SQLite Object Cache (Drop-in)
4 * Version: 1.6.5
5 * Note: This Version number must match the one in SQLite_Object_Cache::_construct.
6 * Plugin URI: https://wordpress.org/plugins/sqlite-object-cache/
7 * Description: A persistent object cache backend powered by SQLite3.
8 * Author: Oliver Jones
9 * Author URI: https://plumislandmedia.net
10 * License: GPLv2+
11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
12 * Requires PHP: 5.6
13 * Tested up to: 7.1
14 * Stable tag: 1.6.5
15 *
16 * NOTE: This uses the file .../wp-content/.ht.object_cache.sqlite
17 * and the associated files .../wp-content/.ht.object_cache.sqlite-shm
18 * and .../wp-content/.ht.object_cache.sqlite-wal to hold cached data.
19 * These start with .ht. for security: Many web servers block requests
20 * for files with that prefix. Use the UNIX ls -a command to
21 * see these files from your command line.
22 *
23 * Some config settings control this.
24 * WP_SQLITE_OBJECT_CACHE_APCU, if true, enables cache acceleration with APCu RAM. This setting can be updated from the plugin's Settings page.
25 * WP_SQLITE_OBJECT_CACHE_DB_FILE, if defined, is the cache file path.
26 * /var/tmp/cache.sqlite puts the cache file outside the document root.
27 * WP_CACHE_KEY_SALT, if present, is used as part of the cache file name, and as a prefix for APCu keys.
28 * WP_SQLITE_OBJECT_CACHE_TIMEOUT is the SQLite timeout in place of 5000 milliseconds.
29 * WP_SQLITE_OBJECT_CACHE_SERIALIZE, if true, requires the use of php serialize.
30 * WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE is the SQLite journal mode in place of 'WAL'.
31 * It can be DELETE | TRUNCATE | PERSIST | MEMORY | WAL. See https://www.sqlite.org/pragma.html#pragma_journal_mode.
32 * WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH is the number of digits for optimizing consecutive integer cache keys, default 6.
33 * WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS allows fewer SQL statements but can retrieve extra items, default 2.
34 * WP_SQLITE_OBJECT_CACHE_MMAP_SIZE sets SQLite's mmap_size in MiB. Default 0: disabled.
35 * WP_SQLITE_OBJECT_CACHE_CHECKPOINT_FREQ. How often, probabilistically, to force checkpointing SQLite3. Make this number smaller on busy sites if your WAL file gets too long. Default 5000.
36 *
37 * Credit: Till Krüss's https://wordpress.org/plugins/redis-cache/ plugin. Thanks, Till!
38 *
39 * @package SQLiteCache
40 */
41
42 /** @noinspection SqlDialectInspection */
43
44 if ( ! defined( 'ABSPATH' ) ) {
45 exit;
46 }
47
48 if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLED' ) && WP_SQLITE_OBJECT_CACHE_DISABLED ) {
49 return;
50 }
51
52 /**
53 * hrtime polyfill if needed, pre php 7.3.
54 */
55 if ( ! function_exists( 'hrtime' ) ) {
56 function hrtime( $as_float = false ) {
57 if ( $as_float ) {
58 return microtime( true ) * 1000;
59 }
60 $result = microtime( false );
61 $result[1] = 1000 * $result [1];
62 return $result;
63 }
64 }
65
66 /**
67 * Object Cache API: WP_Object_Cache class, reworked for SQLite3 drop-in.
68 *
69 * NOTE WELL: SQL in this file is not for use with $wpdb, but for SQLite3.
70 *
71 * @package WordPress
72 * @subpackage Cache
73 * @since 5.4.0
74 */
75
76 /**
77 * Core class that implements an object cache.
78 *
79 * The WordPress Object Cache is used to save on trips to the database. The
80 * Object Cache stores cache data to memory and makes the cache
81 * contents available by using a key, which is used to name and later retrieve
82 * the cache contents.
83 *
84 * This module is a drop-in, placed in the WP_CONTENT folder, implementing
85 * the WordPress Object Cache class, while using SQLite3 for persistent storage.
86 *
87 * @since 0.1.0
88 */
89 class WP_Object_Cache {
90 const OBJECT_STATS_TABLE = 'object_stats';
91 const OBJECT_CACHE_TABLE = 'object_cache';
92 const OBJECT_FLAGS_TABLE = 'object_flags';
93 const NOEXPIRE_TIMESTAMP_OFFSET = 500000000000;
94 const INTKEY_LENGTH = 6;
95 const MMAP_SIZE = 0.0;
96 const INTKEY_ERODE_GAPS = 2;
97 const INTKEY_SENTINEL = "\x1f"; /* Only one character allowed here. */
98 const SQLITE_TIMEOUT = 5000;
99 const SQLITE_FILENAME = '.ht.object-cache.sqlite';
100 const JOURNAL_MODE = 'WAL'; /* or 'MEMORY' */
101 const TRANSACTION_SIZE_LIMIT = 64;
102 const CHECKPOINT_FREQ = 2000;
103
104 private $dropin_version = '1.6.5';
105 /** @var bool True if a transaction is active. */
106 private $transaction_active = false;
107 /** Path to SQLite file. @var string */
108 public $sqlite_path;
109
110 /**
111 * @var string|null Version of SQLite3 software in use.
112 */
113 private $sqlite_version;
114
115 /**
116 * @var int Checkpoint frequency. Probabilistic.
117 */
118 private $checkpoint_freq;
119
120 /**
121 * SQLite's journal mode.
122 *
123 * Avoid the OFF journal mode, especially in pre-3.24 versions of SQLite.
124 *
125 * @see https://www.sqlite.org/pragma.html#pragma_journal_mode
126 *
127 * @var string MEMORY, WAL, DELETE, TRUNCATE, PERSIST, OFF
128 */
129 private $sqlite_journal_mode;
130 /**
131 * Timeout waiting for transaction completion.
132 *
133 * @var int
134 */
135 private $sqlite_timeout;
136 /**
137 * The amount of times the cache data was already stored in the cache.
138 *
139 * @since 2.5.0
140 * @var int
141 */
142 public $cache_hits = 0;
143 /**
144 * Amount of times the cache did not have the request in cache.
145 *
146 * @since 2.0.0
147 * @var int
148 */
149 public $cache_misses = 0;
150 /**
151 * The amount of times the cache data was already stored in the persistent cache.
152 *
153 * @since 2.5.0
154 * @var int
155 */
156 public $persistent_hits = 0;
157 /**
158 * Amount of times the cache did not have the request in persistent cache.
159 *
160 * @since 2.0.0
161 * @var int
162 */
163 public $persistent_misses = 0;
164 /**
165 * Amount of times the apcu cache had the item.
166 *
167 * @since 2.0.0
168 * @var int
169 */
170 public $apcu_hits = 0;
171 /**
172 * Amount of times the apcu cache did not have the item.
173 *
174 * @since 2.0.0
175 * @var int
176 */
177 public $apcu_misses = 0;
178 /**
179 * The blog prefix to prepend to keys in non-global groups.
180 *
181 * @since 3.5.0
182 * @var string For multisite, n:, For single site, empty.
183 */
184 public $blog_prefix;
185 /**
186 * List of groups that will not be flushed.
187 *
188 * @var array
189 */
190 public $unflushable_groups = array();
191 /**
192 * List of groups not saved to cache.
193 *
194 * @var array
195 */
196 public $ignored_groups = array(
197 'counts',
198 'plugins',
199 'themes',
200 );
201 /**
202 * List of groups and their types.
203 *
204 * @var array
205 */
206 public $group_type = array();
207 /**
208 * Prefix used for global groups.
209 *
210 * @var string
211 */
212 public $global_prefix = '';
213 /**
214 * List of global groups.
215 *
216 * @var array
217 */
218 protected $global_groups = array(
219 'blog-details',
220 'blog-id-cache',
221 'blog-lookup',
222 'global-posts',
223 'networks',
224 'rss',
225 'sites',
226 'site-details',
227 'site-lookup',
228 'site-options',
229 'site-transient',
230 'users',
231 'useremail',
232 'userlogins',
233 'usermeta',
234 'user_meta',
235 'userslugs',
236 );
237
238 /**
239 * @var array One-level associative array $name=>$value
240 */
241 private $cache = array();
242 /**
243 * Holds the value of is_multisite().
244 *
245 * @since 3.5.0
246 * @var bool
247 */
248 private $multisite;
249
250 /**
251 * Prepared statement to get one cache element.
252 *
253 * @var SQLite3Stmt SELECT statement.
254 */
255 private $getone_stmt;
256
257 /**
258 * Prepared statement to get a range of cache elements, for get_multiple.
259 *
260 * @var SQLite3Stmt SELECT statement.
261 */
262 private $getrange_stmt;
263
264 /**
265 * Prepared statement to delete one cache element.
266 *
267 * @var SQLite3Stmt DELETE statement.
268 */
269 private $deleteone_stmt;
270
271 /**
272 * Prepared statement to delete a group of cache elements.
273 *
274 * @var SQLite3Stmt
275 */
276 private $deletegroup_stmt;
277
278 /**
279 * Prepared statement to upsert one cache element.
280 *
281 * @var SQLite3Stmt
282 */
283 private $upsertone_stmt;
284
285 /**
286 * Prepared statement to insert one cache element.
287 *
288 * @var SQLite3Stmt
289 */
290 private $insertone_stmt;
291
292 /**
293 * Prepared statement to update one cache element.
294 *
295 * @var SQLite3Stmt
296 */
297 private $updateone_stmt;
298
299 /**
300 * Prepared statement to clear a flagt.
301 *
302 * @var SQLite3Stmt
303 */
304 private $clearflag_stmt;
305
306 /**
307 * Prepared statement to set a flagt.
308 *
309 * @var SQLite3Stmt
310 */
311 private $setflag_stmt;
312
313 /**
314 * Associative array of items we know ARE NOT in SQLite.
315 *
316 * When a name is not in this array it means we don't know if it is in SQLite or not.
317 *
318 * @var array Keys are cached item names. Values are true.
319 */
320 private $not_in_persistent_cache = array();
321 /**
322 * Cache table name.
323 *
324 * @var string Usually 'object_cache'.
325 */
326 private $cache_table_name;
327 /**
328 * Flags table name.
329 *
330 * @var string Usually 'object_flags'.
331 */
332 private $flags_table_name;
333 /**
334 * Flag for availability of igbinary serialization extension.
335 * This will be false if igbinary is not available or if WP_SQLITE_OBJECT_CACHE_SERIALIZE is true.
336 *
337 * @var bool true if it is available.
338 */
339 private $has_igbinary;
340 /**
341 * The expiration time of non-expiring cache entries has this added to the timestamp.
342 *
343 * This is a sentinel value, marking a non-expiring cache entry AND
344 * recording when it was inserted or updated.
345 * It allows a least-recently-changed cache-entry purging strategy.
346 *
347 * If we wanted a least-recently-used purge, we would need to
348 * update each cache item's row whenever we accessed it. That
349 * would cost more than it's worth.
350 *
351 * @var int a large number of seconds, much larger than 2**32
352 */
353 private $noexpire_timestamp_offset;
354 /**
355 * The starting time of the request, from time().
356 * @var
357 */
358 private $start_time;
359 /**
360 * The starting time of the request, from hrtime().
361 * @var
362 */
363 private $start_hrtime;
364 /**
365 * An array of overall get times, excluding RAM cache.
366 * @var array
367 */
368 private $get_times = array();
369 /**
370 * An array of elapsed times for each cache-retrieval operation.
371 *
372 * @var array[float]
373 */
374 private $select_times = array();
375 /**
376 * An array of elapsed times for each cache-insertion / update operation.
377 *
378 * @var array[float]
379 */
380 private $insert_times = array();
381 /**
382 * An array of elapsed times for each single-row cache deletion operation.
383 *
384 * @var array[float]
385 */
386 private $delete_times = array();
387
388 /**
389 * The times for individual checkpoint -- PRAGMA wal_checkpoint(RESTART) -- times
390 * @var array
391 */
392 private $checkpoint_times = array();
393 /**
394 * The times for individual get_multiple operations.
395 *
396 * @var array[float]
397 */
398 private $get_multiple_times = array();
399 /**
400 * The times for apcu_store operations.
401 * @var array
402 */
403 private $apcu_fetch_hit_times = array();
404 /**
405 * The times for apcu_store operations.
406 * @var array
407 */
408 private $apcu_fetch_miss_times = array();
409 /**
410 * The times for apcu_store operations.
411 * @var array
412 */
413 private $apcu_store_times = array();
414
415 /**
416 * The humber of keys for individual get_multiple operations.
417 *
418 * @var array[int]
419 */
420 private $get_multiple_keys = array();
421 /**
422 * The time it took to open the db.
423 *
424 * @var float
425 */
426 private $open_time;
427
428 /**
429 * Monitoring options for the SQLite cache.
430 *
431 * Options in array [
432 * 'capture' => (bool)
433 * 'resolution' => how often in seconds (float)
434 * 'lifetime' => how long until entries expire in seconds (int)
435 * 'verbose' => (bool) capture extra stuff.
436 * ]
437 *
438 * @var array $options Option list.
439 */
440 private $monitoring_options;
441
442 /**
443 * Recursion count.
444 *
445 * @var int Recursion in the get command.
446 */
447 private $get_depth = 31;
448 /**
449 * Database object.
450 * @var SQLite3 instance.
451 */
452 private $sqlite;
453 /**
454 * @var int The max number of digits in optimized integer cache keys.
455 *
456 * Longer integers than this are treated as text.
457 */
458 private $intkey_length;
459 /**
460 * @var int The maximum value of integer keys before we handle them as strings.
461 *
462 * Longer integers than this are treated as text.
463 */
464 private $intkey_max;
465 /**
466 * @var int Erode gaps in consecutive runs of integers by this amount.
467 *
468 * This makes for fewer SQL queries at the cost of some extra retrieved items.
469 */
470 private $erode_gaps;
471
472 /**
473 * @var int mmap_size setting for SQLite. Zero to disable.
474 */
475 private $mmap_size = 0;
476 /**
477 * The APCu cache is active in this request
478 * @var bool
479 */
480 private $apcu_active = false;
481 /**
482 * The APCu cache is active in this site, but not in this request.
483 *
484 * This happens for wp-cli programs.
485 * @var bool
486 */
487 private $apcu_supported = false;
488 private $salt;
489 /**
490 * @var string The APCu prefix, ending with '|'.
491 */
492 public $apcusalt;
493
494 /**
495 * Constructor for SQLite Object Cache.
496 *
497 * @since 2.0.8
498 */
499 public function __construct() {
500 $this->start_hrtime = hrtime( true );
501 $this->start_time = time();
502 global $table_prefix;
503 $this->cache_group_types();
504
505 /* The environment. */
506 $apc = defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU;
507 $cli = defined( 'WP_CLI' ) && WP_CLI;
508 $this->apcu_active = $apc && function_exists( 'apcu_enabled' ) && apcu_enabled() && ! $cli;
509 $this->apcu_supported = $apc && $cli;
510
511 $force_serialize = defined( 'WP_SQLITE_OBJECT_CACHE_SERIALIZE' ) && WP_SQLITE_OBJECT_CACHE_SERIALIZE;
512 $this->has_igbinary = function_exists( 'igbinary_serialize' ) && ! $force_serialize;
513 $this->salt = defined( 'WP_CACHE_KEY_SALT' )
514 ? preg_replace( '/[^-_A-Za-z0-9]/', '_', WP_CACHE_KEY_SALT )
515 : '';
516 if ( $this->apcu_active ) {
517 /* As unique as possible to avoid collisions with other instances on the same server. */
518 $this->apcusalt = ( ( '' !== $this->salt )
519 ? $this->salt
520 : substr( base64_encode( md5( $this->salt . $table_prefix . DB_HOST . DB_USER . DB_NAME . AUTH_KEY . AUTH_SALT ) ),
521 0, 12 ) ) . '|';
522
523 }
524 $this->sqlite_path = $this->create_database_path();
525
526 $this->checkpoint_freq = 4 * ( defined( 'WP_SQLITE_OBJECT_CACHE_CHECKPOINT_FREQ' )
527 ? (int) WP_SQLITE_OBJECT_CACHE_CHECKPOINT_FREQ
528 : self::CHECKPOINT_FREQ );
529
530 $this->sqlite_timeout = defined( 'WP_SQLITE_OBJECT_CACHE_TIMEOUT' )
531 ? WP_SQLITE_OBJECT_CACHE_TIMEOUT
532 : self::SQLITE_TIMEOUT;
533
534 $this->sqlite_journal_mode = defined( 'WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE' )
535 ? WP_SQLITE_OBJECT_CACHE_JOURNAL_MODE
536 : self::JOURNAL_MODE;
537
538 $this->erode_gaps = defined( 'WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS' )
539 ? (int) WP_SQLITE_OBJECT_CACHE_INTKEY_ERODE_GAPS
540 : self::INTKEY_ERODE_GAPS;
541
542 $this->intkey_length = defined( 'WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH' )
543 ? (int) WP_SQLITE_OBJECT_CACHE_INTKEY_LENGTH
544 : self::INTKEY_LENGTH;
545
546 $this->intkey_max = - 1 + (int) str_pad( '1', 1 + $this->intkey_length, 0, STR_PAD_RIGHT );
547
548 $this->mmap_size = defined( 'WP_SQLITE_OBJECT_CACHE_MMAP_SIZE' )
549 ? (int) WP_SQLITE_OBJECT_CACHE_MMAP_SIZE
550 : self::MMAP_SIZE;
551 $this->mmap_size = (int) $this->mmap_size * 1024 * 1024;
552
553 $this->multisite = is_multisite();
554 $this->blog_prefix = $this->multisite ? get_current_blog_id() . ':' : '';
555 $this->cache_table_name = self::OBJECT_CACHE_TABLE;
556 $this->flags_table_name = self::OBJECT_FLAGS_TABLE;
557 $this->noexpire_timestamp_offset = self::NOEXPIRE_TIMESTAMP_OFFSET;
558 $this->open_connection();
559
560 /* If wp-cli code cached something into SQLite, clear the APCu cache because it's stale. */
561 if ( $this->apcu_active && $this->clear_flag() ) {
562 $this->apcu_clear_cache();
563 }
564 }
565
566 /**
567 * Defer the closing of the SQLite connection to another destructor.
568 *
569 * This is needed because some plugins set transients in their object destructors.
570 */
571 public function __destruct() {
572 global $sqlite_object_cache_reaper;
573 $sqlite_object_cache_reaper = new SQLite_Object_Cache_Reaper( $this->sqlite );
574 }
575
576 /**
577 * Convert a list of integers into a list of runs: consecutive integers.
578 *
579 * Runs expand to include up to $erode_gaps extra integers, to make
580 * fewer, longer runs. (Each run turns into a single database query,
581 * so fewer of them is better.)
582 *
583 * @param int[] $intkeys List of integers. This can contain duplicate values.
584 * @param int $erode_gaps Combine runs separated by this or fewer integers.
585 *
586 * @return array Associative array with elements start => end
587 */
588 private function runs( &$intkeys, $erode_gaps = 2 ) {
589 if ( 0 === count( $intkeys ) ) {
590 return array();
591 }
592 sort( $intkeys, SORT_NUMERIC );
593 $previous = $intkeys[0];
594 $runstart = $previous;
595 $runs = array();
596 foreach ( $intkeys as $intkey ) {
597 if ( $intkey > $previous + 1 + $erode_gaps ) {
598 $runs[ $runstart ] = $previous;
599 $runstart = $intkey;
600 }
601 $previous = $intkey;
602 }
603 if ( null !== $runstart ) {
604 $runs[ $runstart ] = $previous;
605 }
606
607 return $runs;
608 }
609
610 /**
611 * Create the pathname for the sqlite database.
612 *
613 * This is based on WP_SQLITE_OBJECT_CACHE_DB_FILE, WP_CACHE_KEY_SALT,
614 * and whether igbinary is available.
615 * It may have -wal and -shm appended to it by the SQLite engine.
616 *
617 * @return string Full filesystem pathname for SQLite database.
618 */
619 private function create_database_path() {
620
621 $result = defined( 'WP_SQLITE_OBJECT_CACHE_DB_FILE' )
622 ? WP_SQLITE_OBJECT_CACHE_DB_FILE
623 : WP_CONTENT_DIR . '/' . self::SQLITE_FILENAME;
624
625 $salt = $this->salt;
626 $salt .= $this->has_igbinary ? '' : '-a';
627
628 if ( strlen( $salt ) > 0 ) {
629 $splits = explode( '.', $result );
630 if ( count( $splits ) >= 2 && 'sqlite' === $splits [ count( $splits ) - 1 ] ) {
631 $splits[ count( $splits ) - 1 ] = $salt;
632 $splits [] = 'sqlite';
633 $result = implode( '.', $splits );
634 } else {
635 $result .= '.' . $salt . '.sqlite';
636 }
637 }
638
639 $directory = dirname( $result );
640 if ( ! wp_is_writable( $directory ) ) {
641 $message = sprintf( 'The SQLite Object Cache cannot be activated because the %s directory is not writable.', $directory );
642 WP_Object_Cache::drop_dead( $message );
643 }
644
645 return $result;
646 }
647
648 /**
649 * @param string|null $msg
650 *
651 * @return void
652 */
653 public static function drop_dead( $msg = null ) {
654 wp_die( esc_html( $msg ?: 'The SQLite Object Cache temporarily failed. Please try again now.' ) );
655 }
656
657 /**
658 * Log an error.
659 *
660 * @param string $msg
661 * @param Exception $exception
662 *
663 * @return void
664 */
665 private function error_log( $msg, $exception = null ) {
666 $log_exception = ! ! $exception;
667 $server_software = 'unk';
668 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
669 if ( isset( $_SERVER['SERVER_SOFTWARE'] ) && is_string( $_SERVER['SERVER_SOFTWARE'] ) ) {
670 $server_software = $_SERVER['SERVER_SOFTWARE'];
671 }
672 // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
673 $msgs = array();
674 $msgs [] = 'SQLite Object Cache:';
675 $msgs [] = $this->dropin_version;
676 $msgs [] = 'SQLite:';
677 $msgs [] = $this->sqlite_get_version();
678 $msgs [] = $this->has_igbinary ? 'igbinary' : 'no igbinary';
679 $msgs [] = $this->apcu_active ? 'APCu active' : 'APCu inactive';
680 $msgs [] = 'php:';
681 $msgs [] = PHP_VERSION;
682 $msgs [] = 'server:';
683 $msgs [] = esc_html( $server_software );
684 $msgs [] = $msg;
685 if ( $this->sqlite ) {
686 if ( $this->sqlite->lastErrorMsg() ) {
687 $msgs [] = $this->sqlite->lastErrorMsg();
688 $msgs [] = '(' . $this->sqlite->lastErrorCode() . ')';
689 $log_exception = $log_exception && $this->sqlite->lastErrorMsg() !== $exception->getMessage();
690 }
691 }
692 if ( $log_exception ) {
693 $msgs[] = $exception->getMessage();
694 $msgs [] = '(' . $exception->getCode() . ')';
695 $msgs [] = $exception->getTraceAsString();
696 }
697 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
698 error_log( implode( ' ', $msgs ) );
699 }
700
701 /**
702 * Open SQLite3 connection.
703 * @return void
704 */
705 private function open_connection() {
706 if ( $this->sqlite ) {
707 return;
708 }
709 $retries = 3;
710 while ( $retries -- > 0 ) {
711 try {
712 $this->actual_open_connection();
713
714 return;
715 } catch ( Exception $ex ) {
716 /* something went wrong opening */
717 $this->error_log( 'open_connection failure', $ex );
718 $this->delete_offending_files( $retries );
719 }
720 }
721 }
722
723 /**
724 * Open SQLite3 connection.
725 *
726 * @return void
727 * @throws Exception Announce SQLite failure.
728 */
729 private function actual_open_connection() {
730 $start = hrtime( true );
731
732 if ( defined ( 'PHP_OS') || 0 !== stripos(PHP_OS, 'WIN')) {
733 /* Not Windows: Create file manually to set correct file permissions and make the DB group writable, for WP-CLI's sake */
734 if ( ! file_exists( $this->sqlite_path ) ) {
735 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch
736 touch( $this->sqlite_path );
737 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
738 chmod( $this->sqlite_path, 0664 );
739 }
740 }
741
742 $this->sqlite = new SQLite3( $this->sqlite_path, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, '' );
743 $this->sqlite->enableExceptions( true );
744 $this->sqlite->busyTimeout( $this->sqlite_timeout );
745
746 /* Set some initial pragma stuff.
747 * Notice we sometimes use a journal mode (MEMORY) that risks database corruption.
748 * That's OK, because it's faster, and because we have an error
749 * recovery procedure that deletes and recreates a corrupt database file.
750 */
751 $this->sqlite->exec( 'PRAGMA page_size = 4096' );
752 if ( $this->mmap_size ) {
753 $this->sqlite->exec( 'PRAGMA mmap_size = ' . $this->mmap_size );
754 }
755 $this->sqlite->exec( 'PRAGMA synchronous = OFF' );
756 $this->sqlite->exec( "PRAGMA journal_mode = $this->sqlite_journal_mode" );
757 $this->sqlite->exec( "PRAGMA encoding = 'UTF-8'" );
758 $this->sqlite->exec( 'PRAGMA case_sensitive_like = true' );
759 $this->create_object_cache_tables( );
760 $this->prepare_statements( $this->cache_table_name );
761
762 $this->open_time = hrtime( true ) - $start;
763 }
764
765 /**
766 * Set group type array
767 *
768 * @return void
769 */
770 protected function cache_group_types() {
771 foreach ( $this->global_groups as $group ) {
772 $this->group_type[ $group ] = 'global';
773 }
774
775 foreach ( $this->unflushable_groups as $group ) {
776 $this->group_type[ $group ] = 'unflushable';
777 }
778
779 foreach ( $this->ignored_groups as $group ) {
780 $this->group_type[ $group ] = 'ignored';
781 }
782 }
783
784 /**
785 * Do the necessary Data Definition Language work, for the cache table and flags table
786 *
787 * We use a single name column comprising group|key in one text string.
788 * Why?
789 * In recent versions of SQLite, it can serve as a clustered-index simple primary key.
790 * SQLite's ANALYZE facilty only builds query - planner stats for the first column of composite keys .
791 *
792 * "groups" are all text .
793 *
794 * "keys" are sometimes alphanumeric text and sometimes integers . So, they are all treated as text
795 * in the name column of the database .
796 *
797 * Now, range scanning( BETWEEN ) is a hassle in get_multiple, especially when using
798 * get_multiple to retrieve a range of keys from a group.
799 *
800 * @return void
801 * @throws Exception If something fails .
802 * @noinspection SqlResolve
803 */
804 private function create_object_cache_tables() {
805 $this->sqlite->exec( 'BEGIN' );
806 /* does our table exist? */
807 $query = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$this->cache_table_name';";
808 if ( 0 === $this->sqlite->querySingle( $query ) ) {
809 /* later versions of SQLite3 have clustered primary keys, "WITHOUT ROWID" */
810 $uses_rowid = version_compare( $this->sqlite_get_version(), '3.8.2' ) < 0;
811 if ( $uses_rowid ) {
812 /* @noinspection SqlIdentifier */
813 $t = "
814 CREATE TABLE IF NOT EXISTS $this->cache_table_name (
815 name TEXT NOT NULL COLLATE BINARY,
816 expires INT,
817 value BLOB
818 );
819 CREATE UNIQUE INDEX IF NOT EXISTS cache_name ON $this->cache_table_name (name);
820 CREATE INDEX IF NOT EXISTS expires ON $this->cache_table_name (expires);";
821 } else {
822 /* @noinspection SqlIdentifier */
823 $t = "
824 CREATE TABLE IF NOT EXISTS $this->cache_table_name (
825 name TEXT NOT NULL PRIMARY KEY COLLATE BINARY,
826 expires INT,
827 value BLOB
828 ) WITHOUT ROWID;
829 CREATE INDEX IF NOT EXISTS expires ON $this->cache_table_name (expires);";
830 }
831 $this->sqlite->exec( $t );
832
833 if ( $uses_rowid ) {
834 /* @noinspection SqlIdentifier */
835 $t = "
836 CREATE TABLE IF NOT EXISTS $this->flags_table_name (
837 name TEXT NOT NULL COLLATE BINARY
838 );
839 CREATE UNIQUE INDEX IF NOT EXISTS flags_name ON $this->flags_table_name (name);";
840 } else {
841 /* @noinspection SqlIdentifier */
842 $t = "
843 CREATE TABLE IF NOT EXISTS $this->flags_table_name (
844 name TEXT NOT NULL PRIMARY KEY COLLATE BINARY
845 ) WITHOUT ROWID;";
846 }
847 $this->sqlite->exec( $t );
848
849 /* Put the drop-in's version number in the SQLite file, for troubleshooting. */
850 $version = str_replace( '.', '0', $this->dropin_version );
851 if ( is_numeric( $version ) ) {
852 $this->sqlite->exec( "PRAGMA user_version=" . ( (int) $version ) . ";" );
853 }
854 /* Creating SQLite tables; clear APCu at the same time. */
855 $this->apcu_clear_cache();
856 }
857 $this->sqlite->exec( 'COMMIT' );
858 }
859
860 /**
861 * Do the necessary Data Definition Language work.
862 *
863 * @param string $tbl The name of the table.
864 *
865 * @return void
866 * @throws Exception If something fails.
867 * @noinspection SqlResolve
868 */
869 private function maybe_create_stats_table( $tbl ) {
870 $this->sqlite->exec( 'BEGIN' );
871 /* Does our table exist? */
872 $q = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name = '$tbl';";
873 $r = $this->sqlite->querySingle( $q );
874 if ( 0 === $r ) {
875 /* @noinspection SqlIdentifier */
876 $t = "
877 CREATE TABLE IF NOT EXISTS $tbl (
878 value BLOB,
879 timestamp INT
880 );
881 CREATE INDEX IF NOT EXISTS expires ON $tbl (timestamp);";
882 $this->sqlite->exec( $t );
883 }
884 $this->sqlite->exec( 'COMMIT' );
885 }
886
887 /**
888 * Create the prepared statements to use.
889 *
890 * @param string $tbl Table name.
891 *
892 * @return void
893 * @throws Exception Announce failure.
894 * @noinspection SqlResolve
895 */
896 private function prepare_statements( $tbl ) {
897 $now = $this->start_time;
898 $this->getone_stmt =
899 $this->sqlite->prepare( "SELECT value, expires FROM $tbl WHERE name = :name AND expires >= $now;" );
900 $this->getrange_stmt =
901 $this->sqlite->prepare( "SELECT name, value, expires FROM $tbl WHERE name BETWEEN :first AND :last AND expires >= $now;" );
902 $this->deleteone_stmt = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name = :name;" );
903 $this->deletegroup_stmt = $this->sqlite->prepare( "DELETE FROM $tbl WHERE name LIKE :group || '%';" );
904 /*
905 * Some versions of SQLite3 built into php predate the 3.38 advent of unixepoch() (2022-02-22).
906 * And, others predate the 3.24 advent of UPSERT (that is, ON CONFLICT) syntax.
907 * In that case we have to do attempt-update then insert to get updates to work. Sigh.
908 */
909 $has_upsert = version_compare( $this->sqlite_get_version(), '3.24', 'ge' );
910 if ( $has_upsert ) {
911 $this->upsertone_stmt =
912 $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires) ON CONFLICT(name) DO UPDATE SET value=excluded.value, expires=excluded.expires;" );
913 } else {
914 $this->insertone_stmt =
915 $this->sqlite->prepare( "INSERT INTO $tbl (name, value, expires) VALUES (:name, :value, $now + :expires);" );
916 $this->updateone_stmt =
917 $this->sqlite->prepare( "UPDATE $tbl SET value = :value, expires = $now + :expires WHERE name = :name;" );
918 }
919 }
920
921 /**
922 * Serialize data for persistence if need be. Use igbinary if available.
923 *
924 * @param mixed $data To be serialized.
925 *
926 * @return string|mixed Data ready for dbms insertion.
927 */
928 private function encode( $data ) {
929 return $this->has_igbinary
930 ? igbinary_serialize( $data )
931 : maybe_serialize( $data );
932 }
933
934
935 /**
936 * Unserialize persistend data. Use igbinary if available.
937 *
938 * @param mixed $data To be unserialized.
939 *
940 * @return string|mixed Data ready for use.
941 */
942 private function decode( $data ) {
943 return $this->has_igbinary
944 ? igbinary_unserialize( $data )
945 : maybe_unserialize( $data );
946 }
947
948 /**
949 * @param mixed $data The serialized data to reconsititute
950 *
951 * @return mixed|string The data, cloned if an object.
952 */
953 private function reconstitute( $data ) {
954 $ret = $this->decode( $data );
955 return is_object( $ret ) ? clone $ret : $ret;
956 }
957
958 /**
959 * Determine whether we can use SQLite3.
960 *
961 * @return bool|string true, or an error message.
962 */
963 public static function has_sqlite() {
964
965 if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
966 return 'The SQLite Object Cache cannot be activated because the SQLite3 extension is not loaded.';
967 }
968
969 return true;
970 }
971
972 /**
973 * Set the monitoring options for the SQLite cache.
974 *
975 * Options in array [
976 * 'capture' => (bool)
977 * 'resolution' => how often in seconds (float)
978 * 'lifetime' => how long until entries expire in seconds (int)
979 * 'verbose' => (bool) capture extra stuff.
980 * ]
981 *
982 * @param array $options Option list.
983 *
984 * @return void
985 */
986 public function set_sqlite_monitoring_options( $options ) {
987 $this->monitoring_options = $options;
988 }
989
990 /**
991 * Is recording this performance sample appropriate.
992 *
993 * We decide to take a performance sample based upon:
994 * -- the sqlite_object_cache_settings option existing.
995 * -- $option.capture having the 'on' value.
996 * -- $option.samplerate >= 100 or samplerate greater than a random number.
997 *
998 * @return bool True if this sample should be recorded.
999 */
1000 private function is_sample() {
1001 $options = get_option( 'sqlite_object_cache_settings', 'missing_option' );
1002 if ( 'missing_option' === $options ) {
1003 /* set an absent option to the empty array, so we don't repeatedly hammer the cache looking for a missing option */
1004 update_option( 'sqlite_object_cache_settings', array(), true );
1005
1006 return false;
1007 }
1008 if ( is_array( $options ) && array_key_exists( 'capture', $options ) && 'on' === $options['capture'] ) {
1009 if ( array_key_exists( 'samplerate', $options ) && is_numeric( $options['samplerate'] ) ) {
1010 /* samplerate is a percentage likelihood in the option setting */
1011 $samplerate = $options['samplerate'];
1012 if ( $samplerate > 0 ) {
1013 /* a random sample at $samplerate */
1014 if ( $samplerate >= 100 ) {
1015 return true;
1016 }
1017 // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand
1018 return ( $samplerate * 10000 ) > rand( 1, 1000000 );
1019 }
1020 }
1021 }
1022
1023 return false;
1024 }
1025
1026 /**
1027 * Capture statistics if need be. Leave the connection open for late-arriving cache operations.
1028 *
1029 * @return bool
1030 */
1031 public function close() {
1032 if ( $this->sqlite ) {
1033 if ( $this->is_sample() ) {
1034 $this->capture( $this->monitoring_options );
1035 }
1036 /*
1037 * Once in a while checkpoint the whole WAL log, so it doesn't grow without bound on a busy site.
1038 * One in four times we do this, truncate (zero the length of) the WAL log file */
1039 // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand
1040 $r = rand( 1, $this->checkpoint_freq );
1041 if ( 1 === $r ) {
1042 $this->checkpoint( 'TRUNCATE' );
1043 } else if ( $r <= 4 ) {
1044 $this->checkpoint();
1045 }
1046 }
1047 return true;
1048 }
1049
1050 /**
1051 * Remove statistics entries from the cache
1052 *
1053 * @param int|null $age Number of seconds' worth to retain. Default: retain none.
1054 *
1055 * @return void
1056 */
1057 public function sqlite_reset_statistics( $age = null ) {
1058
1059 try {
1060 $object_stats = self::OBJECT_STATS_TABLE;
1061 $this->maybe_create_stats_table( $object_stats );
1062 if ( ! is_numeric( $age ) ) {
1063 /* @noinspection SqlWithoutWhere */
1064 $sql = "DELETE FROM $object_stats;";
1065 $this->sqlite->exec( $sql );
1066 } else {
1067 $expires = (int) ( $this->start_time - $age );
1068 $limit = self::TRANSACTION_SIZE_LIMIT;
1069 $hits = $limit;
1070 while ( $hits >= $limit ) {
1071 /* @noinspection SqlResolve */
1072 $sql = "DELETE FROM $object_stats WHERE timestamp IN (SELECT timestamp FROM $object_stats WHERE timestamp < $expires LIMIT $limit);";
1073 $this->sqlite->exec( $sql );
1074 $hits = $this->sqlite->changes();
1075 }
1076 }
1077 } catch ( Exception $ex ) {
1078 $this->error_log( 'SQLite Object Cache exception resetting statistics. ', $ex );
1079 }
1080 }
1081
1082 /**
1083 * Remove old entries.
1084 *
1085 * @return boolean True if any items were removed.
1086 * @noinspection SqlResolve
1087 */
1088 public function sqlite_remove_expired() {
1089 $items_removed = 0;
1090 try {
1091 $limit = self::TRANSACTION_SIZE_LIMIT;
1092 $hit = $limit;
1093
1094 /* Remove items with definite expirations, like transients */
1095 $sql = 'DELETE FROM ' . $this->cache_table_name . ' WHERE name IN (SELECT name FROM ' . $this->cache_table_name . ' WHERE expires <= ' . $this->start_time . ' LIMIT ' . $limit . ')';
1096
1097 while ( $hit >= $limit ) {
1098 $this->sqlite->exec( $sql );
1099 $hit = $this->sqlite->changes();
1100 $items_removed += $hit;
1101 }
1102 } catch ( Exception $ex ) {
1103 $this->error_log( 'sqlite_remove_expired', $ex );
1104 }
1105 $this->checkpoint( 'TRUNCATE' );
1106
1107 return $items_removed > 0;
1108 }
1109
1110 /**
1111 * Get the size of the cache database.
1112 *
1113 * @return int Size of current cache database in bytes.
1114 */
1115 public function sqlite_get_size() {
1116 $object_cache = self::OBJECT_CACHE_TABLE;
1117 $sql = "SELECT SUM(LENGTH(value) + LENGTH(name)) length FROM $object_cache";
1118 $stmt = $this->sqlite->prepare( $sql );
1119 $resultset = $stmt->execute();
1120 $row = $resultset->fetchArray( SQLITE3_NUM );
1121 $result = $row[0];
1122 $resultset->finalize();
1123
1124 return (int) $result;
1125 }
1126
1127 /**
1128 * Read object names, sizes, expirations from cache, ordered by expiration time oldest first.
1129 *
1130 * @param $timestamps true If the timestamps returned should be expirations, false means raw
1131 *
1132 * @return Generator of name/length/timestamp rows.
1133 * @throws Exception Announce SQLite failure.
1134 * @noinspection SqlResolve
1135 */
1136 public function &sqlite_load_usages( $timestamps = true ) {
1137 $object_cache = self::OBJECT_CACHE_TABLE;
1138 $offset = $this->noexpire_timestamp_offset;
1139 $sql = "SELECT name, LENGTH(value) + LENGTH(name) length, expires FROM $object_cache";
1140 $stmt = $this->sqlite->prepare( $sql );
1141 try {
1142 $resultset = $stmt->execute();
1143 while ( true ) {
1144 $row = $resultset->fetchArray( SQLITE3_ASSOC );
1145 if ( ! $row ) {
1146 break;
1147 }
1148 $row = (object) $row;
1149 if ( $timestamps ) {
1150 $expires = $row->expires;
1151 if ( $expires >= self::NOEXPIRE_TIMESTAMP_OFFSET ) {
1152 $expires -= self::NOEXPIRE_TIMESTAMP_OFFSET;
1153 }
1154 $row->expires = $expires;
1155 }
1156 yield $row;
1157 }
1158 } finally {
1159 $resultset->finalize();
1160 }
1161 }
1162
1163 public function sqlite_sizes() {
1164 $object_stats = self::OBJECT_STATS_TABLE;
1165 $this->maybe_create_stats_table( $object_stats );
1166
1167 $items = array(
1168 'page_size' => 'PRAGMA page_size;',
1169 'free_pages' => 'PRAGMA freelist_count;',
1170 'total_pages' => 'PRAGMA page_count;',
1171 'stats_items' => "SELECT COUNT(value) FROM $object_stats;",
1172 'stats_size' => "SELECT SUM(LENGTH(value)+ 4) FROM $object_stats;",
1173 'mmap_size' => "PRAGMA mmap_size;",
1174 );
1175
1176 $result = array();
1177 foreach ( $items as $item => $query ) {
1178 $stmt = $this->sqlite->prepare( $query );
1179 $resultset = $stmt->execute();
1180 $row = $resultset->fetchArray( SQLITE3_NUM );
1181 $val = (int) $row[0];
1182 $resultset->finalize();
1183 $result [ $item ] = $val;
1184 }
1185
1186 return $result;
1187 }
1188
1189 /**
1190 * Read timestamps and object sizes of non-expiring items, oldest first, in buckets of 16 seconds.
1191 *
1192 * Object sizes are the summed lengths of name, value, and timestamp, and ignore index overhead.
1193 *
1194 * @return SQLite3Result Resultset containing length/timestamp rows.
1195 * @throws Exception Announce SQLite failure.
1196 * @noinspection SqlResolve
1197 */
1198 private function sqlite_load_sizes() {
1199 $object_cache = self::OBJECT_CACHE_TABLE;
1200 $offset = $this->noexpire_timestamp_offset;
1201 $sql =
1202 "SELECT SUM(LENGTH(value) + LENGTH(name) + 6) length, ((expires+15)/16)*16 expires FROM $object_cache WHERE expires >= $offset GROUP BY ((expires+15)/16)*16 ORDER BY ((expires+15)/16)*16";
1203 $stmt = $this->sqlite->prepare( $sql );
1204
1205 return $stmt->execute();
1206 }
1207
1208 /**
1209 * Read rows from the stored statistics.
1210 *
1211 * @return Generator
1212 * @throws Exception Announce SQLite failure.
1213 * @noinspection SqlResolve
1214 */
1215 public function sqlite_load_statistics() {
1216 $object_stats = self::OBJECT_STATS_TABLE;
1217 $this->maybe_create_stats_table( $object_stats );
1218 $sql = "SELECT value FROM $object_stats;";
1219 $stmt = $this->sqlite->prepare( $sql );
1220 try {
1221 $resultset = $stmt->execute();
1222 while ( true ) {
1223 $row = $resultset->fetchArray( SQLITE3_NUM );
1224 if ( ! $row ) {
1225 break;
1226 }
1227 $value = $this->decode( $row[0] );
1228 yield (object) $value;
1229 }
1230 } finally {
1231 $resultset->finalize();
1232 }
1233 }
1234
1235 /**
1236 * Do the performance-capture operation.
1237 *
1238 * Put a row named sqlite_object_cache.mon.123456 into sqlite containing the raw data.
1239 *
1240 * @param array $options Contents of $this->monitoring_options.
1241 *
1242 * @return void
1243 * @noinspection SqlResolve
1244 */
1245 private function capture( $options ) {
1246 $now = microtime( true );
1247 global $wpdb;
1248 $record = array(
1249 'time' => $now,
1250 'elapsed' => hrtime( true ) - $this->start_hrtime,
1251 'RAMhits' => $this->cache_hits,
1252 'RAMmisses' => $this->cache_misses,
1253 'DISKhits' => $this->persistent_hits,
1254 'DISKmisses' => $this->persistent_misses,
1255 'open' => $this->open_time,
1256 'selects' => $this->select_times,
1257 'gets' => $this->get_times,
1258 'get_multiples' => $this->get_multiple_times,
1259 'get_multiple_keys' => $this->get_multiple_keys,
1260 'inserts' => $this->insert_times,
1261 'deletes' => $this->delete_times,
1262 'checkpoints' => $this->checkpoint_times,
1263 'DBMSqueries' => $wpdb->num_queries,
1264 'RAM' => memory_get_peak_usage( true ),
1265 'APCuhits' => $this->apcu_hits,
1266 'APCumisses' => $this->apcu_misses,
1267 'APCufetchhit' => $this->apcu_fetch_hit_times,
1268 'APCufetchmiss' => $this->apcu_fetch_miss_times,
1269 'APCustore' => $this->apcu_store_times,
1270
1271 );
1272 $object_stats = self::OBJECT_STATS_TABLE;
1273 try {
1274 $this->maybe_create_stats_table( $object_stats );
1275 $sql =
1276 "INSERT INTO $object_stats (value, timestamp) VALUES (:value, :timestamp);";
1277 $stmt = $this->sqlite->prepare( $sql );
1278 $stmt->bindValue( ':value', $this->encode( $record ), SQLITE3_BLOB );
1279 $stmt->bindValue( ':timestamp', $this->start_time, SQLITE3_INTEGER );
1280 $result = $stmt->execute();
1281 $result->finalize();
1282 } catch ( Exception $ex ) {
1283 $this->error_log( 'error capturing performance stats, skipping.', $ex );
1284 }
1285 unset( $record, $stmt );
1286 }
1287
1288 /** Get the version of the drop-in.
1289 *
1290 * @return string drop-in version.
1291 */
1292 public function dropin_get_version() {
1293 return $this->dropin_version;
1294 }
1295
1296 /**
1297 * Get the version of SQLite in use.
1298 *
1299 * @return string
1300 */
1301 public function sqlite_get_version() {
1302 if ( $this->sqlite_version ) {
1303 return $this->sqlite_version;
1304 }
1305 $v = SQLite3::version();
1306 $this->sqlite_version = $v['versionString'];
1307
1308 return $this->sqlite_version;
1309 }
1310
1311 /**
1312 * Sets the list of groups not to be cached by Redis.
1313 *
1314 * @param array $groups List of groups that are to be ignored.
1315 */
1316 public function add_non_persistent_groups( $groups ) {
1317 /**
1318 * Filters list of groups to be added to {@see self::$ignored_groups}
1319 *
1320 * @param string[] $groups List of groups to be ignored.
1321 *
1322 * @since 2.1.7
1323 */
1324 $groups = apply_filters( 'sqlite_object_cache_add_non_persistent_groups', (array) $groups );
1325
1326 $this->ignored_groups = array_unique( array_merge( $this->ignored_groups, $groups ) );
1327 $this->cache_group_types();
1328 }
1329
1330 /**
1331 * Makes private properties readable for backward compatibility.
1332 *
1333 * @param string $name Property to get.
1334 *
1335 * @return mixed Property.
1336 * @since 4.0.0
1337 */
1338 public function __get( $name ) {
1339 return $this->$name;
1340 }
1341
1342 /**
1343 * Makes private properties settable for backward compatibility.
1344 *
1345 * @param string $name Property to set.
1346 * @param mixed $value Property value.
1347 *
1348 * @return mixed Newly-set property.
1349 * @since 4.0.0
1350 */
1351 public function __set( $name, $value ) {
1352 return $this->$name = $value;
1353 }
1354
1355 /**
1356 * Makes private properties checkable for backward compatibility.
1357 *
1358 * @param string $name Property to check if set.
1359 *
1360 * @return bool Whether the property is set.
1361 * @since 4.0.0
1362 */
1363 public function __isset( $name ) {
1364 return isset( $this->$name );
1365 }
1366
1367 /**
1368 * Makes private properties un-settable for backward compatibility.
1369 *
1370 * @param string $name Property to unset.
1371 *
1372 * @since 4.0.0
1373 */
1374 public function __unset( $name ) {
1375 unset( $this->$name );
1376 }
1377
1378 /**
1379 * Adds multiple values to the cache in one call.
1380 *
1381 * @param array $data Array of keys and values to be added.
1382 * @param string $group Optional. Where the cache contents are grouped. Default empty.
1383 * @param int $expire Optional. When to expire the cache contents, in seconds.
1384 * Default 0 (no expiration).
1385 *
1386 * @return bool[] Array of return values, grouped by key. Each value is either
1387 * true on success, or false if cache key and group already exist.
1388 * @since 6.0.0
1389 */
1390 public function add_multiple( array &$data, $group = '', $expire = 0 ) {
1391 if ( 0 === count( $data ) ) {
1392 return array();
1393 }
1394 $values = array();
1395 /* sort the array to reduce index page fragmentation */
1396 ksort( $data, SORT_NUMERIC );
1397 try {
1398 /* use a transaction to accelerate add_multiple */
1399 $this->transaction_active = true;
1400 $this->sqlite->exec( 'BEGIN' );
1401 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1402 foreach ( $data as $key => $value ) {
1403 $values[ $key ] = $this->add( $key, $value, $group, $expire );
1404 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1405 if ( -- $transaction_size <= 0 ) {
1406 $this->sqlite->exec( 'COMMIT' );
1407 $this->sqlite->exec( 'BEGIN' );
1408 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1409 }
1410 }
1411 $this->sqlite->exec( 'COMMIT' );
1412 $this->transaction_active = false;
1413 } catch ( Exception $ex ) {
1414 $this->error_log( 'add_multiple', $ex );
1415 $this->delete_offending_files();
1416 self::drop_dead();
1417 }
1418
1419 return $values;
1420 }
1421
1422 /**
1423 * Adds data to the cache if it doesn't already exist.
1424 *
1425 * @param int|string $key What to call the contents in the cache.
1426 * @param mixed $data The contents to store in the cache.
1427 * @param string $group Optional. Where to group the cache contents. Default 'default'.
1428 * @param int $expire Optional. When to expire the cache contents, in seconds.
1429 * Default 0 (no expiration).
1430 *
1431 * @return bool True on success, false if cache key and group already exist.
1432 * @throws Exception Announce database failure.
1433 * @since 2.0.0
1434 *
1435 * @uses WP_Object_Cache::cache_item_exists() Checks to see if the cache already has data.
1436 * @uses WP_Object_Cache::set() Sets the data after the checking the cache
1437 * contents existence.
1438 */
1439 public function add( $key, $data, $group = 'default', $expire = 0 ) {
1440 if ( wp_suspend_cache_addition() ) {
1441 return false;
1442 }
1443
1444 if ( ! $this->is_valid_key( $key ) ) {
1445 return false;
1446 }
1447
1448 $name = $this->normalize_name( $key, $group );
1449
1450 if ( $this->cache_item_not_exists( $name ) ) {
1451 return $this->set( $key, $data, $group, (int) $expire );
1452 }
1453
1454 return false;
1455 }
1456
1457 /**
1458 * Serves as a utility function to determine whether a key is valid.
1459 *
1460 * @param int|string $key Cache key to check for validity.
1461 *
1462 * @return bool Whether the key is valid.
1463 * @since 6.1.0
1464 */
1465 protected function is_valid_key( $key ) {
1466 if ( is_int( $key ) ) {
1467 return true;
1468 }
1469
1470 if ( is_string( $key ) && trim( $key ) !== '' ) {
1471 return true;
1472 }
1473
1474 $type = gettype( $key );
1475
1476 if ( ! function_exists( '__' ) ) {
1477 wp_load_translations_early();
1478 }
1479
1480 // phpcs:disable WordPress.WP.I18n.MissingArgDomain,WordPress.Security.EscapeOutput.OutputNotEscaped,WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
1481 $message =
1482 // Core I18n Phrases
1483 is_string( $key ) ? __( 'Cache key must not be an empty string.' )
1484 /* translators: This i18n string is from core. */
1485 : sprintf( __( 'Cache key must be integer or non-empty string, %s given.' ), $type );
1486 _doing_it_wrong( sprintf( '%s::%s', __CLASS__, debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 )[1]['function'] ), $message, '6.1.0' );
1487 //phpcs:enable WordPress.WP.I18n.MissingArgDomain,WordPress.Security.EscapeOutput.OutputNotEscaped,,WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
1488
1489 return false;
1490 }
1491
1492 /**
1493 * Determine whether a key exists in the cache.
1494 *
1495 * As a side-effect and optimization, copy the value from the SQLite store
1496 * to RAM if it exists in the SQLite store.
1497 *
1498 * @param int|string $name Cache key to check for existence.
1499 *
1500 * @return bool Whether the key exists in the cache for the given group.
1501 * @throws Exception Announce database failure.
1502 * @since 3.4.0
1503 */
1504 protected function cache_item_exists( $name ) {
1505 $exists = false;
1506
1507 if ( array_key_exists( $name, $this->not_in_persistent_cache ) ) {
1508 return false;
1509 }
1510 $val = $this->get_by_name( $name, $fetchsuccess );
1511 if ( $fetchsuccess ) {
1512 $this->cache[ $name ] = $val;
1513 $exists = true;
1514 $this->persistent_hits ++;
1515 unset( $this->not_in_persistent_cache[ $name ] );
1516 } else {
1517 $this->persistent_misses ++;
1518 $this->not_in_persistent_cache[ $name ] = true;
1519 }
1520
1521 return $exists;
1522 }
1523
1524 /**
1525 * Determine whether a key does not exist in the cache. either local or SQLite
1526 *
1527 * @param int|string $name Cache key to check for existence.
1528 *
1529 * @return bool Whether the key does not exists in the cache.
1530 * @throws Exception Announce database failure.
1531 * @since 3.4.0
1532 */
1533 protected function cache_item_not_exists( $name ) {
1534
1535 if ( array_key_exists( $name, $this->cache ) ) {
1536 return false;
1537 }
1538 if ( array_key_exists( $name, $this->not_in_persistent_cache ) ) {
1539 return true;
1540 }
1541
1542 return ! $this->cache_item_exists( $name );
1543 }
1544
1545 /**
1546 * Get one item from external cache.
1547 *
1548 * @param string $name Cache key.
1549 * @param bool|null $success Set to true if the item was found.
1550 *
1551 * @return mixed|null Cached item, cloned if an object. Null if not found. (Cached item can be false.)
1552 */
1553 private function get_by_name( $name, &$success ) {
1554 if ( $this->apcu_active ) {
1555 $astart = hrtime( true );
1556 $data = apcu_fetch( $this->apcusalt . $name, $fetchsuccess );
1557 if ( $fetchsuccess ) {
1558 if ( is_object( $data ) ) {
1559 $data = clone $data;
1560 }
1561 ++ $this->apcu_hits;
1562 $this->apcu_fetch_hit_times[] = hrtime( true ) - $astart;
1563 $success = true;
1564 return $data;
1565 } else {
1566 ++ $this->apcu_misses;
1567 $this->apcu_fetch_miss_times[] = hrtime( true ) - $astart;
1568 }
1569 }
1570 $data = null;
1571 $fetchsuccess = false;
1572 $expires = 0;
1573 $start = hrtime( true );
1574 try {
1575 $stmt = $this->getone_stmt;
1576 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1577 $result = $stmt->execute();
1578 $row = $result->fetchArray( SQLITE3_NUM );
1579 $result->finalize();
1580 if ( false !== $row ) {
1581 $fetchsuccess = true;
1582 $expires = $row[1];
1583 if ( $expires < self::NOEXPIRE_TIMESTAMP_OFFSET ) {
1584 /* Item has explicit expiration time. */
1585 $expires -= $this->start_time;
1586 if ( $expires <= 0 ) {
1587 /* Item has expired */
1588 unset ( $this->cache[ $name ] );
1589 $this->not_in_persistent_cache [ $name ] = true;
1590 $success = false;
1591 return null;
1592 }
1593 } else {
1594 $expires = DAY_IN_SECONDS;
1595 }
1596 $data = $this->reconstitute( $row[0] );
1597 }
1598 if ( $fetchsuccess ) {
1599 /* Pull item into APCu */
1600 if ( $this->apcu_active ) {
1601 $astart = hrtime( true );
1602 apcu_store( $this->apcusalt . $name, $data, $expires );
1603 $this->apcu_store_times[] = hrtime( true ) - $astart;
1604 }
1605
1606 unset ( $this->not_in_persistent_cache[ $name ] );
1607 } else {
1608 $this->not_in_persistent_cache [ $name ] = true;
1609 }
1610 } catch ( Exception $ex ) {
1611 unset( $this->not_in_persistent_cache [ $name ] );
1612 $this->error_log( 'get_by_name', $ex );
1613 $this->delete_offending_files();
1614 self::drop_dead();
1615 }
1616 $this->select_times[] = hrtime( true ) - $start;
1617
1618 $success = $fetchsuccess;
1619 return $data;
1620 }
1621
1622 /**
1623 * Get the expiration time of an item
1624 *
1625 * @param string $name Cache key.
1626 *
1627 * @return boolean | integer False if no such item, the expiration ttl (from now)), or zero if no expireation.
1628 */
1629 private function get_expiration_by_name( $name ) {
1630 try {
1631 $stmt = $this->getone_stmt;
1632 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1633 $result = $stmt->execute();
1634 $row = $result->fetchArray( SQLITE3_NUM );
1635 $result->finalize();
1636 if ( false !== $row ) {
1637 $expires = $row[1];
1638 if ( $expires < self::NOEXPIRE_TIMESTAMP_OFFSET ) {
1639 /* Item has explicit expiration time. */
1640 $expires -= $this->start_time;
1641 if ( $expires <= 0 ) {
1642 if ( $this->apcu_active ) {
1643 apcu_delete( $this->apcusalt . $name );
1644 }
1645 return false;
1646 } else {
1647 return $expires;
1648 }
1649 } else {
1650 return 0;
1651 }
1652 } else {
1653 return false;
1654 }
1655 } catch ( Exception $ex ) {
1656 unset( $this->not_in_persistent_cache [ $name ] );
1657 $this->error_log( 'get_by_name', $ex );
1658 $this->delete_offending_files();
1659 self::drop_dead();
1660 }
1661 return false;
1662 }
1663
1664 /**
1665 * Sets the data contents into the cache.
1666 *
1667 * The cache contents are grouped by the $group parameter followed by the
1668 * $key. This allows for duplicate IDs in unique groups. Therefore, naming of
1669 * the group should be used with care and should follow normal function
1670 * naming guidelines outside of core WordPress usage.
1671 *
1672 * The $expire parameter is not used, because the cache will automatically
1673 * expire for each time a page is accessed and PHP finishes. The method is
1674 * more for cache plugins which use files.
1675 *
1676 * @param int|string $key What to call the contents in the cache.
1677 * @param mixed $data The contents to store in the cache. Objects are cloned before being stored.
1678 * @param string $group Optional. Where to group the cache contents. Default 'default'.
1679 * @param int $expire Optional. Not used.
1680 *
1681 * @return bool True if contents were set, false if key is invalid.
1682 * @since 2.0.0
1683 * @since 6.1.0 Returns false if cache key is invalid.
1684 *
1685 */
1686 public function set( $key, $data, $group = 'default', $expire = 0 ) {
1687
1688 if ( ! $this->is_valid_key( $key ) ) {
1689 return false;
1690 }
1691
1692 $name = $this->normalize_name( $key, $group );
1693
1694 if ( is_object( $data ) ) {
1695 $data = clone $data;
1696 }
1697
1698 $this->cache[ $name ] = $data;
1699
1700 if ( $this->is_ignored_group( $group ) ) {
1701 return true;
1702 }
1703
1704 $start = hrtime( true );
1705 $this->put_by_name( $name, $data, $expire );
1706 $this->insert_times[] = hrtime( true ) - $start;
1707
1708 return true;
1709 }
1710
1711 /**
1712 * Write to the persistent cache, with timeout retry.
1713 *
1714 * @param string $name What to call the contents in the cache.
1715 * @param mixed $data The contents to store in the cache.
1716 * @param int $expire Optional.
1717 *
1718 * @return void
1719 */
1720 private function put_by_name( $name, $data, $expire ) {
1721 $exception = null;
1722 $expires = $expire ?: $this->noexpire_timestamp_offset;
1723 $retries = 3;
1724 while ( $retries -- > 0 ) {
1725 try {
1726 $this->actual_put_by_name( $name, $this->encode( $data ), $expires );
1727 unset( $this->not_in_persistent_cache[ $name ] );
1728 if ( $this->apcu_active ) {
1729 $astart = hrtime( true );
1730 apcu_store( $this->apcusalt . $name, $data, $expire ?: DAY_IN_SECONDS );
1731 $this->apcu_store_times[] = hrtime( true ) - $astart;
1732 }
1733 return;
1734 } catch ( Exception $ex ) {
1735 $exception = $ex;
1736 if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
1737 break;
1738 }
1739 }
1740 sleep( 1 );
1741 }
1742 if ( $exception ) {
1743 $this->error_log( 'put_by_name', $exception );
1744 $this->delete_offending_files();
1745 self::drop_dead();
1746 }
1747 }
1748
1749 /**
1750 * Actually write to the cache.
1751 *
1752 * @param string $name What to call the contents in the cache.
1753 * @param string $value The seriolized value.
1754 * @param int $expires Expiration time.
1755 *
1756 * @return void
1757 */
1758 private function actual_put_by_name( $name, $value, $expires ) {
1759 if ( $this->apcu_supported ) {
1760 $this->set_flag();
1761 }
1762 if ( $this->upsertone_stmt ) {
1763 $stmt = $this->upsertone_stmt;
1764 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1765 $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1766 $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1767 $result = $stmt->execute();
1768 $result->finalize();
1769 } else {
1770 /* Pre-upsert version (pre- 3.24) of SQLite,
1771 * Need to try update, then do insert if need be.
1772 * Race conditions are possible, hence BEGIN / COMMIT
1773 */
1774 if ( ! $this->transaction_active ) {
1775 $this->sqlite->exec( 'BEGIN' );
1776 }
1777 $stmt = $this->updateone_stmt;
1778 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1779 $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1780 $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1781 $result = $stmt->execute();
1782 $result->finalize();
1783 if ( 0 === $this->sqlite->changes() ) {
1784 /* Updated zero rows, so we need an insert. */
1785 $stmt = $this->insertone_stmt;
1786 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
1787 $stmt->bindValue( ':value', $value, SQLITE3_BLOB );
1788 $stmt->bindValue( ':expires', $expires, SQLITE3_INTEGER );
1789 $result = $stmt->execute();
1790 $result->finalize();
1791
1792 }
1793 if ( ! $this->transaction_active ) {
1794 $this->sqlite->exec( 'COMMIT' );
1795 }
1796 }
1797 }
1798
1799 /**
1800 * Replaces the contents in the cache, if contents already exist.
1801 *
1802 * @param int|string $key What to call the contents in the cache.
1803 * @param mixed $data The contents to store in the cache.
1804 * @param string $group Optional. Where to group the cache contents. Default 'default'.
1805 * @param int $expire Optional. When to expire the cache contents, in seconds.
1806 * Default 0 (no expiration).
1807 *
1808 * @return bool True if contents were replaced, false if original value does not exist.
1809 * @see WP_Object_Cache::set()
1810 *
1811 * @since 2.0.0
1812 *
1813 */
1814 public function replace( $key, $data, $group = 'default', $expire = 0 ) {
1815 if ( ! $this->is_valid_key( $key ) ) {
1816 return false;
1817 }
1818
1819 $name = $this->normalize_name( $key, $data );
1820
1821 if ( $this->cache_item_not_exists( $name ) ) {
1822 return false;
1823 }
1824
1825 return $this->set( $key, $data, $group, (int) $expire );
1826 }
1827
1828 /**
1829 * Sets multiple values to the cache in one call.
1830 *
1831 * @param array $data Array of key and value to be set.
1832 * @param string $group Optional. Where the cache contents are grouped. Default empty.
1833 * @param int $expire Optional. When to expire the cache contents, in seconds.
1834 * Default 0 (no expiration).
1835 *
1836 * @return bool[] Array of return values, grouped by key. Each value is always true.
1837 * @since 6.0.0
1838 */
1839 public function set_multiple( array &$data, $group = '', $expire = 0 ) {
1840 if ( 0 === count( $data ) ) {
1841 return array();
1842 }
1843 $values = array();
1844 /* Sort the array to reduce index page fragmentation */
1845 ksort( $data, SORT_NUMERIC );
1846 try {
1847 /* use a transaction to accelerate set_multiple */
1848 $this->transaction_active = true;
1849 $this->sqlite->exec( 'BEGIN' );
1850 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1851
1852 foreach ( $data as $key => $value ) {
1853 $values[ $key ] = $this->set( $key, $value, $group, $expire );
1854 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
1855 if ( -- $transaction_size <= 0 ) {
1856 $this->sqlite->exec( 'COMMIT' );
1857 $this->sqlite->exec( 'BEGIN' );
1858 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1859 }
1860 }
1861 $this->sqlite->exec( 'COMMIT' );
1862 $this->transaction_active = false;
1863 } catch ( Exception $ex ) {
1864 $this->error_log( 'set_multiple', $ex );
1865 $this->delete_offending_files();
1866 self::drop_dead();
1867 }
1868
1869 return $values;
1870 }
1871
1872 /**
1873 * Retrieves multiple values from the cache in one call.
1874 *
1875 * @param string[]|int[] $input_keys
1876 * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
1877 * @param bool $force Optional. Whether to force an update of the local cache
1878 * from the persistent cache. Default false.
1879 *
1880 * @return array Array of return values, grouped by key. Each value is either
1881 * the cache contents on success, or false on failure. Objects are cloned
1882 * before putting them in the array.
1883 * @since 5.5.5
1884 */
1885 public function get_multiple( $input_keys, $group = 'default', $force = false ) {
1886 $values = array();
1887 if ( count( $input_keys ) <= 1 || $force ) {
1888 /* Send the degenerate get_multiple calls, and forced calls, to plain old get. That logic is simpler. */
1889 foreach ( $input_keys as $key ) {
1890 $values[ $key ] = $this->get( $key, $group, $force );
1891 }
1892
1893 return $values;
1894 }
1895 $start = hrtime( true );
1896
1897 $normalized = array();
1898 $keys_not_found = array();
1899 /* Find already-cached keys, pruning down the list of keys to fetch. */
1900 foreach ( $input_keys as $key ) {
1901 $name = $this->normalize_name( $key, $group );
1902 $normalized [ $key ] = $name;
1903 if ( array_key_exists( $name, $this->cache ) ) {
1904 $values [ $key ] = is_object( $this->cache[ $name ] )
1905 ? clone $this->cache[ $name ]
1906 : $this->cache[ $name ];
1907 ++ $this->cache_hits;
1908 } else {
1909 $keys_not_found[ $key ] = $name;
1910 $values[ $key ] = false;
1911 }
1912 }
1913
1914 /* Examine APCu cache for stashed items. */
1915 if ( $this->apcu_active && count( $keys_not_found ) > 0 ) {
1916 $keys_not_found_apcu = array();
1917 foreach ( $keys_not_found as $key => $name ) {
1918 //TODO this can get an array form of the fetch operation.
1919 $astart = hrtime( true );
1920 $val = apcu_fetch( $this->apcusalt . $name, $success );
1921 if ( $success ) {
1922 if ( is_object( $val ) ) {
1923 $val = clone $val;
1924 }
1925 ++ $this->apcu_hits;
1926 $this->apcu_fetch_hit_times[] = hrtime( true ) - $astart;
1927
1928 $values [ $key ] = $val;
1929 } else {
1930 ++ $this->apcu_misses;
1931 $this->apcu_fetch_miss_times[] = hrtime( true ) - $astart;
1932 $keys_not_found_apcu[ $key ] = $name;
1933 }
1934 }
1935 $keys_not_found = $keys_not_found_apcu;
1936 }
1937
1938 if ( count( $keys_not_found ) <= 1 ) {
1939 /* Degenerate case after fulfilment from RAM: handle as simple get */
1940 foreach ( $keys_not_found as $key => $name ) {
1941 $success = false;
1942 $data = $this->get_by_normalized_name( $name, $success );
1943 if ( $success ) {
1944 $values[ $key ] = $data;
1945 }
1946 }
1947
1948 $this->get_multiple_times[] = hrtime( true ) - $start;
1949 $this->get_multiple_keys [] = count( $input_keys );
1950 return $values;
1951 }
1952 /* split into alpha and numeric keys */
1953 $alphakeys = array();
1954 $intkeys = array();
1955 foreach ( $keys_not_found as $key => $name ) {
1956 if ( is_numeric( $key ) && (int) $key == $key && (int) $key > 0 && (int) $key <= $this->intkey_max ) {
1957 $intkeys [] = (int) $key;
1958 } else {
1959 $alphakeys [ $key ] = $name;
1960 }
1961 }
1962 try {
1963 /* Get the consecutive integer key runs */
1964 $runs = $this->runs( $intkeys, $this->erode_gaps );
1965
1966 /* use a transaction to accelerate get_multiple */
1967 $this->transaction_active = true;
1968 $this->sqlite->exec( 'BEGIN' );
1969 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
1970
1971 /* Start by loading the consecutive runs of int keys */
1972 foreach ( $runs as $first => $last ) {
1973 $stmt = $this->getrange_stmt;
1974 $stmt->bindValue( ':first', $normalized[ $first ], SQLITE3_TEXT );
1975 $stmt->bindValue( ':last', $normalized[ $last ], SQLITE3_TEXT );
1976 $resultset = $stmt->execute();
1977 while ( true ) {
1978 $row = $resultset->fetchArray( SQLITE3_NUM );
1979 if ( ! $row ) {
1980 break;
1981 }
1982 ++ $this->persistent_hits;
1983 $name = $row[0];
1984 $this->cache[ $name ] = $this->reconstitute( $row[1] );
1985
1986 $expires = $row[2];
1987 if ( $expires < self::NOEXPIRE_TIMESTAMP_OFFSET ) {
1988 $expires -= $this->start_time;
1989 } else {
1990 $expires = DAY_IN_SECONDS;
1991 }
1992 if ( $expires > 0 ) {
1993 /* Item has not expired */
1994 ++ $this->persistent_hits;
1995 $this->cache[ $name ] = $this->reconstitute( $row[1] );
1996 unset( $this->not_in_persistent_cache[ $name ] );
1997
1998 if ( $this->apcu_active ) {
1999 $astart = hrtime( true );
2000 apcu_store( $this->apcusalt . $name, $this->cache[ $name ], $expires );
2001 $this->apcu_store_times[] = hrtime( true ) - $astart;
2002
2003 }
2004 }
2005 }
2006 $resultset->finalize();
2007 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
2008 if ( -- $transaction_size <= 0 ) {
2009 $this->sqlite->exec( 'COMMIT' );
2010 $this->sqlite->exec( 'BEGIN' );
2011 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2012 }
2013 }
2014 /* Do the alpha keys, if any */
2015 foreach ( $alphakeys as $key => $name ) {
2016 if ( false === $values[ $key ] ) {
2017 $success = false;
2018 $data = $this->get_by_normalized_name( $name, $success );
2019 if ( $success ) {
2020 $values[ $key ] = $data;
2021 }
2022 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
2023 if ( -- $transaction_size <= 0 ) {
2024 $this->sqlite->exec( 'COMMIT' );
2025 $this->sqlite->exec( 'BEGIN' );
2026 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2027 }
2028 }
2029 }
2030 foreach ( $intkeys as $key ) {
2031 if ( false === $values[ $key ] ) {
2032 $success = false;
2033 $data = $this->get_by_normalized_name( $normalized[ $key ], $success );
2034 if ( $success ) {
2035 $values[ $key ] = $data;
2036 }
2037 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
2038 if ( -- $transaction_size <= 0 ) {
2039 $this->sqlite->exec( 'COMMIT' );
2040 $this->sqlite->exec( 'BEGIN' );
2041 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2042 }
2043 }
2044 }
2045 $this->sqlite->exec( 'COMMIT' );
2046 $this->transaction_active = false;
2047 } catch ( Exception $ex ) {
2048 $this->error_log( 'get_multiple', $ex );
2049 $this->delete_offending_files();
2050 self::drop_dead();
2051 }
2052 $this->get_multiple_keys [] = count( $input_keys );
2053 $this->get_multiple_times [] = hrtime( true ) - $start;
2054
2055 return $values;
2056 }
2057
2058 /**
2059 * Get the cache row name for a key and group.
2060 *
2061 * Notice that numeric keys have leading zeros applied so we can do range queries.
2062 *
2063 * @param int|string $key Key name.
2064 * @param string $group Group name, default = 'default'.
2065 *
2066 * @return string
2067 */
2068 private function normalize_name( $key, $group ) {
2069 if ( is_numeric( $key ) && (int) $key == $key && (int) $key >= 0 && (int) $key <= $this->intkey_max ) {
2070 $key = self::INTKEY_SENTINEL . str_pad( $key, 1 + $this->intkey_length, '0', STR_PAD_LEFT );
2071 }
2072
2073 if ( $this->multisite && ! isset( $this->global_groups[ $group ] ) ) {
2074 $key = $this->blog_prefix . $key;
2075 }
2076 if ( empty( $group ) ) {
2077 $group = 'default';
2078 }
2079
2080 return $group . '|' . $key;
2081 }
2082
2083 /**
2084 * Retrieves the cache contents, if it exists.
2085 *
2086 * The contents will be first attempted to be retrieved by searching by the
2087 * key in the cache group. If the cache is hit (success) then the contents
2088 * are returned.
2089 *
2090 * On failure, the number of cache misses will be incremented.
2091 *
2092 * @param int|string $key The key under which the cache contents are stored.
2093 * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
2094 * @param bool $force Optional. Whether to force an update of the local cache
2095 * from the persistent cache. Default false.
2096 * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
2097 * Disambiguates a return of false, a storable value. Default null.
2098 *
2099 * @return mixed|false The cache contents on success, false on failure to retrieve contents.
2100 * @since 2.0.0
2101 */
2102 public function get( $key, $group = 'default', $force = false, &$found = null ) {
2103 if ( -- $this->get_depth <= 0 ) {
2104 return false;
2105 }
2106
2107 if ( ! $this->is_valid_key( $key ) ) {
2108 ++ $this->get_depth;
2109
2110 return false;
2111 }
2112
2113 $start = hrtime( true );
2114 $name = $this->normalize_name( $key, $group );
2115
2116 if ( $force ) {
2117 unset( $this->cache[ $name ] );
2118 unset ( $this->not_in_persistent_cache[ $name ] );
2119 }
2120
2121 try {
2122 if ( array_key_exists( $name, $this->cache ) ) {
2123 $found = true;
2124 ++ $this->cache_hits;
2125 ++ $this->get_depth;
2126
2127 return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2128 }
2129 if ( $this->cache_item_exists( $name ) ) {
2130 $found = true;
2131 ++ $this->cache_hits;
2132 ++ $this->get_depth;
2133
2134 $this->get_times[] = hrtime( true ) - $start;
2135
2136 return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2137 }
2138 } catch ( Exception $ex ) {
2139 $this->delete_offending_files();
2140
2141 ++ $this->get_depth;
2142
2143 return false;
2144 }
2145
2146 $found = false;
2147 $this->cache_misses ++;
2148
2149 ++ $this->get_depth;
2150
2151 return false;
2152 }
2153
2154 /**
2155 * Retrieves the cache contents, if it exists.
2156 *
2157 * The contents will be first attempted to be retrieved by searching by the
2158 * key in the cache group. If the cache is hit (success) then the contents
2159 * are returned.
2160 *
2161 * On failure, the number of cache misses will be incremented.
2162 *
2163 * @param string $name Normalized name.
2164 * @param bool $found Whether the key was found in the cache (passed by reference). Disambiguates a return of false, a storable value.
2165 *
2166 * @return mixed|false The cache contents -- clones of objects -- on success, false on failure to retrieve contents.
2167 * @since 2.0.0
2168 */
2169 private function get_by_normalized_name( $name, &$found ) {
2170 if ( -- $this->get_depth <= 0 ) {
2171 $found = false;
2172 return false;
2173 }
2174 try {
2175 if ( array_key_exists( $name, $this->cache ) || $this->cache_item_exists( $name ) ) {
2176 ++ $this->cache_hits;
2177 ++ $this->get_depth;
2178 $found = true;
2179
2180 return is_object( $this->cache[ $name ] ) ? clone( $this->cache[ $name ] ) : $this->cache[ $name ];
2181 }
2182 } catch ( Exception $ex ) {
2183 $this->delete_offending_files();
2184
2185 ++ $this->get_depth;
2186
2187 $found = false;
2188 return false;
2189 }
2190 $this->cache_misses ++;
2191 ++ $this->get_depth;
2192
2193 $found = false;
2194 return false;
2195 }
2196
2197 /**
2198 * Deletes multiple values from the cache in one call.
2199 *
2200 * @param array $keys Array of keys to be deleted.
2201 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2202 *
2203 * @return bool[] Array of return values, grouped by key. Each value is either
2204 * true on success, or false if the contents were not deleted.
2205 * @since 6.0.0
2206 */
2207 public function delete_multiple( array $keys, $group = '' ) {
2208 if ( 0 === count( $keys ) ) {
2209 return array();
2210 }
2211 $values = array();
2212
2213 /* use a transaction to accelerate delete_multiple */
2214 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2215 $this->transaction_active = true;
2216 $this->sqlite->exec( 'BEGIN' );
2217
2218 foreach ( $keys as $key ) {
2219 $values[ $key ] = $this->delete( $key, $group );
2220 /* limit the size of the transaction, hopefully preventing timeouts in other clients */
2221 if ( -- $transaction_size <= 0 ) {
2222 $this->sqlite->exec( 'COMMIT' );
2223 $this->sqlite->exec( 'BEGIN' );
2224 $transaction_size = self::TRANSACTION_SIZE_LIMIT;
2225 }
2226 }
2227 $this->sqlite->exec( 'COMMIT' );
2228 $this->transaction_active = false;
2229
2230 return $values;
2231 }
2232
2233 /**
2234 * Removes the contents of the cache key in the group.
2235 *
2236 * If the cache key does not exist in the group, then nothing will happen.
2237 *
2238 * @param int|string $key What the contents in the cache are called.
2239 * @param string $group Optional. Where the cache contents are grouped. Default 'default'.
2240 * @param bool $deprecated Optional. Unused. Default false.
2241 *
2242 * @return bool True on success, false if the contents were not deleted.
2243 * @since 2.0.0
2244 *
2245 */
2246 public function delete( $key, $group = 'default', $deprecated = false ) {
2247 if ( ! $this->is_valid_key( $key ) ) {
2248 return false;
2249 }
2250
2251 $name = $this->normalize_name( $key, $group );
2252 unset ( $this->cache[ $name ] );
2253 $this->delete_by_name( $name );
2254
2255 return true;
2256 }
2257
2258 /**
2259 * Flush a group from the APCu cache.
2260 *
2261 * @param string $group to clear from APCu cache.
2262 *
2263 * @return void
2264 */
2265 public function apcu_clear_cache_group( $group ) {
2266 /* Immediate cache group purge. */
2267 if ( $this->apcu_active ) {
2268 foreach ( new APCUIterator( '/^' . preg_quote( $this->apcusalt . $group . '|', '/' ) . '/', APC_ITER_KEY ) as $item ) {
2269 apcu_delete( $item['key'] );
2270 }
2271 }
2272 /* Deferred cache clear if we're in CLI context. */
2273 if ( $this->apcu_supported ) {
2274 $this->set_flag();
2275 }
2276 }
2277
2278 /**
2279 * Clear the APCu cache.
2280 * @return void
2281 */
2282 public function apcu_clear_cache() {
2283 /* Immediate cache clear. */
2284 if ( $this->apcu_active ) {
2285 foreach ( new APCUIterator( '/^' . preg_quote( $this->apcusalt, '/' ) . '/', APC_ITER_KEY ) as $item ) {
2286 apcu_delete( $item['key'] );
2287 }
2288 }
2289 /* Deferred cache clear if we're in CLI context. */
2290 if ( $this->apcu_supported ) {
2291 $this->set_flag();
2292 }
2293 }
2294
2295 /**
2296 * Delete the oldest elements until the size falls below the target size.
2297 *
2298 * This uses a least-recently-UPDATED approach to aging the elements. A least-recently-USED
2299 * approach requires writing the time of use to the cache with every access, and that
2300 * is too expensive.
2301 *
2302 * @param int $target_size Desired size in bytes.
2303 * @param int $current_size Current size in bytes.
2304 *
2305 * @return void
2306 */
2307 public function sqlite_delete_old( $target_size, $current_size ) {
2308 $horizon = null;
2309 if ( ! $this->sqlite ) {
2310 return;
2311 }
2312 try {
2313 if ( $target_size < $current_size ) {
2314 $resultset = $this->sqlite_load_sizes();
2315 if ( ! $resultset ) {
2316 return;
2317 }
2318 while ( true ) {
2319 $row = $resultset->fetchArray( SQLITE3_NUM );
2320 if ( ! $row ) {
2321 break;
2322 }
2323 /* Find the time horizon that will delete enough entries */
2324 $horizon = $row[1];
2325 $current_size -= $row[0];
2326 if ( $current_size <= $target_size ) {
2327 break;
2328 }
2329 }
2330 $resultset->finalize();
2331 if ( ! $horizon ) {
2332 return;
2333 }
2334 $object_cache = self::OBJECT_CACHE_TABLE;
2335 $offset = $this->noexpire_timestamp_offset;
2336 $limit = self::TRANSACTION_SIZE_LIMIT;
2337 $hit = $limit;
2338 $cleared = false;
2339
2340 while ( $hit >= $limit ) {
2341 if ( ! $cleared ) {
2342 /* Clear the APCu cache when we bulk-delete entries from SQLite. */
2343 $this->apcu_clear_cache();
2344 $cleared = true;
2345 }
2346 $sql = "DELETE FROM $object_cache WHERE name IN (SELECT name FROM $object_cache WHERE expires >= $offset AND expires <= $horizon LIMIT $limit)";
2347 $this->sqlite->exec( $sql );
2348 $hit = $this->sqlite->changes();
2349 }
2350 if ( $cleared ) {
2351 /* Clear the APCu cache again after bulk delete to avoid a race condition. */
2352 $this->apcu_clear_cache();
2353 }
2354 $this->sqlite->exec( 'PRAGMA optimize;' );
2355 }
2356 $this->checkpoint( 'TRUNCATE' );
2357 } catch ( Exception $ex ) {
2358 /* Empty, intentionally. */
2359 }
2360 }
2361
2362 /**
2363 * Delete from the persistent cache.
2364 *
2365 * @param string $name What to call the contents in the cache.
2366 *
2367 * @return void
2368 */
2369 private function delete_by_name( $name ) {
2370 $exception = null;
2371 $retries = 3;
2372 $stmt = $this->deleteone_stmt;
2373 $this->not_in_persistent_cache[ $name ] = true;
2374 $start = hrtime( true );
2375 if ( $this->apcu_active ) {
2376 apcu_delete( $this->apcusalt . $name );
2377 }
2378 while ( $retries -- > 0 ) {
2379 try {
2380 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2381 $result = $stmt->execute();
2382 $result->finalize();
2383 $this->delete_times[] = hrtime( true ) - $start;
2384
2385 return;
2386 } catch ( Exception $ex ) {
2387 $exception = $ex;
2388 if ( ! str_contains( $ex->getMessage(), 'database is locked' ) || 5 !== $this->sqlite->lastErrorCode() ) {
2389 break;
2390 }
2391 }
2392 sleep( 1 );
2393 }
2394 if ( $exception ) {
2395 $this->error_log( 'delete_by_name', $exception );
2396 $this->delete_offending_files();
2397 self::drop_dead();
2398 }
2399 }
2400
2401 /**
2402 * Increments numeric cache item's value.
2403 *
2404 * @param int|string $key The cache key to increment.
2405 * @param int $offset Optional. The amount by which to increment the item's value.
2406 * Default 1.
2407 * @param string $group Optional. The group the key is in. Default 'default'.
2408 *
2409 * @return int|false The item's new value on success, false on failure.
2410 * @since 3.3.0
2411 */
2412 public function incr( $key, $offset = 1, $group = 'default' ) {
2413 if ( ! $this->is_valid_key( $key ) ) {
2414 return false;
2415 }
2416
2417 $name = $this->normalize_name( $key, $group );
2418
2419 $this->transaction_active = true;
2420 $this->sqlite->exec( 'BEGIN' );
2421 if ( $this->cache_item_not_exists( $name ) ) {
2422 $this->sqlite->exec( 'COMMIT' );
2423 $this->transaction_active = false;
2424 return false;
2425 }
2426
2427 if ( ! is_numeric( $this->cache[ $name ] ) ) {
2428 $this->cache[ $name ] = 0;
2429 }
2430
2431 $offset = (int) $offset;
2432
2433 $this->cache[ $name ] += $offset;
2434
2435 if ( $this->cache[ $name ] < 0 ) {
2436 $this->cache[ $name ] = 0;
2437 }
2438
2439 /* Maintain the original TTL on these incremented items. */
2440 $expires = $this->get_expiration_by_name( $name );
2441 if ( false !== $expires ) {
2442 $this->put_by_name( $name, $this->cache[ $name ], $expires );
2443 }
2444
2445 $this->sqlite->exec( 'COMMIT' );
2446 $this->transaction_active = false;
2447 return $this->cache[ $name ];
2448 }
2449
2450 /**
2451 * Decrements numeric cache item's value.
2452 *
2453 * @param int|string $key The cache key to decrement.
2454 * @param int $offset Optional. The amount by which to decrement the item's value.
2455 * Default 1.
2456 * @param string $group Optional. The group the key is in. Default 'default'.
2457 *
2458 * @return int|false The item's new value on success, false on failure.
2459 * @since 3.3.0
2460 *
2461 */
2462 public function decr( $key, $offset = 1, $group = 'default' ) {
2463 return $this->incr( $key, - $offset, $group );
2464 }
2465
2466 /**
2467 * Checkpoint and immediately vacuum.
2468 *
2469 * Notice that
2470 * @return void
2471 */
2472 public function vacuum() {
2473 $this->checkpoint( 'TRUNCATE' );
2474 $this->sqlite->exec( 'VACUUM;' );
2475
2476 }
2477
2478 /**
2479 * Clears the object cache of all data.
2480 *
2481 * @param bool $vacuum True to do a VACUUM operation.
2482 *
2483 * @return bool Always returns true.
2484 * @since 2.0.0
2485 */
2486 public function flush( $vacuum = false ) {
2487 try {
2488 $this->apcu_clear_cache();
2489 $this->cache = array();
2490 $this->not_in_persistent_cache = array();
2491
2492 $selective =
2493 defined( 'WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH' ) ? WP_SQLITE_OBJECT_CACHE_SELECTIVE_FLUSH : null;
2494
2495 if ( $selective && is_array( $this->unflushable_groups ) && count( $this->unflushable_groups ) > 0 ) {
2496 $clauses = array();
2497 foreach ( $this->unflushable_groups as $unflushable_group ) {
2498 $unflushable_group = sanitize_key( $unflushable_group );
2499 $clauses [] = "(name NOT LIKE '$unflushable_group|%')";
2500 }
2501 /* @noinspection SqlConstantCondition, SqlConstantExpression */
2502 $limit = self::TRANSACTION_SIZE_LIMIT;
2503 $hit = $limit;
2504 $this->checkpoint();
2505 $sql = 'DELETE FROM ' . $this->cache_table_name . ' WHERE name IN (SELECT name FROM ' . $this->cache_table_name . ' WHERE ' . implode( ' AND ', $clauses ) . ' LIMIT $limit);';
2506 while ( $hit >= $limit ) {
2507 $this->sqlite->exec( $sql );
2508 $hit = $this->sqlite->changes();
2509 }
2510 } else {
2511 /* SQLite's TRUNCATE TABLE equivalent */
2512 $sql = 'DELETE FROM ' . $this->cache_table_name . ';';
2513 $this->sqlite->exec( $sql );
2514 }
2515 $this->checkpoint( 'TRUNCATE' );
2516 $this->sqlite->exec( 'PRAGMA optimize' );
2517
2518 if ( $vacuum ) {
2519 $this->vacuum();
2520 }
2521 } catch ( Exception $ex ) {
2522 $this->error_log( 'flush failure, recreate cache.', $ex );
2523 $this->delete_offending_files();
2524 }
2525
2526 return true;
2527 }
2528
2529 /**
2530 * Clears the in-memory cache of all data leaving the external cache untouched.
2531 *
2532 * @return bool Always returns true.
2533 * @throws Exception Announce SQLite failure.
2534 * @since 2.0.0
2535 */
2536 public function flush_runtime() {
2537 $this->cache = array();
2538 $this->not_in_persistent_cache = array();
2539 /* Reset time horizon for subsequent fetches, for more precise expirations. */
2540 $now = time();
2541 if ( $now > $this->start_time ) {
2542 /* The current time is later than the start time. possibly due to sleep or some slow operation. */
2543 $this->start_time = $now;
2544 $this->prepare_statements( $this->cache_table_name );
2545 /* APCu expiration is bound to the pageview start time. So clean up expired items. */
2546 if ( $this->apcu_active ) {
2547 foreach ( new APCUIterator( '/^' . preg_quote( $this->apcusalt, '/' ) . '/', APC_ITER_KEY | APC_ITER_CTIME | APC_ITER_TTL ) as $item ) {
2548 if ( $item['ttl'] !== 0 && is_numeric( $item['creation_time'] ) && $item['creation_time'] + $item['ttl'] <= $now) {
2549 apcu_delete( $item['key'] );
2550 }
2551 }
2552 }
2553
2554 /* Deferred cache clear if we're in CLI context. */
2555 if ( $this->apcu_supported ) {
2556 $this->set_flag();
2557 }
2558 }
2559 return true;
2560 }
2561
2562 /**
2563 * Removes all cache items in a group.
2564 *
2565 * @param string $group Name of group to remove from cache.
2566 *
2567 * @return true Always returns true.
2568 * @since 6.1.0
2569 */
2570 public function flush_group( $group ) {
2571 $this->apcu_clear_cache_group( $group );
2572
2573 try {
2574 $names_to_flush = array();
2575 $prefix = $group . '|';
2576 foreach ( $this->cache as $name => $data ) {
2577 if ( str_starts_with( $name, $prefix ) ) {
2578 $names_to_flush [] = $name;
2579 }
2580 }
2581 $stmt = $this->deletegroup_stmt;
2582 $stmt->bindValue( ':group', $prefix, SQLITE3_TEXT );
2583 $result = $stmt->execute();
2584 $result->finalize();
2585 } catch ( Exception $ex ) {
2586 $this->error_log( 'flush_group', $ex );
2587 $this->delete_offending_files();
2588 }
2589 foreach ( $names_to_flush as $name ) {
2590 unset ( $this->cache[ $name ] );
2591 $this->not_in_persistent_cache[ $name ] = true;
2592 }
2593 unset ( $names_to_flush );
2594
2595 return true;
2596 }
2597
2598 /**
2599 * Sets the list of groups not to flushed cached.
2600 *
2601 * @param array $groups List of groups that are unflushable.
2602 */
2603 public function add_unflushable_groups( $groups ) {
2604 $groups = (array) $groups;
2605
2606 $this->unflushable_groups = array_unique( array_merge( $this->unflushable_groups, $groups ) );
2607 $this->cache_group_types();
2608 }
2609
2610 /**
2611 * Sets the list of global cache groups.
2612 *
2613 * @param string|string[] $groups List of groups that are global.
2614 *
2615 * @since 3.0.0
2616 */
2617 public function add_global_groups( $groups ) {
2618 $groups = (array) $groups;
2619
2620 $groups = array_fill_keys( $groups, true );
2621 $this->global_groups = array_merge( $this->global_groups, $groups );
2622
2623 $this->cache_group_types();
2624 }
2625
2626 /**
2627 * Switches the internal blog ID.
2628 *
2629 * This changes the blog ID used to create keys in blog specific groups.
2630 *
2631 * @param int $blog_id Blog ID.
2632 *
2633 * @since 3.5.0
2634 *
2635 */
2636 public function switch_to_blog( $blog_id ) {
2637 $blog_id = (int) $blog_id;
2638 $this->blog_prefix = $this->multisite ? $blog_id . ':' : '';
2639 }
2640
2641 /**
2642 * Resets cache keys.
2643 *
2644 * @since 3.0.0
2645 *
2646 * @deprecated 3.5.0 Use WP_Object_Cache::switch_to_blog()
2647 * @see switch_to_blog()
2648 */
2649 public function reset() {
2650 _deprecated_function( __FUNCTION__, '3.5.0', 'WP_Object_Cache::switch_to_blog()' );
2651
2652 // Clear out non-global caches since the blog ID has changed.
2653 $names_to_flush = array();
2654 foreach ( $this->cache as $name => $data ) {
2655 $splits = explode( '|', $name, 2 );
2656 if ( 2 === count( $splits ) ) {
2657 $group = $splits[0];
2658 if ( ! isset( $this->global_groups[ $group ] ) ) {
2659 $names_to_flush[] = $name;
2660 }
2661 }
2662 }
2663 foreach ( $names_to_flush as $name ) {
2664 unset ( $this->cache[ $name ] );
2665 $this->not_in_persistent_cache[ $name ] = true;
2666 }
2667 }
2668
2669 /**
2670 * Echoes the stats of the caching.
2671 *
2672 * Gives the cache hits, and cache misses. Also prints every cached group,
2673 * key and the data.
2674 *
2675 * @since 2.0.0
2676 */
2677 public function stats() {
2678 echo '<p><strong>Cache Hits:</strong> ' . esc_html( $this->cache_hits ) . '<br />';
2679 echo '<p><strong>Cache Misses:</strong> ' . esc_html( $this->cache_misses ) . '<br />';
2680 echo '<p><strong>APCu Hits:</strong> ' . esc_html( $this->apcu_hits ) . '<br />';
2681 echo '<strong>APCu Misses:</strong> ' . esc_html( $this->apcu_misses ) . '<br /></p>' . PHP_EOL;
2682 }
2683
2684 /**
2685 * Return the cache type. For use by "wp-cli cache type" and other display code.
2686 *
2687 * @return string The type of cache, "SQLite".
2688 */
2689 public function get_cache_type() {
2690 return $this->apcu_active ? 'APCu|SQLite' : 'SQLite';
2691 }
2692
2693 /**
2694 * Checks if the given group is part the ignored group array
2695 *
2696 * @param string $group Name of the group to check, pre-sanitized.
2697 *
2698 * @return bool
2699 */
2700 protected function is_ignored_group( $group ) {
2701 return $this->is_group_of_type( $group, 'ignored' );
2702 }
2703
2704 /**
2705 * Checks the type of the given group
2706 *
2707 * @param string $group Name of the group to check, pre-sanitized.
2708 * @param string $type Type of the group to check.
2709 *
2710 * @return bool
2711 */
2712 private function is_group_of_type( $group, $type ) {
2713 return isset( $this->group_type[ $group ] ) && $this->group_type[ $group ] === $type;
2714 }
2715
2716 /**
2717 * Checks if the given group is part the global group array
2718 *
2719 * @param string $group Name of the group to check, pre-sanitized.
2720 *
2721 * @return bool
2722 */
2723 protected function is_global_group( $group ) {
2724 return $this->is_group_of_type( $group, 'global' );
2725 }
2726
2727 /**
2728 * Get the names of the SQLite files.
2729 *
2730 * Notice there are, possibly, multiple files used to hold sqlite data.
2731 *
2732 * @return Generator Name of one of the possible SQLite files.
2733 */
2734 public function sqlite_files() {
2735 foreach ( array( '', '-shm', '-wal', '-wal2' ) as $suffix ) {
2736 yield $this->sqlite_path . $suffix;
2737 }
2738 }
2739
2740 /**
2741 * Delete sqlite files in hopes of recovering from trouble.
2742 *
2743 * @param int $retries
2744 *
2745 * @return void
2746 */
2747 private function delete_offending_files( $retries = 0 ) {
2748 $this->apcu_clear_cache();
2749 try {
2750 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
2751 error_log( "sqlite_object_cache failure, unlinking sqlite files to retry. $retries" );
2752 ob_start();
2753 foreach ( $this->sqlite_files() as $file ) {
2754 if ( @file_exists( realpath( $file ) ) ) {
2755 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
2756 @unlink( realpath( $file ) );
2757 }
2758 }
2759 ob_end_clean();
2760 } catch ( Exception $e ) {
2761 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
2762 error_log( "sqlite_object_cache cleanup failure: " . $e->getMessage() );
2763 }
2764 }
2765
2766 /**
2767 * Checkpoint the WAL log, incorporating it into the database.
2768 *
2769 * This should be done infrequently, but frequently enough so that the log doesn't just keep getting
2770 * bigger in busy sites with lots of concurrency.
2771 *
2772 * @param string $arg 'TRUNCATE' to remove WAL file or, the default, 'RESTART' to re-use it.
2773 *
2774 * @return void
2775 */
2776 private function checkpoint( $arg = 'RESTART' ) {
2777 $start = hrtime( true );
2778 $this->sqlite->exec( "PRAGMA wal_checkpoint($arg)" );
2779 $this->checkpoint_times[] = 0.000001 * ( hrtime( true ) - $start );
2780 }
2781
2782 /**
2783 * Set a named flag.
2784 *
2785 * @param $name string The name of the flag. Default: 'insert'.
2786 *
2787 * @return bool true if the flag was already set, false if it wasn't.
2788 */
2789 public function set_flag( $name = 'insert' ) {
2790 if ( ! $this->setflag_stmt ) {
2791 $tbl = $this->flags_table_name;
2792 $this->setflag_stmt =
2793 $this->sqlite->prepare( "INSERT OR IGNORE INTO $tbl (name) VALUES (:name );" );
2794 }
2795 $stmt = $this->setflag_stmt;
2796 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2797 $result = $stmt->execute();
2798 $already = 0 === $this->sqlite->changes();
2799 $result->finalize();
2800 return $already;
2801 }
2802
2803 /**
2804 * Clear a named flag.
2805 *
2806 * @param $name string The name of the flag. Default: 'insert'.
2807 *
2808 * @return bool true if the flag was set, false if it wasn't.
2809 */
2810 public function clear_flag( $name = 'insert' ) {
2811 if ( ! $this->clearflag_stmt ) {
2812 $tbl = $this->flags_table_name;
2813 $this->clearflag_stmt =
2814 $this->sqlite->prepare( "DELETE FROM $tbl WHERE name = :name;" );
2815 }
2816 $stmt = $this->clearflag_stmt;
2817 $stmt->bindValue( ':name', $name, SQLITE3_TEXT );
2818 $result = $stmt->execute();
2819 $already = 1 === $this->sqlite->changes();
2820 $result->finalize();
2821 return $already;
2822 }
2823 }
2824
2825 /**
2826 * Object Cache API
2827 *
2828 * @link https://developer.wordpress.org/reference/classes/wp_object_cache/
2829 *
2830 * @package WordPress
2831 * @subpackage Cache
2832 */
2833
2834 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
2835 /**
2836 * Sets up Object Cache Global and assigns it.
2837 *
2838 * @throws RuntimeException If we cannot write the db file into the specified directory.
2839 * @since 2.0.0
2840 *
2841 * @global WP_Object_Cache $wp_object_cache
2842 */
2843 function wp_cache_init() {
2844 $message = WP_Object_Cache::has_sqlite();
2845 if ( true === $message ) {
2846 // We need to override this WordPress global in order to inject our cache.
2847 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
2848 $GLOBALS['wp_object_cache'] = new WP_Object_Cache();
2849 } else {
2850 WP_Object_Cache::drop_dead( $message );
2851 }
2852 }
2853
2854 /**
2855 * Adds data to the cache, if the cache key doesn't already exist.
2856 *
2857 * @param int|string $key The cache key to use for retrieval later.
2858 * @param mixed $data The data to add to the cache.
2859 * @param string $group Optional. The group to add the cache to. Enables the same key
2860 * to be used across groups. Default empty.
2861 * @param int $expire Optional. When the cache data should expire, in seconds.
2862 * Default 0 (no expiration).
2863 *
2864 * @return bool True on success, false if cache key and group already exist.
2865 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2866 *
2867 * @since 2.0.0
2868 *
2869 * @see WP_Object_Cache::add()
2870 */
2871 function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
2872 global $wp_object_cache;
2873
2874 return $wp_object_cache->add( $key, $data, $group, (int) $expire );
2875 }
2876
2877 /**
2878 * Adds multiple values to the cache in one call.
2879 *
2880 * @param array $data Array of keys and values to be set.
2881 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2882 * @param int $expire Optional. When to expire the cache contents, in seconds.
2883 * Default 0 (no expiration).
2884 *
2885 * @return bool[] Array of return values, grouped by key. Each value is either
2886 * true on success, or false if cache key and group already exist.
2887 * @see WP_Object_Cache::add_multiple()
2888 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2889 *
2890 * @since 6.0.0
2891 */
2892 function wp_cache_add_multiple( array $data, $group = '', $expire = 0 ) {
2893 global $wp_object_cache;
2894
2895 return $wp_object_cache->add_multiple( $data, $group, $expire );
2896 }
2897
2898 /**
2899 * Replaces the contents of the cache with new data.
2900 *
2901 * @param int|string $key The key for the cache data that should be replaced.
2902 * @param mixed $data The new data to store in the cache.
2903 * @param string $group Optional. The group for the cache data that should be replaced.
2904 * Default empty.
2905 * @param int $expire Optional. When to expire the cache contents, in seconds.
2906 * Default 0 (no expiration).
2907 *
2908 * @return bool True if contents were replaced, false if original value does not exist.
2909 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2910 *
2911 * @since 2.0.0
2912 *
2913 * @see WP_Object_Cache::replace()
2914 */
2915 function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
2916 global $wp_object_cache;
2917
2918 return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
2919 }
2920
2921 /**
2922 * Saves the data to the cache.
2923 *
2924 * Differs from wp_cache_add() and wp_cache_replace() in that it will always write data.
2925 *
2926 * @param int|string $key The cache key to use for retrieval later.
2927 * @param mixed $data The contents to store in the cache.
2928 * @param string $group Optional. Where to group the cache contents. Enables the same key
2929 * to be used across groups. Default empty.
2930 * @param int $expire Optional. When to expire the cache contents, in seconds.
2931 * Default 0 (no expiration).
2932 *
2933 * @return bool True on success, false on failure.
2934 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2935 *
2936 * @since 2.0.0
2937 *
2938 * @see WP_Object_Cache::set()
2939 */
2940 function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
2941 global $wp_object_cache;
2942
2943 return $wp_object_cache->set( $key, $data, $group, (int) $expire );
2944 }
2945
2946 /**
2947 * Sets multiple values to the cache in one call.
2948 *
2949 * @param array $data Array of keys and values to be set.
2950 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2951 * @param int $expire Optional. When to expire the cache contents, in seconds.
2952 * Default 0 (no expiration).
2953 *
2954 * @return bool[] Array of return values, grouped by key. Each value is either
2955 * true on success, or false on failure.
2956 * @see WP_Object_Cache::set_multiple()
2957 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2958 *
2959 * @since 6.0.0
2960 */
2961 function wp_cache_set_multiple( array $data, $group = '', $expire = 0 ) {
2962 global $wp_object_cache;
2963
2964 return $wp_object_cache->set_multiple( $data, $group, $expire );
2965 }
2966
2967 /**
2968 * Retrieves the cache contents from the cache by key and group.
2969 *
2970 * @param int|string $key The key under which the cache contents are stored.
2971 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2972 * @param bool $force Optional. Whether to force an update of the local cache
2973 * from the persistent cache. Default false.
2974 * @param bool $found Optional. Whether the key was found in the cache (passed by reference).
2975 * Disambiguates a return of false, a storable value. Default null.
2976 *
2977 * @return mixed|false The cache contents on success, false on failure to retrieve contents.
2978 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
2979 *
2980 * @since 2.0.0
2981 *
2982 * @see WP_Object_Cache::get()
2983 */
2984 function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
2985 global $wp_object_cache;
2986
2987 return $wp_object_cache->get( $key, $group, $force, $found );
2988 }
2989
2990 /**
2991 * Retrieves multiple values from the cache in one call.
2992 *
2993 * @param array $keys Array of keys under which the cache contents are stored.
2994 * @param string $group Optional. Where the cache contents are grouped. Default empty.
2995 * @param bool $force Optional. Whether to force an update of the local cache
2996 * from the persistent cache. Default false.
2997 *
2998 * @return array Array of return values, grouped by key. Each value is either
2999 * the cache contents on success, or false on failure.
3000 * @see WP_Object_Cache::get_multiple()
3001 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3002 *
3003 * @since 5.5.0
3004 */
3005 function wp_cache_get_multiple( $keys, $group = '', $force = false ) {
3006 if ( 0 === count( $keys ) ) {
3007 return array();
3008 }
3009 global $wp_object_cache;
3010
3011 return $wp_object_cache->get_multiple( $keys, $group, $force );
3012 }
3013
3014 /**
3015 * Removes the cache contents matching key and group.
3016 *
3017 * @param int|string $key What the contents in the cache are called.
3018 * @param string $group Optional. Where the cache contents are grouped. Default empty.
3019 *
3020 * @return bool True on successful removal, false on failure.
3021 * @since 2.0.0
3022 *
3023 * @see WP_Object_Cache::delete()
3024 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3025 */
3026 function wp_cache_delete( $key, $group = '' ) {
3027 global $wp_object_cache;
3028
3029 return $wp_object_cache->delete( $key, $group );
3030 }
3031
3032 /**
3033 * Deletes multiple values from the cache in one call.
3034 *
3035 * @param array $keys Array of keys for deletion.
3036 * @param string $group Optional. Where the cache contents are grouped. Default empty.
3037 *
3038 * @return bool[] Array of return values, grouped by key. Each value is either
3039 * true on success, or false if the contents were not deleted.
3040 * @since 6.0.0
3041 *
3042 * @see WP_Object_Cache::delete_multiple()
3043 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3044 */
3045 function wp_cache_delete_multiple( array $keys, $group = '' ) {
3046 global $wp_object_cache;
3047
3048 return $wp_object_cache->delete_multiple( $keys, $group );
3049 }
3050
3051 /**
3052 * Increments numeric cache item's value.
3053 *
3054 * @param int|string $key The key for the cache contents that should be incremented.
3055 * @param int $offset Optional. The amount by which to increment the item's value.
3056 * Default 1.
3057 * @param string $group Optional. The group the key is in. Default empty.
3058 *
3059 * @return int|false The item's new value on success, false on failure.
3060 * @see WP_Object_Cache::incr()
3061 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3062 *
3063 * @since 3.3.0
3064 */
3065 function wp_cache_incr( $key, $offset = 1, $group = '' ) {
3066 global $wp_object_cache;
3067
3068 return $wp_object_cache->incr( $key, $offset, $group );
3069 }
3070
3071 /**
3072 * Decrements numeric cache item's value.
3073 *
3074 * @param int|string $key The cache key to decrement.
3075 * @param int $offset Optional. The amount by which to decrement the item's value.
3076 * Default 1.
3077 * @param string $group Optional. The group the key is in. Default empty.
3078 *
3079 * @return int|false The item's new value on success, false on failure.
3080 * @see WP_Object_Cache::decr()
3081 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3082 *
3083 * @since 3.3.0
3084 */
3085 function wp_cache_decr( $key, $offset = 1, $group = '' ) {
3086 global $wp_object_cache;
3087
3088 return $wp_object_cache->decr( $key, $offset, $group );
3089 }
3090
3091 /**
3092 * Removes all cache items.
3093 *
3094 * @return bool True on success, false on failure.
3095 * @see WP_Object_Cache::flush()
3096 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3097 *
3098 * @since 2.0.0
3099 *
3100 */
3101 function wp_cache_flush() {
3102 global $wp_object_cache;
3103
3104 return $wp_object_cache->flush();
3105 }
3106
3107 /**
3108 * Removes all cache items from the in-memory runtime cache.
3109 *
3110 * @return bool True on success, false on failure.
3111 * @see WP_Object_Cache::flush()
3112 *
3113 * @since 6.0.0
3114 *
3115 */
3116 function wp_cache_flush_runtime() {
3117 global $wp_object_cache;
3118
3119 return $wp_object_cache->flush_runtime();
3120 }
3121
3122 /**
3123 * Removes all cache items in a group, if the object cache implementation supports it.
3124 *
3125 * Before calling this function, always check for group flushing support using the
3126 * `wp_cache_supports( 'flush_group' )` function.
3127 *
3128 * @param string $group Name of group to remove from cache.
3129 *
3130 * @return bool True if group was flushed, false otherwise.
3131 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3132 *
3133 * @since 6.1.0
3134 *
3135 * @see WP_Object_Cache::flush_group()
3136 */
3137 function wp_cache_flush_group( $group ) {
3138 global $wp_object_cache;
3139
3140 return $wp_object_cache->flush_group( $group );
3141 }
3142
3143 /**
3144 * Determines whether the object cache implementation supports a particular feature.
3145 *
3146 * @param string $feature Name of the feature to check for. Possible values include:
3147 * 'add_multiple', 'set_multiple', 'get_multiple', 'delete_multiple',
3148 * 'flush_runtime', 'flush_group'.
3149 *
3150 * @return bool True if the feature is supported, false otherwise.
3151 * @since 6.1.0
3152 */
3153 function wp_cache_supports( $feature ) {
3154 switch ( $feature ) {
3155 case 'add_multiple':
3156 case 'set_multiple':
3157 case 'get_multiple':
3158 case 'delete_multiple':
3159 case 'flush_runtime':
3160 case 'flush_group':
3161 return true;
3162
3163 default:
3164 return false;
3165 }
3166 }
3167
3168 /**
3169 * Closes the cache.
3170 *
3171 * This function has ceased to do anything since WordPress 2.5. The
3172 * functionality was removed along with the rest of the persistent cache.
3173 *
3174 * This does not mean that plugins can't implement this function when they need
3175 * to make sure that the cache is cleaned up after WordPress no longer needs it.
3176 *
3177 * @return true Always returns true.
3178 * @since 2.0.0
3179 */
3180 function wp_cache_close() {
3181 global $wp_object_cache;
3182
3183 return $wp_object_cache->close();
3184 }
3185
3186 /**
3187 * Adds a group or set of groups to the list of global groups.
3188 *
3189 * @param string|string[] $groups A group or an array of groups to add.
3190 *
3191 * @see WP_Object_Cache::add_global_groups()
3192 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3193 *
3194 * @since 2.6.0
3195 */
3196 function wp_cache_add_global_groups( $groups ) {
3197 global $wp_object_cache;
3198
3199 $wp_object_cache->add_global_groups( $groups );
3200 }
3201
3202 /**
3203 * Adds a group or set of groups to the list of non-persistent groups.
3204 *
3205 * @param string|string[] $groups A group or an array of groups to add.
3206 *
3207 * @since 2.6.0
3208 */
3209 function wp_cache_add_non_persistent_groups( $groups ) {
3210
3211 global $wp_object_cache;
3212
3213 $wp_object_cache->add_non_persistent_groups( $groups );
3214 }
3215
3216 /**
3217 * Switches the internal blog ID.
3218 *
3219 * This changes the blog id used to create keys in blog specific groups.
3220 *
3221 * @param int $blog_id Site ID.
3222 *
3223 * @see WP_Object_Cache::switch_to_blog()
3224 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3225 *
3226 * @since 3.5.0
3227 */
3228 function wp_cache_switch_to_blog( $blog_id ) {
3229 global $wp_object_cache;
3230
3231 $wp_object_cache->switch_to_blog( $blog_id );
3232 }
3233
3234 /**
3235 * Resets internal cache keys and structures.
3236 *
3237 * If the cache back end uses global blog or site IDs as part of its cache keys,
3238 * this function instructs the back end to reset those keys and perform any cleanup
3239 * since blog or site IDs have changed since cache init.
3240 *
3241 * This function is deprecated. Use wp_cache_switch_to_blog() instead of this
3242 * function when preparing the cache for a blog switch. For clearing the cache
3243 * during unit tests, consider using wp_cache_init(). wp_cache_init() is not
3244 * recommended outside unit tests as the performance penalty for using it is high.
3245 *
3246 * @since 3.0.0
3247 * @deprecated 3.5.0 Use wp_cache_switch_to_blog()
3248 * @see WP_Object_Cache::reset()
3249 *
3250 * @global WP_Object_Cache $wp_object_cache Object cache global instance.
3251 */
3252 function wp_cache_reset() {
3253 _deprecated_function( __FUNCTION__, '3.5.0', 'wp_cache_switch_to_blog()' );
3254
3255 global $wp_object_cache;
3256
3257 $wp_object_cache->reset();
3258 }
3259 // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
3260
3261 /**
3262 * Manage the deferring of destructing the SQLite3 connection.
3263 *
3264 * This is necessary because some plugins set transients in their object destructors.
3265 */
3266 class SQLite_Object_Cache_Reaper {
3267 private $db;
3268 public function __construct( $db ) {
3269 $this->db = $db;
3270 }
3271 public function __destruct() {
3272 $this->db->close();
3273 unset( $this->db );
3274 }
3275 }
3276