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

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