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

class-sqlite-object-cache.php in SQLite Object Cache 1.6.0, at includes/class-sqlite-object-cache.php

607 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main plugin class file.
4 *
5 * @package WordPress Plugin Template/Includes
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Main plugin class.
14 *
15 * Only make one instance of this, please.
16 */
17 class SQLite_Object_Cache {
18 const CLEAN_EVENT_HOOK = 'sqlite_object_cache_clean';
19
20 /**
21 * Local instance of SQLite_Object_Cache_Admin_API
22 *
23 * @var SQLite_Object_Cache_Admin_API|null
24 */
25 public $admin;
26
27 /**
28 * Settings class object
29 *
30 * @var object
31 * @access public
32 * @since 1.0.0
33 */
34 public $settings;
35
36 /**
37 * The version number.
38 *
39 * @var string
40 * @access public
41 * @since 1.0.0
42 */
43 public $_version;
44
45 /**
46 * The token.
47 *
48 * @var string
49 * @access public
50 * @since 1.0.0
51 */
52 public $_token;
53
54 /**
55 * The main plugin file.
56 *
57 * @var string
58 * @access public
59 * @since 1.0.0
60 */
61 public $file;
62
63 /**
64 * The main plugin directory.
65 *
66 * @var string
67 * @access public
68 * @since 1.0.0
69 */
70 public $dir;
71
72 /**
73 * The plugin assets directory.
74 *
75 * @var string
76 * @access public
77 * @since 1.0.0
78 */
79 public $assets_dir;
80
81 /**
82 * The plugin assets URL.
83 *
84 * @var string
85 * @access public
86 * @since 1.0.0
87 */
88 public $assets_url;
89
90 /**
91 * Suffix for JavaScripts.
92 *
93 * @var string
94 * @access public
95 * @since 1.0.0
96 */
97 public $script_suffix;
98
99 /**
100 * Path for the drop-in when it is working.
101 * @var string
102 */
103 public $dropinfiledest;
104
105 /**
106 * Path for the drop-in in the plugin tree.
107 * @var string
108 */
109 public $dropinfilesource;
110
111 /**
112 * Minimum required sqlite version.
113 *
114 * @var string
115 */
116 public $minimum_sqlite_version = '3.7.0';
117
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.6.0' ) {
125 $this->_version = $version;
126 $this->_token = 'sqlite_object_cache';
127
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';
135
136 $this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) );
137
138 $this->script_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
139
140 register_activation_hook( $this->file, array( $this, 'on_activation' ) );
141 register_deactivation_hook( $this->file, array( $this, 'on_deactivation' ) );
142
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
159 }
160 } );
161 }
162
163 add_action( 'admin_init', array( $this, 'maybe_update_dropin' ) );
164
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 );
173
174
175 /* handle cron cache cleanup */
176 add_action( self::CLEAN_EVENT_HOOK, array( $this, 'clean_job' ), 10, 0 );
177 if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
178 wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
179 }
180 /* Handle probabilistic non-cron cleanup, one request in 2000. */
181 if ( 1 === rand( 1, 2000 ) ) {
182 add_action( 'shutdown', function () {
183 $this->clean_job( 1.25 );
184 }, 999, 0 );
185 }
186 }
187
188 /**
189 * WP_Cron task to clean cache entries.
190 *
191 * @param float $grace_factor Default 1.0
192 *
193 * @return void
194 */
195 public function clean_job( $grace_factor = 1.0 ) {
196 $option = get_option( $this->_token . '_settings', array() );
197 $target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
198 $target_size *= ( 1024 * 1024 );
199 $threshold_size = (int) ( $target_size * $grace_factor );
200
201 global $wp_object_cache;
202 if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
203 return;
204 }
205 /* Clean up old statistics. Do this even when the cache is not over size. */
206 $retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
207 $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
208
209 $current_size = $wp_object_cache->sqlite_get_size();
210 /* Skip this if the current size is small enough. */
211 if ( $current_size <= $threshold_size ) {
212 return;
213 }
214
215 /* Remove expired items (transients mostly). */
216 if ( $wp_object_cache->sqlite_remove_expired() ) {
217 /* If anything was removed, get the size again. */
218 $current_size = $wp_object_cache->sqlite_get_size();
219 }
220
221 /* Skip this if the size is small enough, after removing expired items. */
222 if ( $current_size <= $threshold_size ) {
223 return;
224 }
225
226 /* Delete the least-recently-updated items to get to the target size. */
227 $wp_object_cache->sqlite_delete_old( $target_size, $current_size );
228 }
229
230 /**
231 * Plugin activation hook
232 *
233 * @return void
234 */
235 public function on_activation() {
236
237 $this->sync_apcu_global_to_option( true );
238 if ( true === $this->has_sqlite() ) {
239 add_action( 'shutdown', array( $this, 'update_dropin' ) );
240 add_action( 'shutdown', array( $this, 'delete_all_transients_from_db' ) );
241 }
242 }
243
244 /**
245 * Determine whether we can use SQLite3.
246 *
247 * @return bool|string true, or an error message.
248 */
249 public function has_sqlite() {
250 if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
251 return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
252 }
253
254 $sqlite_version = $this->sqlite_get_version();
255 if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
256 return sprintf(
257 /* translators: 1 actual SQLite version. 2 required SQLite version) */
258 __( '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' ),
259 $sqlite_version, $this->minimum_sqlite_version );
260 }
261
262 return true;
263 }
264
265 /**
266 * Get the version number for the SQLite extension.
267 *
268 * @return string|false SQLite's version number.
269 */
270 public function sqlite_get_version() {
271
272 if ( class_exists( 'SQLite3' ) ) {
273 $version = SQLite3::version();
274
275 return $version['versionString'];
276 }
277
278 return false;
279 }
280
281 /**
282 * Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
283 *
284 * @return true|WP_Error
285 * @author Till Krüss
286 *
287 */
288 public function test_filesystem_writing() {
289 global $wp_filesystem;
290
291 if ( ! $this->initialize_filesystem( '', true ) ) {
292 return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
293 }
294
295 $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
296
297 if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
298 return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
299 }
300
301 if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
302 return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
303 }
304
305 if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
306 return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
307 }
308
309 if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
310 return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
311 }
312
313 if ( ! $wp_filesystem->exists( $testfiledest ) ) {
314 return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
315 }
316
317 $meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
318
319 if ( $meta['Version'] !== $this->_version ) {
320 return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
321 }
322
323 if ( ! $wp_filesystem->delete( $testfiledest ) ) {
324 return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
325 }
326
327 return true;
328 }
329
330 /**
331 * Initializes the WP filesystem API to be ready for use
332 *
333 * @param string $url The URL to post the form to.
334 * @param bool $silent Whether to ask the user for credentials if necessary or not.
335 *
336 * @return bool
337 * @author Till Krüss
338 *
339 */
340 public function initialize_filesystem( $url, $silent = false ) {
341 require_once ABSPATH . 'wp-admin/includes/file.php';
342 if ( $silent ) {
343 ob_start();
344 }
345
346 $credentials = request_filesystem_credentials( $url );
347
348 if ( false === $credentials ) {
349 if ( $silent ) {
350 ob_end_clean();
351 }
352
353 return false;
354 }
355
356 if ( ! WP_Filesystem( $credentials ) ) {
357 request_filesystem_credentials( $url );
358
359 if ( $silent ) {
360 ob_end_clean();
361 }
362
363 return false;
364 }
365
366 return true;
367 }
368
369 /**
370 * Calls the drop-in update method if necessary
371 *
372 * @return void
373 * @author Till Krüss
374 */
375 public function maybe_update_dropin() {
376 if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
377 return;
378 }
379
380 $has = $this->has_sqlite();
381 if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
382 add_action( 'shutdown', array( $this, 'update_dropin' ) );
383 }
384 }
385
386 /**
387 * Checks if the `object-cache.php` drop-in is outdated
388 * @return bool
389 * @author Till Krüss
390 *
391 */
392 public function object_cache_dropin_needs_updating() {
393 global $wp_object_cache;
394 if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
395 return true;
396 }
397 return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
398 }
399
400 /**
401 * Updates the `object-cache.php` drop-in
402 *
403 * @return void
404 * @author Till Krüss
405 */
406 public function update_dropin() {
407 global $wp_filesystem;
408 $has = $this->has_sqlite();
409 if ( true === $has && $this->initialize_filesystem( '', true ) ) {
410
411 $this->delete_sqlite_files();
412 $result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
413 /**
414 * Fires on cache enable event
415 *
416 * @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
417 *
418 * @since 0.1.0
419 */
420 do_action( 'sqlite_object_cache_update_dropin', $result );
421 }
422 }
423
424 /**
425 * Validates the `object-cache.php` drop-in
426 *
427 * @return bool
428 * @author Till Krüss
429 */
430 public function validate_object_cache_dropin() {
431 global $wp_object_cache;
432
433 if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
434 return false;
435 }
436
437 $dropin = get_plugin_data( $this->dropinfiledest );
438 $plugin = get_plugin_data( $this->dropinfilesource );
439
440 /**
441 * Filters the drop-in validation state
442 *
443 * @param bool $state The validation state of the drop-in.
444 * @param string $dropin The `PluginURI` of the drop-in.
445 * @param string $plugin The `PluginURI` of the plugin.
446 *
447 * @since 2.0.16
448 */
449 return apply_filters(
450 'sqlite_object_cache_validate_dropin',
451 $dropin['PluginURI'] === $plugin['PluginURI'],
452 $dropin['PluginURI'],
453 $plugin['PluginURI']
454 );
455 }
456
457 /**
458 * Plugin deactivation hook
459 *
460 * @param string $plugin Plugin basename.
461 *
462 * @return void
463 * @author Till Krüss
464 */
465 public function on_deactivation( $plugin ) {
466
467 wp_cache_flush();
468
469 wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
470 $this->delete_sqlite_files();
471 $this->delete_dropin();
472 $this->delete_all_transients_from_db();
473 }
474
475 private function delete_sqlite_files() {
476 global $wp_filesystem;
477 global $wp_object_cache;
478
479 if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
480 if ( $this->initialize_filesystem( '', true ) ) {
481 foreach ( $wp_object_cache->sqlite_files() as $file ) {
482 $wp_filesystem->delete( $file );
483 }
484 }
485 }
486 }
487
488 private function delete_dropin() {
489 global $wp_filesystem;
490 ob_start();
491
492 if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
493 $wp_filesystem->delete( $this->dropinfiledest );
494 }
495
496 ob_end_clean();
497 }
498
499 /**
500 * Deletes all transients from the database.
501 *
502 * It is necessary to do this when deactivating a persistent object cache
503 * because the transients are kept there, and the ones in the database are
504 * possibly stale.
505 *
506 * This hammers performance. But deactivating a persistent object cache is
507 * not a common operation.
508 *
509 * @global wpdb $wpdb WordPress database abstraction object.
510 */
511 public function delete_all_transients_from_db() {
512 global $wpdb;
513 if ( ! is_multisite() ) {
514 $this->clear_blog_transients();
515 // Single site stores site transients in the options table.
516 $this->clear_blog_transients( '_site_transient_' );
517 } else {
518 foreach ( get_sites( array( 'number' => 0, 'fields' => 'ids', 'no_found_rows' => true, 'orderby' => false ) ) as $site_id ) {
519 try {
520 switch_to_blog( $site_id );
521 $this->clear_blog_transients();
522 } catch ( Exception $ex ) {
523 /* Avoid crashes on transient clearing */
524 error_log( 'SQLite Object Cache problem clearing transients. Blog ' . $site_id . ':' . $ex->getMessage() );
525 } finally {
526 restore_current_blog();
527 }
528 }
529 // Multisite stores site transients in the sitemeta table.
530 $wpdb->query(
531 $wpdb->prepare(
532 "DELETE FROM {$wpdb->sitemeta} WHERE a.meta_key LIKE %s",
533 $wpdb->esc_like( '_site_transient_' ) . '%'
534 )
535 );
536 }
537 }
538
539 /**
540 * DELETE transients from the current blog's options table.
541 *
542 * @param string $tag _site_transient_ or _transient_, the default.
543 *
544 * @return void
545 */
546 private function clear_blog_transients( $tag = '_transient_') {
547 global $wpdb;
548 $wpdb->query(
549 $wpdb->prepare(
550 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
551 $wpdb->esc_like( $tag ) . '%'
552 )
553 );
554 }
555
556 /**
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 }
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 }
570
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;
587 }
588 $target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
589 if ( ! $unconditional && $updated_flag ) {
590 $target_use_apcu = $use_apcu;
591 }
592 if ( $target_use_apcu !== $use_apcu ) {
593 $option['use_apcu'] = $target_use_apcu;
594 $option_dirty = true;
595 }
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 );
601 }
602
603 return $target_use_apcu;
604 }
605
606 }
607