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 +604 -434 1.5.41.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,542 +14,712 @@
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.4' ) {
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( WP_CONTENT_DIR . '/plugins/' . 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 - }
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 + } );
153 + }
149 154
150 - // Handle localization.
151 - $this->load_plugin_textdomain();
152 - add_action( 'admin-init', array( $this, 'load_localization' ), 0 );
153 - add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
155 + add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
154 156
155 - /* handle cron cache cleanup */
156 - add_action( self::CLEAN_EVENT_HOOK, array( $this, 'clean_job' ), 10, 0 );
157 - if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
158 - wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
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 );
165 +
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 + }
180 +
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 + }
159 192 }
160 - /* Handle probabilistic non-cron cleanup, one request in 2000. */
161 - if ( 1 === rand( 1, 2000 ) ) {
162 - add_action( 'shutdown', function () {
163 - $this->clean_job( 1.25 );
164 - }, 999, 0 );
165 - }
166 - }
167 193
168 - /**
169 - * Load plugin textdomain
170 - *
171 - * @access public
172 - * @return void
173 - * @since 1.0.0
174 - */
175 - public function load_plugin_textdomain() {
176 - $domain = 'sqlite-object-cache';
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 );
177 206
178 - $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
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 );
179 214
180 - load_textdomain( $domain, WP_LANG_DIR . '/' . $domain . '/' . $domain . '-' . $locale . '.mo' );
181 - load_plugin_textdomain( $domain, false, dirname( plugin_basename( $this->file ) ) . '/languages/' );
182 - }
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 + }
183 220
184 - /**
185 - * WP_Cron task to clean cache entries.
186 - *
187 - * @param float $grace_factor Default 1.0
188 - *
189 - * @return void
190 - */
191 - public function clean_job( $grace_factor = 1.0 ) {
192 - $option = get_option( $this->_token . '_settings', array() );
193 - $target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
194 - $target_size *= ( 1024 * 1024 );
195 - $threshold_size = (int) ( $target_size * $grace_factor );
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 + }
196 226
197 - global $wp_object_cache;
198 - if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
199 - 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 );
200 234 }
201 - /* Clean up old statistics. Do this even when the cache is not over size. */
202 - $retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
203 - $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
204 235
205 - $current_size = $wp_object_cache->sqlite_get_size();
206 - /* Skip this if the current size is small enough. */
207 - if ( $current_size <= $threshold_size ) {
208 - return;
236 + /**
237 + * Plugin activation hook
238 + *
239 + * @return void
240 + */
241 + public function on_activation() {
242 +
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 + }
209 248 }
210 249
211 - /* Remove expired items (transients mostly). */
212 - if ( $wp_object_cache->sqlite_remove_expired() ) {
213 - /* If anything was removed, get the size again. */
214 - $current_size = $wp_object_cache->sqlite_get_size();
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 + }
259 +
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 + }
267 +
268 + return true;
215 269 }
216 270
217 - /* Skip this if the size is small enough, after removing expired items. */
218 - if ( $current_size <= $threshold_size ) {
219 - return;
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() {
277 +
278 + if ( class_exists( 'SQLite3' ) ) {
279 + $version = SQLite3::version();
280 +
281 + return $version['versionString'];
282 + }
283 +
284 + return false;
220 285 }
221 286
222 - /* Delete the least-recently-updated items to get to the target size. */
223 - $wp_object_cache->sqlite_delete_old( $target_size, $current_size );
224 - }
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;
225 296
226 - /**
227 - * Plugin activation hook
228 - *
229 - * @return void
230 - */
231 - public function on_activation() {
297 + if ( ! $this->initialize_filesystem( '', true ) ) {
298 + return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
299 + }
232 300
233 - $this->sync_apcu_global_to_option( true );
234 - if ( true === $this->has_sqlite() ) {
235 - add_action( 'shutdown', array( $this, 'update_dropin' ) );
236 - }
237 - }
301 + $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
238 302
239 - /**
240 - * Determine whether we can use SQLite3.
241 - *
242 - * @return bool|string true, or an error message.
243 - */
244 - public function has_sqlite() {
245 - if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
246 - return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
247 - }
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 + }
248 306
249 - $sqlite_version = $this->sqlite_get_version();
250 - if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
251 - return sprintf(
252 - /* translators: 1 actual SQLite version. 2 required SQLite version) */
253 - __( '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' ),
254 - $sqlite_version, $this->minimum_sqlite_version );
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 + }
310 +
311 + if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
312 + return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
313 + }
314 +
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 + }
318 +
319 + if ( ! $wp_filesystem->exists( $testfiledest ) ) {
320 + return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
321 + }
322 +
323 + $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
324 +
325 + if ( $meta['Version'] !== $this->_version ) {
326 + return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
327 + }
328 +
329 + if ( ! $wp_filesystem->delete( $testfiledest ) ) {
330 + return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
331 + }
332 +
333 + return true;
255 334 }
256 335
257 - return true;
258 - }
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 + }
259 351
260 - /**
261 - * Get the version number for the SQLite extension.
262 - *
263 - * @return string|false SQLite's version number.
264 - */
265 - public function sqlite_get_version() {
352 + $credentials = request_filesystem_credentials( $url );
266 353
267 - if ( class_exists( 'SQLite3' ) ) {
268 - $version = SQLite3::version();
354 + if ( false === $credentials ) {
355 + if ( $silent ) {
356 + ob_end_clean();
357 + }
269 358
270 - return $version['versionString'];
271 - }
359 + return false;
360 + }
272 361
273 - return false;
274 - }
362 + if ( ! WP_Filesystem( $credentials ) ) {
363 + request_filesystem_credentials( $url );
275 364
276 - /**
277 - * Load plugin localization
278 - *
279 - * @access public
280 - * @return void
281 - * @since 1.0.0
282 - */
283 - public function load_localization() {
284 - load_plugin_textdomain( 'sqlite-object-cache', false, dirname( plugin_basename( $this->file ) ) . '/languages/' );
285 - }
365 + if ( $silent ) {
366 + ob_end_clean();
367 + }
286 368
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;
369 + return false;
370 + }
296 371
297 - if ( ! $this->initialize_filesystem( '', true ) ) {
298 - return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
372 + return true;
299 373 }
300 374
301 - $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
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 + }
302 385
303 - if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
304 - return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
386 + $has = $this->has_sqlite();
387 + if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
388 + add_action( 'shutdown', array( $this, 'update_dropin' ) );
389 + }
305 390 }
306 391
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' ) );
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, '<' );
309 404 }
310 405
311 - if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
312 - return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
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 + }
313 428 }
314 429
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 - }
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;
318 438
319 - if ( ! $wp_filesystem->exists( $testfiledest ) ) {
320 - return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
321 - }
439 + if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
440 + return false;
441 + }
322 442
323 - $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
443 + $dropin = get_plugin_data( $this->dropinfiledest );
444 + $plugin = get_plugin_data( $this->dropinfilesource );
324 445
325 - if ( $meta['Version'] !== $this->_version ) {
326 - return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
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 + );
327 461 }
328 462
329 - if ( ! $wp_filesystem->delete( $testfiledest ) ) {
330 - return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
331 - }
463 + /**
464 + * Plugin deactivation hook
465 + *
466 + * @param string $plugin Plugin basename.
467 + *
468 + * @return void
469 + * @author Till Krüss
470 + */
471 + public function on_deactivation( $plugin ) {
332 472
333 - return true;
334 - }
473 + wp_cache_flush();
335 474
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();
475 + wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
476 + $this->delete_sqlite_files();
477 + $this->delete_dropin();
478 + $this->delete_all_transients_from_db();
350 479 }
351 480
352 - $credentials = request_filesystem_credentials( $url );
481 + private function delete_sqlite_files() {
482 + global $wp_filesystem;
483 + global $wp_object_cache;
353 484
354 - if ( false === $credentials ) {
355 - if ( $silent ) {
356 - ob_end_clean();
357 - }
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 + }
491 + }
492 + }
358 493
359 - return false;
360 - }
494 + private function delete_dropin() {
495 + global $wp_filesystem;
496 + ob_start();
361 497
362 - if ( ! WP_Filesystem( $credentials ) ) {
363 - request_filesystem_credentials( $url );
498 + if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
499 + $wp_filesystem->delete( $this->dropinfiledest );
500 + }
364 501
365 - if ( $silent ) {
366 502 ob_end_clean();
367 - }
368 -
369 - return false;
370 503 }
371 504
372 - return true;
373 - }
505 + /**
506 + * Deletes all transients from the database.
507 + *
508 + * It is necessary to do this when deactivating a persistent object cache
509 + * because the transients are kept there, and the ones in the database are
510 + * possibly stale.
511 + *
512 + * This hammers performance. But deactivating a persistent object cache is
513 + * not a common operation.
514 + *
515 + * @global wpdb $wpdb WordPress database abstraction object.
516 + */
517 + public function delete_all_transients_from_db() {
518 + global $wpdb;
519 + if ( ! is_multisite() ) {
520 + $this->clear_blog_transients();
521 + // Single site stores site transients in the options table.
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 + }
534 + // Multisite stores site transients in the sitemeta table.
535 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
536 + $wpdb->query(
537 + $wpdb->prepare(
538 + "DELETE FROM {$wpdb->sitemeta} WHERE a.meta_key LIKE %s",
539 + $wpdb->esc_like( '_site_transient_' ) . '%'
540 + )
541 + );
374 542
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;
543 + }
384 544 }
385 545
386 - $has = $this->has_sqlite();
387 - if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
388 - add_action( 'shutdown', array( $this, 'update_dropin' ) );
546 + /**
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 + );
389 562 }
390 - }
391 563
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;
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();
402 570 }
403 - return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
404 - }
405 571
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 );
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();
427 577 }
428 - }
429 578
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;
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 + }
438 610
439 - if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
440 - return false;
611 + return $target_use_apcu;
441 612 }
442 613
443 - $dropin = get_plugin_data( $this->dropinfiledest );
444 - $plugin = get_plugin_data( $this->dropinfilesource );
445 -
446 614 /**
447 - * Filters the drop-in validation state
615 + * Add a flush button to the admin bar.
448 616 *
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.
617 + * @param WP_Admin_Bar $wp_admin_bar The admin bar instance.
452 618 *
453 - * @since 2.0.16
619 + * @return void
454 620 */
455 - return apply_filters(
456 - 'sqlite_object_cache_validate_dropin',
457 - $dropin['PluginURI'] === $plugin['PluginURI'],
458 - $dropin['PluginURI'],
459 - $plugin['PluginURI']
460 - );
461 - }
621 + public function admin_bar_flush_button( $wp_admin_bar ) {
622 + if ( ! is_admin_bar_showing() ) {
623 + return;
624 + }
462 625
463 - /**
464 - * Plugin deactivation hook
465 - *
466 - * @param string $plugin Plugin basename.
467 - *
468 - * @return void
469 - * @author Till Krüss
470 - */
471 - public function on_deactivation( $plugin ) {
626 + if ( ! current_user_can( 'manage_options' ) ) {
627 + return;
628 + }
472 629
473 - wp_cache_flush();
630 + // Only show on main site for multisite installations.
631 + if ( is_multisite() && ! is_main_site() ) {
632 + return;
633 + }
474 634
475 - wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
476 - $this->delete_sqlite_files();
477 - $this->delete_dropin();
478 - }
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' );
479 640
480 - private function delete_sqlite_files() {
481 - global $wp_filesystem;
482 - global $wp_object_cache;
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 + );
651 + }
483 652
484 - if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
485 - if ( $this->initialize_filesystem( '', true ) ) {
486 - foreach ( $wp_object_cache->sqlite_files() as $file ) {
487 - $wp_filesystem->delete( $file );
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;
488 661 }
489 - }
490 - }
491 - }
492 662
493 - private function delete_dropin() {
494 - global $wp_filesystem;
495 - ob_start();
663 + $action = sanitize_key( wp_unslash( $_GET['sqlite_object_cache_action'] ) );
496 664
497 - if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
498 - $wp_filesystem->delete( $this->dropinfiledest );
499 - }
665 + if ( 'flush' !== $action ) {
666 + return;
667 + }
500 668
501 - ob_end_clean();
502 - }
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 + }
503 672
504 - /**
505 - * @return bool True if APCu support is activated for this plugin.
506 - */
507 - public function apcu_is_activated() {
508 - return defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU
509 - && $this->apcu_extension_is_enabled();
510 - }
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 + }
511 677
512 - /**
513 - * @return bool True if the APCu extension is loaded and enabled.
514 - */
515 - public function apcu_extension_is_enabled() {
516 - return function_exists( 'apcu_enabled' ) && apcu_enabled();
517 - }
678 + check_admin_referer( 'sqlite_object_cache_flush' );
518 679
519 - /**
520 - * Make sure WP_SQLITE_OBJECT_CACHE_APCU and $option['use_apcu'] match.
521 - *
522 - * @param bool $unconditional true if we should always change the option to match WP_SQLITE_OBJECT_CACHE_APCU.
523 - *
524 - * @return string 'on' or 'off': the current activation state of epcu.
525 - */
526 - public function sync_apcu_global_to_option( $unconditional = true ) {
527 - global $wp_object_cache;
528 - $option = get_option( $this->_token . '_settings', array() );
529 - $option_dirty = false;
530 - $updated_flag = array_key_exists( 'use_apcu_updated', $option );
531 - $use_apcu = array_key_exists( 'use_apcu', $option ) && 'on' === $option['use_apcu'] ? 'on' : 'off';
532 - if ( $updated_flag ) {
533 - unset ( $option['use_apcu_updated'] );
534 - $option_dirty = true;
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;
535 697 }
536 - $target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
537 - if ( ! $unconditional && $updated_flag ) {
538 - $target_use_apcu = $use_apcu;
698 +
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 + }
539 710 }
540 - if ( $target_use_apcu !== $use_apcu ) {
541 - $option['use_apcu'] = $target_use_apcu;
542 - $option_dirty = true;
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
543 723 }
544 - if ( $option_dirty ) {
545 - if ( method_exists( $wp_object_cache, 'apcu-clear_cache' ) ) {
546 - $wp_object_cache->apcu_clear_cache();
547 - }
548 - update_option( $this->_token . '_settings', $option, true );
549 - }
550 -
551 - return $target_use_apcu;
552 - }
553 -
554 724
555 725 }