_version = $version;
$this->_token = 'sqlite_object_cache';
// Load plugin environment variables.
$this->file = $file;
$this->dir =
trailingslashit( dirname( trailingslashit( WP_PLUGIN_DIR ) . plugin_basename( $this->file ) ) );
$this->assets_dir = trailingslashit( $this->dir . 'assets' );
$this->dropinfilesource = $this->assets_dir . 'drop-in/object-cache.php';
$this->dropinfiledest = trailingslashit( WP_CONTENT_DIR ) . 'object-cache.php';
$this->assets_url = esc_url( trailingslashit( plugins_url( '/assets/', $this->file ) ) );
$this->script_suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
register_activation_hook( $this->file, array( $this, 'on_activation' ) );
register_deactivation_hook( $this->file, array( $this, 'on_deactivation' ) );
if ( is_admin() ) {
// Load API for generic admin functions.
$this->admin = new SQLite_Object_Cache_Admin_API();
// Suppress backups
new SQLite_Backup_Exclusion();
add_action( 'admin_notices', function () {
$purged = get_site_transient( 'sqlite-object-cache-flush-on-update' );
if ( $purged ) {
delete_site_transient( 'sqlite-object-cache-flush-on-update' );
$message = esc_html( __( 'A software installation or upgrade operation flushed the SQLite Object Cache.', 'sqlite-object-cache' ) )
?>
clean_job( 1.25 );
}, 999, 0 );
}
}
/**
* WP_Cron task to clean cache entries.
*
* @param float $grace_factor Default 1.0
*
* @return void
*/
public function clean_job( $grace_factor = 1.0 ) {
$option = get_option( $this->_token . '_settings', array() );
$target_size = empty ( $option['target_size'] ) ? 16 : $option['target_size'];
$target_size *= ( 1024 * 1024 );
$threshold_size = (int) ( $target_size * $grace_factor );
global $wp_object_cache;
if ( ! method_exists( $wp_object_cache, 'sqlite_get_size' ) ) {
return;
}
/* Clean up old statistics. Do this even when the cache is not over size. */
$retention = empty ( $option['retainmeasurements'] ) ? 24 : $option['retainmeasurements'];
$wp_object_cache->sqlite_reset_statistics( $retention * HOUR_IN_SECONDS );
$current_size = $wp_object_cache->sqlite_get_size();
/* Skip this if the current size is small enough. */
if ( $current_size <= $threshold_size ) {
return;
}
/* Remove expired items (transients mostly). */
if ( $wp_object_cache->sqlite_remove_expired() ) {
/* If anything was removed, get the size again. */
$current_size = $wp_object_cache->sqlite_get_size();
}
/* Skip this if the size is small enough, after removing expired items. */
if ( $current_size <= $threshold_size ) {
return;
}
/* Delete the least-recently-updated items to get to the target size. */
$wp_object_cache->sqlite_delete_old( $target_size, $current_size );
}
/**
* Plugin activation hook
*
* @return void
*/
public function on_activation() {
$this->sync_apcu_global_to_option( true );
if ( true === $this->has_sqlite() ) {
add_action( 'shutdown', array( $this, 'update_dropin' ) );
add_action( 'shutdown', array( $this, 'delete_all_transients_from_db' ) );
}
}
/**
* Determine whether we can use SQLite3.
*
* @return bool|string true, or an error message.
*/
public function has_sqlite() {
if ( ! class_exists( 'SQLite3' ) || ! extension_loaded( 'sqlite3' ) ) {
return __( 'You cannot use the SQLite Object Cache plugin. Your server does not have php\'s SQLite3 extension installed.', 'sqlite-object-cache' );
}
$sqlite_version = $this->sqlite_get_version();
if ( version_compare( $sqlite_version, $this->minimum_sqlite_version ) < 0 ) {
return sprintf(
/* translators: 1 actual SQLite version. 2 required SQLite version) */
__( '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' ),
$sqlite_version, $this->minimum_sqlite_version );
}
return true;
}
/**
* Get the version number for the SQLite extension.
*
* @return string|false SQLite's version number.
*/
public function sqlite_get_version() {
if ( class_exists( 'SQLite3' ) ) {
$version = SQLite3::version();
return $version['versionString'];
}
return false;
}
/**
* Test if we can write in the WP_CONTENT_DIR and modify the `object-cache.php` drop-in
*
* @return true|WP_Error
* @author Till Krüss
*
*/
public function test_filesystem_writing() {
global $wp_filesystem;
if ( ! $this->initialize_filesystem( '', true ) ) {
return new WP_Error( 'fs', __( 'Could not initialize filesystem.', 'sqlite-object-cache' ) );
}
$testfiledest = WP_CONTENT_DIR . '/sqlite-write-test.tmp';
if ( ! $wp_filesystem->exists( $this->dropinfilesource ) ) {
return new WP_Error( 'exists', __( 'Object cache drop-in file doesn’t exist.', 'sqlite-object-cache' ) );
}
if ( $wp_filesystem->exists( $testfiledest ) && ! $wp_filesystem->delete( $testfiledest ) ) {
return new WP_Error( 'delete', __( 'Test file exists, but couldn’t be deleted.', 'sqlite-object-cache' ) );
}
if ( ! $wp_filesystem->is_writable( WP_CONTENT_DIR ) ) {
return new WP_Error( 'copy', __( 'Content directory is not writable.', 'sqlite-object-cache' ) );
}
if ( ! $wp_filesystem->copy( $this->dropinfilesource, $testfiledest, true, FS_CHMOD_FILE ) ) {
return new WP_Error( 'copy', __( 'Failed to copy test file.', 'sqlite-object-cache' ) );
}
if ( ! $wp_filesystem->exists( $testfiledest ) ) {
return new WP_Error( 'exists', __( 'Copied test file doesn’t exist.', 'sqlite-object-cache' ) );
}
$meta = get_file_data( $testfiledest, array( 'Version' => 'Version' ) );
if ( $meta['Version'] !== $this->_version ) {
return new WP_Error( 'version', __( 'Couldn’t verify test file contents.', 'sqlite-object-cache' ) );
}
if ( ! $wp_filesystem->delete( $testfiledest ) ) {
return new WP_Error( 'delete', __( 'Copied test file couldn’t be deleted.', 'sqlite-object-cache' ) );
}
return true;
}
/**
* Initializes the WP filesystem API to be ready for use
*
* @param string $url The URL to post the form to.
* @param bool $silent Whether to ask the user for credentials if necessary or not.
*
* @return bool
* @author Till Krüss
*
*/
public function initialize_filesystem( $url, $silent = false ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
if ( $silent ) {
ob_start();
}
$credentials = request_filesystem_credentials( $url );
if ( false === $credentials ) {
if ( $silent ) {
ob_end_clean();
}
return false;
}
if ( ! WP_Filesystem( $credentials ) ) {
request_filesystem_credentials( $url );
if ( $silent ) {
ob_end_clean();
}
return false;
}
return true;
}
/**
* Calls the drop-in update method if necessary
*
* @return void
* @author Till Krüss
*/
public function maybe_update_dropin() {
if ( defined( 'WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE' ) && WP_SQLITE_OBJECT_CACHE_DISABLE_DROPIN_AUTOUPDATE ) {
return;
}
$has = $this->has_sqlite();
if ( ( true === $has ) && $this->object_cache_dropin_needs_updating() ) {
add_action( 'shutdown', array( $this, 'update_dropin' ) );
}
}
/**
* Checks if the `object-cache.php` drop-in is outdated
* @return bool
* @author Till Krüss
*
*/
public function object_cache_dropin_needs_updating() {
global $wp_object_cache;
if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
return true;
}
return version_compare( $wp_object_cache->dropin_get_version(), $this->_version, '<' );
}
/**
* Updates the `object-cache.php` drop-in
*
* @return void
* @author Till Krüss
*/
public function update_dropin() {
global $wp_filesystem;
$has = $this->has_sqlite();
if ( true === $has && $this->initialize_filesystem( '', true ) ) {
$this->delete_sqlite_files();
$result = $wp_filesystem->copy( $this->dropinfilesource, $this->dropinfiledest, true, FS_CHMOD_FILE );
/**
* Fires on cache enable event
*
* @param bool $result Whether the filesystem event (copy of the `object-cache.php` file) was successful.
*
* @since 0.1.0
*/
do_action( 'sqlite_object_cache_update_dropin', $result );
}
}
/**
* Validates the `object-cache.php` drop-in
*
* @return bool
* @author Till Krüss
*/
public function validate_object_cache_dropin() {
global $wp_object_cache;
if ( ! method_exists( $wp_object_cache, 'dropin_get_version' ) ) {
return false;
}
$dropin = get_plugin_data( $this->dropinfiledest );
$plugin = get_plugin_data( $this->dropinfilesource );
/**
* Filters the drop-in validation state
*
* @param bool $state The validation state of the drop-in.
* @param string $dropin The `PluginURI` of the drop-in.
* @param string $plugin The `PluginURI` of the plugin.
*
* @since 2.0.16
*/
return apply_filters(
'sqlite_object_cache_validate_dropin',
$dropin['PluginURI'] === $plugin['PluginURI'],
$dropin['PluginURI'],
$plugin['PluginURI']
);
}
/**
* Plugin deactivation hook
*
* @param string $plugin Plugin basename.
*
* @return void
* @author Till Krüss
*/
public function on_deactivation( $plugin ) {
wp_cache_flush();
wp_unschedule_hook( self::CLEAN_EVENT_HOOK );
$this->delete_sqlite_files();
$this->delete_dropin();
$this->delete_all_transients_from_db();
}
private function delete_sqlite_files() {
global $wp_filesystem;
global $wp_object_cache;
if ( method_exists( $wp_object_cache, 'sqlite_files' ) ) {
if ( $this->initialize_filesystem( '', true ) ) {
foreach ( $wp_object_cache->sqlite_files() as $file ) {
$wp_filesystem->delete( $file );
}
}
}
}
private function delete_dropin() {
global $wp_filesystem;
ob_start();
if ( $this->validate_object_cache_dropin() && $this->initialize_filesystem( '', true ) ) {
$wp_filesystem->delete( $this->dropinfiledest );
}
ob_end_clean();
}
/**
* Deletes all transients from the database.
*
* It is necessary to do this when deactivating a persistent object cache
* because the transients are kept there, and the ones in the database are
* possibly stale.
*
* This hammers performance. But deactivating a persistent object cache is
* not a common operation.
*
* @global wpdb $wpdb WordPress database abstraction object.
*/
public function delete_all_transients_from_db() {
global $wpdb;
if ( ! is_multisite() ) {
$this->clear_blog_transients();
// Single site stores site transients in the options table.
$this->clear_blog_transients( '_site_transient_' );
} else {
foreach ( get_sites( array( 'number' => 0, 'fields' => 'ids', 'no_found_rows' => true, 'orderby' => false ) ) as $site_id ) {
try {
switch_to_blog( $site_id );
$this->clear_blog_transients();
} catch ( Exception $ex ) {
/* Avoid crashes on transient clearing */
error_log( 'SQLite Object Cache problem clearing transients. Blog ' . $site_id . ':' . $ex->getMessage() );
} finally {
restore_current_blog();
}
}
// Multisite stores site transients in the sitemeta table.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->sitemeta} WHERE a.meta_key LIKE %s",
$wpdb->esc_like( '_site_transient_' ) . '%'
)
);
}
}
/**
* DELETE transients from the current blog's options table.
*
* @param string $tag _site_transient_ or _transient_, the default.
*
* @return void
*/
private function clear_blog_transients( $tag = '_transient_') {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like( $tag ) . '%'
)
);
}
/**
* @return bool True if APCu support is activated for this plugin.
*/
public function apcu_is_activated() {
return defined( 'WP_SQLITE_OBJECT_CACHE_APCU' ) && WP_SQLITE_OBJECT_CACHE_APCU
&& $this->apcu_extension_is_enabled();
}
/**
* @return bool True if the APCu extension is loaded and enabled.
*/
public function apcu_extension_is_enabled() {
return function_exists( 'apcu_enabled' ) && apcu_enabled();
}
/**
* Make sure WP_SQLITE_OBJECT_CACHE_APCU and $option['use_apcu'] match.
*
* @param bool $unconditional true if we should always change the option to match WP_SQLITE_OBJECT_CACHE_APCU.
*
* @return string 'on' or 'off': the current activation state of epcu.
*/
public function sync_apcu_global_to_option( $unconditional = true ) {
global $wp_object_cache;
$option = get_option( $this->_token . '_settings', array() );
$option_dirty = false;
$updated_flag = array_key_exists( 'use_apcu_updated', $option );
$use_apcu = array_key_exists( 'use_apcu', $option ) && 'on' === $option['use_apcu'] ? 'on' : 'off';
if ( $updated_flag ) {
unset ( $option['use_apcu_updated'] );
$option_dirty = true;
}
$target_use_apcu = $this->apcu_is_activated() ? 'on' : 'off';
if ( ! $unconditional && $updated_flag ) {
$target_use_apcu = $use_apcu;
}
if ( $target_use_apcu !== $use_apcu ) {
$option['use_apcu'] = $target_use_apcu;
$option_dirty = true;
}
if ( $option_dirty ) {
if ( method_exists( $wp_object_cache, 'apcu-clear_cache' ) ) {
$wp_object_cache->apcu_clear_cache();
}
update_option( $this->_token . '_settings', $option, true );
}
return $target_use_apcu;
}
}