PluginProbe
SQLite Object Cache / 1.6.0
SQLite Object Cache v1.6.0
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 1.6.0, at assets/drop-in/object-cache.php

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