PluginProbe
SQLite Object Cache / 1.0.0
SQLite Object Cache v1.0.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.0.0, at includes/class-sqlite-object-cache.php

543 lines 13.9 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 funtion.
120 *
121 * @param string $file File constructor.
122 * @param string $version Plugin version.
123 */
124 public function __construct( $file = '', $version = '1.0.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( 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';
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, [ $this, 'on_activation' ] );
141 register_deactivation_hook( $this->file, [ $this, 'on_deactivation' ] );
142
143 // Load API for generic admin functions.
144 if ( is_admin() ) {
145 $this->admin = new SQLite_Object_Cache_Admin_API();
146 }
147
148 // Handle localization.
149 $this->load_plugin_textdomain();
150 add_action( 'admin-init', [ $this, 'load_localization' ], 0 );
151 add_action( 'admin_init', [ $this, 'maybe_update_dropin' ] );
152
153 /* Are we capturing SQLite Object Cache statistics? */
154 $option = get_option( $this->_token . '_settings', [] );
155 if ( is_array( $option ) && array_key_exists( 'capture', $option ) && $option['capture'] === 'on' ) {
156 add_action( 'init', [ $this, 'do_capture' ] );
157 }
158
159 /* handle cron cache cleanup */
160 add_action( self::CLEAN_EVENT_HOOK, [ $this, 'clean' ], 10, 0 );
161 if ( ! wp_next_scheduled( self::CLEAN_EVENT_HOOK ) ) {
162 wp_schedule_event( time() + HOUR_IN_SECONDS, 'hourly', self::CLEAN_EVENT_HOOK );
163 }
164 }
165
166 /**
167 * Load plugin textdomain
168 *
169 * @access public
170 * @return void
171 * @since 1.0.0
172 */
173 public function load_plugin_textdomain() {
174 $domain = 'sqlite-object-cache';
175
176 $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
177
178 load_textdomain( $domain, WP_LANG_DIR . '/' . $domain . '/' . $domain . '-' . $locale . '.mo' );
179 load_plugin_textdomain( $domain, false, dirname( plugin_basename( $this->file ) ) . '/languages/' );
180 }
181
182 /**
183 * WP_Cron task to clean cache entries.
184 *
185 * @return void
186 */
187 public function clean() {
188 global $wp_object_cache;
189 $option = get_option( $this->_token . '_settings', [] );
190 if ( method_exists( $wp_object_cache, 'sqlite_clean_up_cache' ) ) {
191 $retention = array_key_exists( 'retention', $option ) ? $option['retention'] : 24;
192 try {
193 $wp_object_cache->sqlite_clean_up_cache( $retention * HOUR_IN_SECONDS, true, true );
194 } catch ( Exception $ex ) {
195 error_log( 'sqlite_object_cache cache cleanup failure: ', $ex->getMessage() );
196 }
197 }
198
199 if ( method_exists( $wp_object_cache, 'sqlite_reset_statistics' ) ) {
200 $retention = array_key_exists( 'retainmeasurements', $option ) ? $option['retainmeasurements'] : 24;
201 try {
202 $wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
203 } catch ( Exception $ex ) {
204 error_log( 'sqlite_object_cache stats cleanup failure: ', $ex->getMessage() );
205 }
206 }
207 } // End enqueue_styles ()
208
209 /**
210 * Plugin activation hook
211 *
212 * @return void
213 */
214 public function on_activation() {
215 if ( true === $this->has_sqlite() ) {
216 add_action( 'shutdown', [ $this, 'update_dropin' ] );
217 }
218 }
219
220 /**
221 * Determine whether we can use SQLite3.
222 *
223 * @return bool|string true, or an error message.
224 */
225 public function has_sqlite() {
226 if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
227 return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
228 }
229
230 $sqlite_version = $this->sqlite_get_version();
231 if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
232 return sprintf(
233 /* translators: 1 actual SQLite version. 2 required SQLite version) */
234 __( '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' ),
235 $sqlite_version, $this->minimum_sqlite_version );
236 }
237
238 return true;
239 }
240
241 /**
242 * Get the version number for the SQLite extension.
243 *
244 * @return string|false SQLite's version number.
245 */
246 public function sqlite_get_version() {
247
248 if ( class_exists( 'SQLite3' ) ) {
249 $version = SQLite3::version();
250
251 return $version['versionString'];
252 }
253
254 return false;
255 }
256
257 /**
258 * Load plugin localization
259 *
260 * @access public
261 * @return void
262 * @since 1.0.0
263 */
264 public function load_localization() {
265 load_plugin_textdomain( 'sqlite-object-cache', false, dirname( plugin_basename( $this->file ) ) . '/languages/' );
266 }
267
268 /**
269 * Init action to initiate statistics capture from this page view.
270 *
271 * @return void
272 */
273 public function do_capture() {
274 $option = get_option( $this->_token . '_settings' );
275 if ( ! is_array( $option ) || ! array_key_exists( 'frequency', $option ) || ! array_key_exists( 'retainmeasurements', $option ) ) {
276 /* Something's wrong with the option. Just ignore it. */
277 return;
278 }
279 $frequency = $option['frequency'];
280 $frequency = $frequency ?: 1;
281 $headway = (int) ( HOUR_IN_SECONDS / $frequency );
282 $now = time();
283 $previouscapture = array_key_exists( 'previouscapture', $option ) ? $option['previouscapture'] : 0;
284 $sincelastcapture = $previouscapture > 0 ? $now - $previouscapture : $headway;
285 if ( $sincelastcapture >= $headway ) {
286 /* Time for a capture! */
287 global $wp_object_cache;
288 if ( method_exists( $wp_object_cache, 'set_sqlite_monitoring_options' ) ) {
289 $monitoring_options = [
290 'capture' => true,
291 'resolution' => $headway,
292 'lifetime' => $option['retainmeasurements'] * HOUR_IN_SECONDS,
293 'verbose' => true,
294 ];
295 $wp_object_cache->set_sqlite_monitoring_options( $monitoring_options );
296 }
297 $previouscapture = $now;
298 $option['previouscapture'] = $previouscapture;
299 update_option( $this->_token . '_settings', $option );
300 }
301 }
302
303 /**
304 * Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
305 *
306 * @return true|WP_Error
307 * @author Till Krüss
308 *
309 */
310 public function test_filesystem_writing() {
311 global $wp_filesystem;
312
313 if ( ! $this->initialize_filesystem( '', true ) ) {
314 return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
315 }
316
317 $testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
318
319 if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
320 return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
321 }
322
323 if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
324 return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
325 }
326
327 if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
328 return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
329 }
330
331 if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
332 return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
333 }
334
335 if ( ! $wp_filesystem->exists( $testfiledest ) ) {
336 return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
337 }
338
339 $meta = get_file_data( $testfiledest, [ 'Version' => 'Version' ] );
340
341 if ( $meta['Version'] !== $this->_version ) {
342 return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
343 }
344
345 if ( ! $wp_filesystem->delete( $testfiledest ) ) {
346 return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
347 }
348
349 return true;
350 }
351
352 /**
353 * Initializes the WP filesystem API to be ready for use
354 *
355 * @param string $url The URL to post the form to.
356 * @param bool $silent Whether to ask the user for credentials if necessary or not.
357 *
358 * @return bool
359 * @author Till Krüss
360 *
361 */
362 public function initialize_filesystem( $url, $silent = false ) {
363 require_once ABSPATH . 'wp-admin/includes/file.php';;
364 if ( $silent ) {
365 ob_start();
366 }
367
368 $credentials = request_filesystem_credentials( $url );
369
370 if ( false === $credentials ) {
371 if ( $silent ) {
372 ob_end_clean();
373 }
374
375 return false;
376 }
377
378 if ( ! WP_Filesystem( $credentials ) ) {
379 request_filesystem_credentials( $url );
380
381 if ( $silent ) {
382 ob_end_clean();
383 }
384
385 return false;
386 }
387
388 return true;
389 }
390
391 /**
392 * Calls the drop-in update method if necessary
393 *
394 * @return void
395 * @author Till Krüss
396 */
397 public function maybe_update_dropin() {
398 if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
399 return;
400 }
401
402 $has = $this->has_sqlite();
403 if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
404 add_action( 'shutdown', [ $this, 'update_dropin' ] );
405 }
406 }
407
408 /**
409 * Checks if the `object-cache.php` drop-in is outdated
410 * @return bool
411 * @author Till Krüss
412 *
413 */
414 public function object_cache_dropin_needs_updating() {
415 if ( ! $this->object_cache_dropin_exists() ) {
416 return true;
417 }
418
419 $dropin = get_plugin_data( $this->dropinfiledest );
420 $plugin = get_plugin_data( $this->dropinfilesource );
421
422 if ( $dropin['PluginURI'] === $plugin['PluginURI'] ) {
423 return version_compare( $dropin['Version'], $plugin['Version'], '<' );
424 }
425
426 return false;
427 }
428
429 /**
430 * Checks if the `object-cache.php` drop-in exists
431 *
432 * @return bool
433 * @author Till Krüss
434 */
435 public function object_cache_dropin_exists() {
436 return @file_exists( $this->dropinfiledest );
437 }
438
439 /**
440 * Updates the `object-cache.php` drop-in
441 *
442 * @return void
443 * @author Till Krüss
444 */
445 public function update_dropin() {
446 global $wp_filesystem;
447 $has = $this->has_sqlite();
448 if ( true === $has && $this->initialize_filesystem( '', true ) ) {
449
450 $this->delete_sqlite_files();
451 $result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
452 /**
453 * Fires on cache enable event
454 *
455 * @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
456 *
457 * @since 0.1.0
458 */
459 do_action( 'sqlite_object_cache_update_dropin', $result );
460 }
461 }
462
463 private function delete_sqlite_files() {
464 global $wp_filesystem;
465
466 /* the former filename */
467 $old_file = WP_CONTENT_DIR . '/object-cache.sqlite';
468 $db_file = WP_CONTENT_DIR . '/.ht.object-cache.sqlite';
469 if ( defined( 'WP_SQLITE_OBJECT_CACHE_DB_FILE' ) ) {
470 $db_file = WP_SQLITE_OBJECT_CACHE_DB_FILE;
471 }
472
473 ob_start();
474
475 if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
476 foreach ( [ '', '-shm', '-wal' ] as $suffix ) {
477 $wp_filesystem->delete( $db_file . $suffix );
478 $wp_filesystem->delete( $old_file . $suffix );
479 }
480 }
481
482 ob_end_clean();
483 }
484
485 /**
486 * Validates the `object-cache.php` drop-in
487 *
488 * @return bool
489 * @author Till Krüss
490 */
491 public function validate_object_cache_dropin() {
492 if ( ! $this->object_cache_dropin_exists() ) {
493 return false;
494 }
495
496 $dropin = get_plugin_data( $this->dropinfiledest );
497 $plugin = get_plugin_data( $this->dropinfilesource );
498
499 /**
500 * Filters the drop-in validation state
501 *
502 * @param bool $state The validation state of the drop-in.
503 * @param string $dropin The `PluginURI` of the drop-in.
504 * @param string $plugin The `PluginURI` of the plugin.
505 *
506 * @since 2.0.16
507 */
508 return apply_filters(
509 'sqlite_object_cache_validate_dropin',
510 $dropin['PluginURI'] === $plugin['PluginURI'],
511 $dropin['PluginURI'],
512 $plugin['PluginURI']
513 );
514 }
515
516 /**
517 * Plugin deactivation hook
518 *
519 * @param string $plugin Plugin basename.
520 *
521 * @return void
522 * @author Till Krüss
523 */
524 public function on_deactivation( $plugin ) {
525 wp_cache_flush();
526
527 wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
528 $this->delete_sqlite_files();
529 $this->delete_dropin();
530 }
531
532 private function delete_dropin() {
533 global $wp_filesystem;
534 ob_start();
535
536 if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
537 $wp_filesystem->delete( $this->dropinfiledest );
538 }
539
540 ob_end_clean();
541 }
542 }
543