PluginProbe
SQLite Object Cache / 1.6.2
SQLite Object Cache v1.6.2
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
← All changes | includes/class-sqlite-object-cache.php +583 -465 1.5.71.6.2 View file →
@@ -5,9 +5,9 @@
5 5 * @package WordPress Plugin Template/Includes
6 6 */
7 7
8 8 if ( ! defined( 'ABSPATH' ) ) {
9 - exit;
9 + exit;
10 10 }
11 11
12 12 /**
13 13 * Main plugin class.
@@ -14,490 +14,495 @@
14 14 *
15 15 * Only make one instance of this, please.
16 16 */
17 17 class SQLite_Object_Cache {
18 - const CLEAN_EVENT_HOOK = 'sqlite_object_cache_clean';
18 + const CLEAN_EVENT_HOOK = 'sqlite_object_cache_clean';
19 19
20 - /**
21 - * Local instance of SQLite_Object_Cache_Admin_API
22 - *
23 - * @var SQLite_Object_Cache_Admin_API|null
24 - */
25 - public $admin;
20 + /**
21 + * Settings class object
22 + *
23 + * @var object
24 + * @access public
25 + * @since 1.0.0
26 + */
27 + public $settings;
26 28
27 - /**
28 - * Settings class object
29 - *
30 - * @var object
31 - * @access public
32 - * @since 1.0.0
33 - */
34 - public $settings;
29 + /**
30 + * The version number.
31 + *
32 + * @var string
33 + * @access public
34 + * @since 1.0.0
35 + */
36 + public $_version;
35 37
36 - /**
37 - * The version number.
38 - *
39 - * @var string
40 - * @access public
41 - * @since 1.0.0
42 - */
43 - public $_version;
38 + /**
39 + * The token.
40 + *
41 + * @var string
42 + * @access public
43 + * @since 1.0.0
44 + */
45 + public $_token;
44 46
45 - /**
46 - * The token.
47 - *
48 - * @var string
49 - * @access public
50 - * @since 1.0.0
51 - */
52 - public $_token;
47 + /**
48 + * The main plugin file.
49 + *
50 + * @var string
51 + * @access public
52 + * @since 1.0.0
53 + */
54 + public $file;
53 55
54 - /**
55 - * The main plugin file.
56 - *
57 - * @var string
58 - * @access public
59 - * @since 1.0.0
60 - */
61 - public $file;
56 + /**
57 + * The main plugin directory.
58 + *
59 + * @var string
60 + * @access public
61 + * @since 1.0.0
62 + */
63 + public $dir;
62 64
63 - /**
64 - * The main plugin directory.
65 - *
66 - * @var string
67 - * @access public
68 - * @since 1.0.0
69 - */
70 - public $dir;
65 + /**
66 + * The plugin assets directory.
67 + *
68 + * @var string
69 + * @access public
70 + * @since 1.0.0
71 + */
72 + public $assets_dir;
71 73
72 - /**
73 - * The plugin assets directory.
74 - *
75 - * @var string
76 - * @access public
77 - * @since 1.0.0
78 - */
79 - public $assets_dir;
74 + /**
75 + * The plugin assets URL.
76 + *
77 + * @var string
78 + * @access public
79 + * @since 1.0.0
80 + */
81 + public $assets_url;
80 82
81 - /**
82 - * The plugin assets URL.
83 - *
84 - * @var string
85 - * @access public
86 - * @since 1.0.0
87 - */
88 - public $assets_url;
83 + /**
84 + * Suffix for JavaScripts.
85 + *
86 + * @var string
87 + * @access public
88 + * @since 1.0.0
89 + */
90 + public $script_suffix;
89 91
90 - /**
91 - * Suffix for JavaScripts.
92 - *
93 - * @var string
94 - * @access public
95 - * @since 1.0.0
96 - */
97 - public $script_suffix;
92 + /**
93 + * Path for the drop-in when it is working.
94 + * @var string
95 + */
96 + public $dropinfiledest;
98 97
99 - /**
100 - * Path for the drop-in when it is working.
101 - * @var string
102 - */
103 - public $dropinfiledest;
98 + /**
99 + * Path for the drop-in in the plugin tree.
100 + * @var string
101 + */
102 + public $dropinfilesource;
104 103
105 - /**
106 - * Path for the drop-in in the plugin tree.
107 - * @var string
108 - */
109 - public $dropinfilesource;
104 + /**
105 + * Minimum required sqlite version.
106 + *
107 + * @var string
108 + */
109 + public $minimum_sqlite_version = '3.7.0';
110 110
111 - /**
112 - * Minimum required sqlite version.
113 - *
114 - * @var string
115 - */
116 - public $minimum_sqlite_version = '3.7.0';
111 + /**
112 + * Constructor function.
113 + *
114 + * @param string $file File constructor.
115 + * @param string $version Plugin version.
116 + */
117 + public function __construct( $file = '', $version = '1.6.2' ) {
118 + $this->_version = $version;
119 + $this->_token = 'sqlite_object_cache';
117 120
118 - /**
119 - * Constructor function.
120 - *
121 - * @param string $file File constructor.
122 - * @param string $version Plugin version.
123 - */
124 - public function __construct( $file = '', $version = '1.5.7' ) {
125 - $this->_version = $version;
126 - $this->_token = 'sqlite_object_cache';
121 + // Load plugin environment variables.
122 + $this->file = $file;
123 + $this->dir =
124 + trailingslashit( dirname( trailingslashit( WP_PLUGIN_DIR ) . plugin_basename( $this->file ) ) );
125 + $this->assets_dir = trailingslashit( $this->dir . 'assets' );
126 + $this->dropinfilesource = $this->assets_dir . 'drop-in/object-cache.php';
127 + $this->dropinfiledest = trailingslashit( WP_CONTENT_DIR ) . 'object-cache.php';
127 128
128 - // Load plugin environment variables.
129 - $this->file = $file;
130 - $this->dir =
131 - trailingslashit( dirname( trailingslashit (WP_PLUGIN_DIR ) . plugin_basename( $this->file ) ) );
132 - $this->assets_dir = trailingslashit( $this->dir . 'assets' );
133 - $this->dropinfilesource = $this->assets_dir . 'drop-in/object-cache.php';
134 - $this->dropinfiledest = trailingslashit( WP_CONTENT_DIR ) . 'object-cache.php';
129 + $this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) );
135 130
136 - $this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) );
131 + register_activation_hook( $this->file, array( $this, 'on_activation' ) );
132 + register_deactivation_hook( $this->file, array( $this, 'on_deactivation' ) );
137 133
138 - $this->script_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
134 + if ( is_admin() ) {
135 + /* Suppress backups of .sqlite files if possible. */
136 + new SQLite_Backup_Exclusion();
139 137
140 - register_activation_hook( $this->file, array( $this, 'on_activation' ) );
141 - register_deactivation_hook( $this->file, array( $this, 'on_deactivation' ) );
138 + /* Health check */
139 + new SQLite_Object_Cache_Opcache();
142 140
143 - if ( is_admin() ) {
144 - // Load API for generic admin functions.
145 - $this->admin = new SQLite_Object_Cache_Admin_API();
146 - // Suppress backups
147 - new SQLite_Backup_Exclusion();
148 -
149 - add_action( 'admin_notices', function () {
150 - $purged = get_site_transient( 'sqlite-object-cache-flush-on-update' );
151 - if ( $purged ) {
152 - delete_site_transient( 'sqlite-object-cache-flush-on-update' );
153 - $message = esc_html( __('A software installation or upgrade operation flushed the SQLite Object Cache.', 'sqlite-object-cache'))
154 - ?>
155 - <div class="notice notice-info sqlite-object-cache is-dismissible">
156 - <p><?php echo $message?></p>
157 - </div>
158 - <?php
141 + add_action( 'admin_notices', function () {
142 + $flushed = get_site_transient( 'sqlite-object-cache-flush-on-update' );
143 + if ( $flushed ) {
144 + delete_site_transient( 'sqlite-object-cache-flush-on-update' );
145 + $message = __( 'Flushed the SQLite Object Cache successfully after a software installation or upgrade.', 'sqlite-object-cache' );
146 + ?>
147 + <div class="notice notice-success sqlite-object-cache is-dismissible">
148 + <p><?php echo esc_html( $message ); ?></p>
149 + </div>
150 + <?php
151 + }
152 + } );
159 153 }
160 - } );
161 - }
162 154
163 - add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
155 + add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
164 156
165 - /* Ensure nothing is cached after software upgrades. */
166 - add_action( 'upgrader_process_complete', function () {
167 - add_action( 'shutdown', function() {
168 - wp_cache_flush();
169 - set_site_transient( 'sqlite-object-cache-flush-on-update', true, HOUR_IN_SECONDS );
170 - }, 999 );
171 - wp_cache_flush();
172 - }, 0 );
157 + /* Ensure nothing is cached after software upgrades. */
158 + add_action( 'upgrader_process_complete', function () {
159 + add_action( 'shutdown', function () {
160 + wp_cache_flush();
161 + set_site_transient( 'sqlite-object-cache-flush-on-update', true, HOUR_IN_SECONDS );
162 + }, 999 );
163 + wp_cache_flush();
164 + }, 0 );
173 165
174 166
167 + /* Handle cron cache cleanup */
168 + add_action( self::CLEAN_EVENT_HOOK, array( $this, 'clean_job' ), 10, 0 );
169 + if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
170 + wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
171 + }
172 + /* Handle probabilistic non-cron cleanup, one request in 2500. */
173 + /* We don't need cryptographic-grade random numbering here. */
174 + // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand
175 + if ( 1 === rand( 1, 2500 ) ) {
176 + add_action( 'shutdown', function () {
177 + $this->clean_job( 1.25 );
178 + }, 999, 0 );
179 + }
175 180
176 - /* handle cron cache cleanup */
177 - add_action( self::CLEAN_EVENT_HOOK, array( $this, 'clean_job' ), 10, 0 );
178 - if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
179 - wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
181 + /* Admin bar flush button - works on both frontend and backend. */
182 + $option = get_option( $this->_token . '_settings', array() );
183 + if ( array_key_exists ('adminbarflush', $option ) && 'on' === $option['adminbarflush'] ) {
184 + add_action( 'init', array( $this, 'handle_admin_bar_flush' ) );
185 + add_action ( 'init', function() {
186 + if ( ! ( is_multisite() && ! is_main_site() ) && current_user_can( 'manage_options') ) {
187 + add_action( 'admin_bar_menu', array( $this, 'admin_bar_flush_button' ), 100 );
188 + add_action( 'admin_notices', array( $this, 'maybe_show_flush_notice' ) );
189 + }
190 + });
191 + }
180 192 }
181 - /* Handle probabilistic non-cron cleanup, one request in 2000. */
182 - if ( 1 === rand( 1, 2000 ) ) {
183 - add_action( 'shutdown', function () {
184 - $this->clean_job( 1.25 );
185 - }, 999, 0 );
186 - }
187 - }
188 193
189 - /**
190 - * WP_Cron task to clean cache entries.
191 - *
192 - * @param float $grace_factor Default 1.0
193 - *
194 - * @return void
195 - */
196 - public function clean_job( $grace_factor = 1.0 ) {
197 - $option = get_option( $this->_token . '_settings', array() );
198 - $target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
199 - $target_size *= ( 1024 * 1024 );
200 - $threshold_size = (int) ( $target_size * $grace_factor );
194 + /**
195 + * WP_Cron task to clean cache entries.
196 + *
197 + * @param float $grace_factor Default 1.0
198 + *
199 + * @return void
200 + */
201 + public function clean_job( $grace_factor = 1.0 ) {
202 + $option = get_option( $this->_token . '_settings', array() );
203 + $target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
204 + $target_size *= ( 1024 * 1024 );
205 + $threshold_size = (int) ( $target_size * $grace_factor );
201 206
202 - global $wp_object_cache;
203 - if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
204 - return;
205 - }
206 - /* Clean up old statistics. Do this even when the cache is not over size. */
207 - $retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
208 - $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
207 + global $wp_object_cache;
208 + if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
209 + return;
210 + }
211 + /* Clean up old statistics. Do this even when the cache is not over size. */
212 + $retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
213 + $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
209 214
210 - $current_size = $wp_object_cache->sqlite_get_size();
211 - /* Skip this if the current size is small enough. */
212 - if ( $current_size <= $threshold_size ) {
213 - return;
214 - }
215 + $current_size = $wp_object_cache->sqlite_get_size();
216 + /* Skip this if the current size is small enough. */
217 + if ( $current_size <= $threshold_size ) {
218 + return;
219 + }
215 220
216 - /* Remove expired items (transients mostly). */
217 - if ( $wp_object_cache->sqlite_remove_expired() ) {
218 - /* If anything was removed, get the size again. */
219 - $current_size = $wp_object_cache->sqlite_get_size();
220 - }
221 + /* Remove expired items (transients mostly). */
222 + if ( $wp_object_cache->sqlite_remove_expired() ) {
223 + /* If anything was removed, get the size again. */
224 + $current_size = $wp_object_cache->sqlite_get_size();
225 + }
221 226
222 - /* Skip this if the size is small enough, after removing expired items. */
223 - if ( $current_size <= $threshold_size ) {
224 - return;
227 + /* Skip this if the size is small enough, after removing expired items. */
228 + if ( $current_size <= $threshold_size ) {
229 + return;
230 + }
231 +
232 + /* Delete the least-recently-updated items to get to the target size. */
233 + $wp_object_cache->sqlite_delete_old( $target_size, $current_size );
225 234 }
226 235
227 - /* Delete the least-recently-updated items to get to the target size. */
228 - $wp_object_cache->sqlite_delete_old( $target_size, $current_size );
229 - }
236 + /**
237 + * Plugin activation hook
238 + *
239 + * @return void
240 + */
241 + public function on_activation() {
230 242
231 - /**
232 - * Plugin activation hook
233 - *
234 - * @return void
235 - */
236 - public function on_activation() {
243 + $this->sync_apcu_global_to_option( true );
244 + if ( true === $this->has_sqlite() ) {
245 + add_action( 'shutdown', array( $this, 'update_dropin' ) );
246 + add_action( 'shutdown', array( $this, 'delete_all_transients_from_db' ) );
247 + }
248 + }
237 249
238 - $this->sync_apcu_global_to_option( true );
239 - if ( true === $this->has_sqlite() ) {
240 - add_action( 'shutdown', array( $this, 'update_dropin' ) );
241 - add_action( 'shutdown', array( $this, 'delete_all_transients_from_db' ) );
242 - }
243 - }
250 + /**
251 + * Determine whether we can use SQLite3.
252 + *
253 + * @return bool|string true, or an error message.
254 + */
255 + public function has_sqlite() {
256 + if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
257 + return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
258 + }
244 259
245 - /**
246 - * Determine whether we can use SQLite3.
247 - *
248 - * @return bool|string true, or an error message.
249 - */
250 - public function has_sqlite() {
251 - if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
252 - return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
253 - }
260 + $sqlite_version = $this->sqlite_get_version();
261 + if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
262 + return sprintf(
263 + /* translators: 1 actual SQLite version. 2 required SQLite version) */
264 + __( 'You cannot use the SQLite Object Cache plugin. Your server only offers SQLite3 version %1$s, but at least %2$s is required.', 'sqlite-object-cache' ),
265 + $sqlite_version, $this->minimum_sqlite_version );
266 + }
254 267
255 - $sqlite_version = $this->sqlite_get_version();
256 - if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
257 - return sprintf(
258 - /* translators: 1 actual SQLite version. 2 required SQLite version) */
259 - __( 'You cannot use the SQLite Object Cache plugin. Your server only offers SQLite3 version %1$s, but at least %2$s is required.', 'sqlite-object-cache' ),
260 - $sqlite_version, $this->minimum_sqlite_version );
268 + return true;
261 269 }
262 270
263 - return true;
264 - }
271 + /**
272 + * Get the version number for the SQLite extension.
273 + *
274 + * @return string|false SQLite's version number.
275 + */
276 + public function sqlite_get_version() {
265 277
266 - /**
267 - * Get the version number for the SQLite extension.
268 - *
269 - * @return string|false SQLite's version number.
270 - */
271 - public function sqlite_get_version() {
278 + if ( class_exists( 'SQLite3' ) ) {
279 + $version = SQLite3::version();
272 280
273 - if ( class_exists( 'SQLite3' ) ) {
274 - $version = SQLite3::version();
281 + return $version['versionString'];
282 + }
275 283
276 - return $version['versionString'];
284 + return false;
277 285 }
278 286
279 - return false;
280 - }
287 + /**
288 + * Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
289 + *
290 + * @return true|WP_Error
291 + * @author Till Krüss
292 + *
293 + */
294 + public function test_filesystem_writing() {
295 + global $wp_filesystem;
281 296
282 - /**
283 - * Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
284 - *
285 - * @return true|WP_Error
286 - * @author Till Krüss
287 - *
288 - */
289 - public function test_filesystem_writing() {
290 - global $wp_filesystem;
297 + if ( ! $this->initialize_filesystem( '', true ) ) {
298 + return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
299 + }
291 300
292 - if ( ! $this->initialize_filesystem( '', true ) ) {
293 - return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
294 - }
301 + $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
295 302
296 - $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
303 + if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
304 + return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
305 + }
297 306
298 - if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
299 - return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
300 - }
307 + if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
308 + return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
309 + }
301 310
302 - if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
303 - return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
304 - }
311 + if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
312 + return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
313 + }
305 314
306 - if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
307 - return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
308 - }
315 + if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
316 + return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
317 + }
309 318
310 - if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
311 - return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
312 - }
319 + if ( ! $wp_filesystem->exists( $testfiledest ) ) {
320 + return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
321 + }
313 322
314 - if ( ! $wp_filesystem->exists( $testfiledest ) ) {
315 - return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
316 - }
323 + $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
317 324
318 - $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
325 + if ( $meta['Version'] !== $this->_version ) {
326 + return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
327 + }
319 328
320 - if ( $meta['Version'] !== $this->_version ) {
321 - return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
322 - }
329 + if ( ! $wp_filesystem->delete( $testfiledest ) ) {
330 + return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
331 + }
323 332
324 - if ( ! $wp_filesystem->delete( $testfiledest ) ) {
325 - return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
333 + return true;
326 334 }
327 335
328 - return true;
329 - }
336 + /**
337 + * Initializes the WP filesystem API to be ready for use
338 + *
339 + * @param string $url The URL to post the form to.
340 + * @param bool $silent Whether to ask the user for credentials if necessary or not.
341 + *
342 + * @return bool
343 + * @author Till Krüss
344 + *
345 + */
346 + public function initialize_filesystem( $url, $silent = false ) {
347 + require_once ABSPATH . 'wp-admin/includes/file.php';
348 + if ( $silent ) {
349 + ob_start();
350 + }
330 351
331 - /**
332 - * Initializes the WP filesystem API to be ready for use
333 - *
334 - * @param string $url The URL to post the form to.
335 - * @param bool $silent Whether to ask the user for credentials if necessary or not.
336 - *
337 - * @return bool
338 - * @author Till Krüss
339 - *
340 - */
341 - public function initialize_filesystem( $url, $silent = false ) {
342 - require_once ABSPATH . 'wp-admin/includes/file.php';
343 - if ( $silent ) {
344 - ob_start();
345 - }
352 + $credentials = request_filesystem_credentials( $url );
346 353
347 - $credentials = request_filesystem_credentials( $url );
354 + if ( false === $credentials ) {
355 + if ( $silent ) {
356 + ob_end_clean();
357 + }
348 358
349 - if ( false === $credentials ) {
350 - if ( $silent ) {
351 - ob_end_clean();
352 - }
359 + return false;
360 + }
353 361
354 - return false;
355 - }
362 + if ( ! WP_Filesystem( $credentials ) ) {
363 + request_filesystem_credentials( $url );
356 364
357 - if ( ! WP_Filesystem( $credentials ) ) {
358 - request_filesystem_credentials( $url );
365 + if ( $silent ) {
366 + ob_end_clean();
367 + }
359 368
360 - if ( $silent ) {
361 - ob_end_clean();
362 - }
369 + return false;
370 + }
363 371
364 - return false;
372 + return true;
365 373 }
366 374
367 - return true;
368 - }
375 + /**
376 + * Calls the drop-in update method if necessary
377 + *
378 + * @return void
379 + * @author Till Krüss
380 + */
381 + public function maybe_update_dropin() {
382 + if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
383 + return;
384 + }
369 385
370 - /**
371 - * Calls the drop-in update method if necessary
372 - *
373 - * @return void
374 - * @author Till Krüss
375 - */
376 - public function maybe_update_dropin() {
377 - if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
378 - return;
386 + $has = $this->has_sqlite();
387 + if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
388 + add_action( 'shutdown', array( $this, 'update_dropin' ) );
389 + }
379 390 }
380 391
381 - $has = $this->has_sqlite();
382 - if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
383 - add_action( 'shutdown', array( $this, 'update_dropin' ) );
392 + /**
393 + * Checks if the `object-cache.php` drop-in is outdated
394 + * @return bool
395 + * @author Till Krüss
396 + *
397 + */
398 + public function object_cache_dropin_needs_updating() {
399 + global $wp_object_cache;
400 + if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
401 + return true;
402 + }
403 + return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
384 404 }
385 - }
386 405
387 - /**
388 - * Checks if the `object-cache.php` drop-in is outdated
389 - * @return bool
390 - * @author Till Krüss
391 - *
392 - */
393 - public function object_cache_dropin_needs_updating() {
394 - global $wp_object_cache;
395 - if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
396 - return true;
406 + /**
407 + * Updates the `object-cache.php` drop-in
408 + *
409 + * @return void
410 + * @author Till Krüss
411 + */
412 + public function update_dropin() {
413 + global $wp_filesystem;
414 + $has = $this->has_sqlite();
415 + if ( true === $has && $this->initialize_filesystem( '', true ) ) {
416 +
417 + $this->delete_sqlite_files();
418 + $result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
419 + /**
420 + * Fires on cache enable event
421 + *
422 + * @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
423 + *
424 + * @since 0.1.0
425 + */
426 + do_action( 'sqlite_object_cache_update_dropin', $result );
427 + }
397 428 }
398 - return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
399 - }
400 429
401 - /**
402 - * Updates the `object-cache.php` drop-in
403 - *
404 - * @return void
405 - * @author Till Krüss
406 - */
407 - public function update_dropin() {
408 - global $wp_filesystem;
409 - $has = $this->has_sqlite();
410 - if ( true === $has && $this->initialize_filesystem( '', true ) ) {
430 + /**
431 + * Validates the `object-cache.php` drop-in
432 + *
433 + * @return bool
434 + * @author Till Krüss
435 + */
436 + public function validate_object_cache_dropin() {
437 + global $wp_object_cache;
411 438
412 - $this->delete_sqlite_files();
413 - $result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
414 - /**
415 - * Fires on cache enable event
416 - *
417 - * @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
418 - *
419 - * @since 0.1.0
420 - */
421 - do_action( 'sqlite_object_cache_update_dropin', $result );
422 - }
423 - }
439 + if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
440 + return false;
441 + }
424 442
425 - /**
426 - * Validates the `object-cache.php` drop-in
427 - *
428 - * @return bool
429 - * @author Till Krüss
430 - */
431 - public function validate_object_cache_dropin() {
432 - global $wp_object_cache;
443 + $dropin = get_plugin_data( $this->dropinfiledest );
444 + $plugin = get_plugin_data( $this->dropinfilesource );
433 445
434 - if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
435 - return false;
446 + /**
447 + * Filters the drop-in validation state
448 + *
449 + * @param bool $state The validation state of the drop-in.
450 + * @param string $dropin The `PluginURI` of the drop-in.
451 + * @param string $plugin The `PluginURI` of the plugin.
452 + *
453 + * @since 2.0.16
454 + */
455 + return apply_filters(
456 + 'sqlite_object_cache_validate_dropin',
457 + $dropin['PluginURI'] === $plugin['PluginURI'],
458 + $dropin['PluginURI'],
459 + $plugin['PluginURI']
460 + );
436 461 }
437 462
438 - $dropin = get_plugin_data( $this->dropinfiledest );
439 - $plugin = get_plugin_data( $this->dropinfilesource );
440 -
441 463 /**
442 - * Filters the drop-in validation state
464 + * Plugin deactivation hook
443 465 *
444 - * @param bool $state The validation state of the drop-in.
445 - * @param string $dropin The `PluginURI` of the drop-in.
446 - * @param string $plugin The `PluginURI` of the plugin.
466 + * @param string $plugin Plugin basename.
447 467 *
448 - * @since 2.0.16
468 + * @return void
469 + * @author Till Krüss
449 470 */
450 - return apply_filters(
451 - 'sqlite_object_cache_validate_dropin',
452 - $dropin['PluginURI'] === $plugin['PluginURI'],
453 - $dropin['PluginURI'],
454 - $plugin['PluginURI']
455 - );
456 - }
471 + public function on_deactivation( $plugin ) {
457 472
458 - /**
459 - * Plugin deactivation hook
460 - *
461 - * @param string $plugin Plugin basename.
462 - *
463 - * @return void
464 - * @author Till Krüss
465 - */
466 - public function on_deactivation( $plugin ) {
473 + wp_cache_flush();
467 474
468 - wp_cache_flush();
475 + wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
476 + $this->delete_sqlite_files();
477 + $this->delete_dropin();
478 + $this->delete_all_transients_from_db();
479 + }
469 480
470 - wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
471 - $this->delete_sqlite_files();
472 - $this->delete_dropin();
473 - $this->delete_all_transients_from_db();
474 - }
481 + private function delete_sqlite_files() {
482 + global $wp_filesystem;
483 + global $wp_object_cache;
475 484
476 - private function delete_sqlite_files() {
477 - global $wp_filesystem;
478 - global $wp_object_cache;
479 -
480 - if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
481 - if ( $this->initialize_filesystem( '', true ) ) {
482 - foreach ( $wp_object_cache->sqlite_files() as $file ) {
483 - $wp_filesystem->delete( $file );
485 + if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
486 + if ( $this->initialize_filesystem( '', true ) ) {
487 + foreach ( $wp_object_cache->sqlite_files() as $file ) {
488 + $wp_filesystem->delete( $file );
489 + }
490 + }
484 491 }
485 - }
486 492 }
487 - }
488 493
489 - private function delete_dropin() {
490 - global $wp_filesystem;
491 - ob_start();
494 + private function delete_dropin() {
495 + global $wp_filesystem;
496 + ob_start();
492 497
493 - if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
494 - $wp_filesystem->delete( $this->dropinfiledest );
498 + if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
499 + $wp_filesystem->delete( $this->dropinfiledest );
500 + }
501 +
502 + ob_end_clean();
495 503 }
496 504
497 - ob_end_clean();
498 - }
499 -
500 505 /**
501 506 * Deletes all transients from the database.
502 507 *
503 508 * It is necessary to do this when deactivating a persistent object cache
@@ -503,11 +508,8 @@
503 508 * It is necessary to do this when deactivating a persistent object cache
504 509 * because the transients are kept there, and the ones in the database are
505 510 * possibly stale.
506 511 *
507 - * The multi-table delete syntax is used to delete the transient record
508 - * from table a, and the corresponding transient_timeout record from table b.
509 - *
510 512 * This hammers performance. But deactivating a persistent object cache is
511 513 * not a common operation.
512 514 *
513 515 * @global wpdb $wpdb WordPress database abstraction object.
@@ -513,95 +515,211 @@
513 515 * @global wpdb $wpdb WordPress database abstraction object.
514 516 */
515 517 public function delete_all_transients_from_db() {
516 518 global $wpdb;
517 -
518 - $wpdb->query(
519 - $wpdb->prepare(
520 - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
521 - WHERE a.option_name LIKE %s
522 - AND a.option_name NOT LIKE %s
523 - AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )",
524 - $wpdb->esc_like( '_transient_' ) . '%',
525 - $wpdb->esc_like( '_transient_timeout_' ) . '%'
526 - )
527 - );
528 -
529 519 if ( ! is_multisite() ) {
520 + $this->clear_blog_transients();
530 521 // Single site stores site transients in the options table.
531 - $wpdb->query(
532 - $wpdb->prepare(
533 - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b
534 - WHERE a.option_name LIKE %s
535 - AND a.option_name NOT LIKE %s
536 - AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )",
537 - $wpdb->esc_like( '_site_transient_' ) . '%',
538 - $wpdb->esc_like( '_site_transient_timeout_' ) . '%'
539 - )
540 - );
541 - } elseif ( is_multisite() && is_main_site() && is_main_network() ) {
522 + $this->clear_blog_transients( '_site_transient_' );
523 + } else {
524 + foreach ( get_sites( array( 'number' => 0, 'fields' => 'ids', 'no_found_rows' => true, 'orderby' => false ) ) as $site_id ) {
525 + try {
526 + switch_to_blog( $site_id );
527 + $this->clear_blog_transients();
528 + } catch ( Exception $ex ) {
529 + /* Empty, intentionally. Don't crash clearing transients. */
530 + } finally {
531 + restore_current_blog();
532 + }
533 + }
542 534 // Multisite stores site transients in the sitemeta table.
535 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
543 536 $wpdb->query(
544 537 $wpdb->prepare(
545 - "DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b
546 - WHERE a.meta_key LIKE %s
547 - AND a.meta_key NOT LIKE %s
548 - AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) ",
549 - $wpdb->esc_like( '_site_transient_' ) . '%',
550 - $wpdb->esc_like( '_site_transient_timeout_' ) . '%'
538 + "DELETE FROM {$wpdb->sitemeta} WHERE a.meta_key LIKE %s",
539 + $wpdb->esc_like( '_site_transient_' ) . '%'
551 540 )
552 541 );
542 +
553 543 }
554 544 }
555 545
556 546 /**
557 - * @return bool True if APCu support is activated for this plugin.
558 - */
559 - public function apcu_is_activated() {
560 - return defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU
561 - && $this->apcu_extension_is_enabled();
562 - }
547 + * DELETE transients from the current blog's options table.
548 + *
549 + * @param string $tag _site_transient_ or _transient_, the default.
550 + *
551 + * @return void
552 + */
553 + private function clear_blog_transients( $tag = '_transient_') {
554 + global $wpdb;
555 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
556 + $wpdb->query(
557 + $wpdb->prepare(
558 + "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
559 + $wpdb->esc_like( $tag ) . '%'
560 + )
561 + );
562 + }
563 563
564 - /**
565 - * @return bool True if the APCu extension is loaded and enabled.
566 - */
567 - public function apcu_extension_is_enabled() {
568 - return function_exists( 'apcu_enabled' ) && apcu_enabled();
569 - }
564 + /**
565 + * @return bool True if APCu support is activated for this plugin.
566 + */
567 + public function apcu_is_activated() {
568 + return defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU
569 + && $this->apcu_extension_is_enabled();
570 + }
570 571
571 - /**
572 - * Make sure WP_SQLITE_OBJECT_CACHE_APCU and $option['use_apcu'] match.
573 - *
574 - * @param bool $unconditional true if we should always change the option to match WP_SQLITE_OBJECT_CACHE_APCU.
575 - *
576 - * @return string 'on' or 'off': the current activation state of epcu.
577 - */
578 - public function sync_apcu_global_to_option( $unconditional = true ) {
579 - global $wp_object_cache;
580 - $option = get_option( $this->_token . '_settings', array() );
581 - $option_dirty = false;
582 - $updated_flag = array_key_exists( 'use_apcu_updated', $option );
583 - $use_apcu = array_key_exists( 'use_apcu', $option ) && 'on' === $option['use_apcu'] ? 'on' : 'off';
584 - if ( $updated_flag ) {
585 - unset ( $option['use_apcu_updated'] );
586 - $option_dirty = true;
572 + /**
573 + * @return bool True if the APCu extension is loaded and enabled.
574 + */
575 + public function apcu_extension_is_enabled() {
576 + return function_exists( 'apcu_enabled' ) && apcu_enabled();
587 577 }
588 - $target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
589 - if ( ! $unconditional && $updated_flag ) {
590 - $target_use_apcu = $use_apcu;
578 +
579 + /**
580 + * Make sure WP_SQLITE_OBJECT_CACHE_APCU and $option['use_apcu'] match.
581 + *
582 + * @param bool $unconditional true if we should always change the option to match WP_SQLITE_OBJECT_CACHE_APCU.
583 + *
584 + * @return string 'on' or 'off': the current activation state of epcu.
585 + */
586 + public function sync_apcu_global_to_option( $unconditional = true ) {
587 + global $wp_object_cache;
588 + $option = get_option( $this->_token . '_settings', array() );
589 + $option_dirty = false;
590 + $updated_flag = array_key_exists( 'use_apcu_updated', $option );
591 + $use_apcu = array_key_exists( 'use_apcu', $option ) && 'on' === $option['use_apcu'] ? 'on' : 'off';
592 + if ( $updated_flag ) {
593 + unset ( $option['use_apcu_updated'] );
594 + $option_dirty = true;
595 + }
596 + $target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
597 + if ( ! $unconditional && $updated_flag ) {
598 + $target_use_apcu = $use_apcu;
599 + }
600 + if ( $target_use_apcu !== $use_apcu ) {
601 + $option['use_apcu'] = $target_use_apcu;
602 + $option_dirty = true;
603 + }
604 + if ( $option_dirty ) {
605 + if ( method_exists( $wp_object_cache, 'apcu-clear_cache' ) ) {
606 + $wp_object_cache->apcu_clear_cache();
607 + }
608 + update_option( $this->_token . '_settings', $option, true );
609 + }
610 +
611 + return $target_use_apcu;
591 612 }
592 - if ( $target_use_apcu !== $use_apcu ) {
593 - $option['use_apcu'] = $target_use_apcu;
594 - $option_dirty = true;
613 +
614 + /**
615 + * Add a flush button to the admin bar.
616 + *
617 + * @param WP_Admin_Bar $wp_admin_bar The admin bar instance.
618 + *
619 + * @return void
620 + */
621 + public function admin_bar_flush_button( $wp_admin_bar ) {
622 + if ( ! is_admin_bar_showing() ) {
623 + return;
624 + }
625 +
626 + if ( ! current_user_can( 'manage_options' ) ) {
627 + return;
628 + }
629 +
630 + // Only show on main site for multisite installations.
631 + if ( is_multisite() && ! is_main_site() ) {
632 + return;
633 + }
634 +
635 + $flush_url = add_query_arg(
636 + array( 'sqlite_object_cache_action' => 'flush' ),
637 + remove_query_arg( 'sqlite_object_cache_action' )
638 + );
639 + $nonced_url = wp_nonce_url( $flush_url, 'sqlite_object_cache_flush' );
640 +
641 + $wp_admin_bar->add_node(
642 + array(
643 + 'id' => 'sqlite-object-cache-flush',
644 + 'title' => __( 'Flush Object Cache', 'sqlite-object-cache' ),
645 + 'href' => $nonced_url,
646 + 'meta' => array(
647 + 'title' => __( 'Delete all object cache entries now, including all transients.', 'sqlite-object-cache' ),
648 + ),
649 + )
650 + );
595 651 }
596 - if ( $option_dirty ) {
597 - if ( method_exists( $wp_object_cache, 'apcu-clear_cache' ) ) {
598 - $wp_object_cache->apcu_clear_cache();
599 - }
600 - update_option( $this->_token . '_settings', $option, true );
652 +
653 + /**
654 + * Handle the admin bar flush action.
655 + *
656 + * @return void
657 + */
658 + public function handle_admin_bar_flush() {
659 + if ( ! isset( $_GET['sqlite_object_cache_action'] ) ) {
660 + return;
661 + }
662 +
663 + $action = sanitize_key( wp_unslash( $_GET['sqlite_object_cache_action'] ) );
664 +
665 + if ( 'flush' !== $action ) {
666 + return;
667 + }
668 +
669 + if ( ! current_user_can( 'manage_options' ) ) {
670 + wp_die( esc_html__( 'You do not have permission to flush the object cache.', 'sqlite-object-cache' ) );
671 + }
672 +
673 + // Only allow flush on main site for multisite installations (matches button visibility).
674 + if ( is_multisite() && ! is_main_site() ) {
675 + wp_die( esc_html__( 'Cache flush is only available on the main site.', 'sqlite-object-cache' ) );
676 + }
677 +
678 + check_admin_referer( 'sqlite_object_cache_flush' );
679 +
680 + wp_cache_flush();
681 +
682 + // Use a user-specific transient so only the initiating user sees the flush notice (applies to both single-site and multisite).
683 + $transient_key = 'sqlite_object_cache_flushed_' . get_current_user_id();
684 + set_transient( $transient_key, true, 30 );
685 +
686 + $referer = wp_get_referer();
687 + if ( $referer ) {
688 + $redirect_url = $referer;
689 + } else {
690 + $redirect_url = is_admin() ? admin_url() : home_url();
691 + }
692 + // Remove action and nonce query parameters from URL.
693 + $redirect_url = remove_query_arg( array( 'sqlite_object_cache_action', '_wpnonce' ), $redirect_url );
694 +
695 + wp_safe_redirect( $redirect_url );
696 + exit;
601 697 }
602 698
603 - return $target_use_apcu;
604 - }
699 + /**
700 + * Check for flush transient and show notice if set.
701 + *
702 + * @return void
703 + */
704 + public function maybe_show_flush_notice() {
705 + $transient_key = 'sqlite_object_cache_flushed_' . get_current_user_id();
706 + if ( get_transient( $transient_key ) ) {
707 + delete_transient( $transient_key );
708 + $this->show_flush_notice();
709 + }
710 + }
605 711
712 + /**
713 + * Display a notice when the object cache was flushed via admin bar.
714 + *
715 + * @return void
716 + */
717 + public function show_flush_notice() {
718 + ?>
719 + <div class="notice notice-success sqlite-object-cache is-dismissible">
720 + <p><?php esc_html_e( 'SQLite Object Cache flushed successfully.', 'sqlite-object-cache' ); ?></p>
721 + </div>
722 + <?php
723 + }
606 724
607 725 }