diff --git a/README.md b/README.md index 7692f71..463dd10 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![CS](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/cs.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/cs.yml) -[![PHPStan](https://github.com/Emilia-Capital/aaa-option-optimizer/actions/workflows/phpstan.yml/badge.svg)](https://github.com/Emilia-Capital/aaa-option-optimizer/actions/workflows/phpstan.yml) -[![Lint](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/lint.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/lint.yml) -[![Security](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/security.yml/badge.svg)](https://github.com/emilia-capital/aaa-option-optimizer/actions/workflows/security.yml) +[![CS](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/cs.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/cs.yml) +[![PHPStan](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/phpstan.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/phpstan.yml) +[![Lint](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/lint.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/lint.yml) +[![Security](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/security.yml/badge.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/actions/workflows/security.yml) [![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/aaa-option-optimizer.svg)](https://wordpress.org/plugins/aaa-option-optimizer/) ![WordPress Plugin: Tested WP Version](https://img.shields.io/wordpress/plugin/tested/aaa-option-optimizer.svg) @@ -10,7 +10,7 @@ [![WordPress Plugin Rating](https://img.shields.io/wordpress/plugin/stars/aaa-option-optimizer.svg)](https://wordpress.org/support/plugin/aaa-option-optimizer/reviews/) [![GitHub](https://img.shields.io/github/license/ProgressPlanner/aaa-option-optimizer.svg)](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/main/LICENSE) -[![Try this plugin on the WordPress playground](https://img.shields.io/badge/Try%20this%20plugin%20on%20the%20WordPress%20Playground-%23117AC9.svg?style=for-the-badge&logo=WordPress&logoColor=ddd)](https://playground.wordpress.net/#%7B%22landingPage%22:%22/wp-admin/tools.php?page=aaa-option-optimizer%22,%22features%22:%7B%22networking%22:true%7D,%22steps%22:%5B%7B%22step%22:%22defineWpConfigConsts%22,%22consts%22:%7B%22IS_PLAYGROUND_PREVIEW%22:true%7D%7D,%7B%22step%22:%22login%22,%22username%22:%22admin%22,%22password%22:%22password%22%7D,%7B%22step%22:%22installPlugin%22,%22pluginZipFile%22:%7B%22resource%22:%22url%22,%22url%22:%22https://bypass-cors.altha.workers.dev/https://github.com/Emilia-Capital/aaa-option-optimizer/archive/refs/heads/develop.zip%22%7D,%22options%22:%7B%22activate%22:true%7D%7D%5D%7D) +[![Try this plugin on the WordPress playground](https://img.shields.io/badge/Try%20this%20plugin%20on%20the%20WordPress%20Playground-%23117AC9.svg?style=for-the-badge&logo=WordPress&logoColor=ddd)](https://playground.wordpress.net/#%7B%22landingPage%22:%22/wp-admin/tools.php?page=aaa-option-optimizer%22,%22features%22:%7B%22networking%22:true%7D,%22steps%22:%5B%7B%22step%22:%22defineWpConfigConsts%22,%22consts%22:%7B%22IS_PLAYGROUND_PREVIEW%22:true%7D%7D,%7B%22step%22:%22login%22,%22username%22:%22admin%22,%22password%22:%22password%22%7D,%7B%22step%22:%22installPlugin%22,%22pluginZipFile%22:%7B%22resource%22:%22url%22,%22url%22:%22https://bypass-cors.altha.workers.dev/https://github.com/ProgressPlanner/aaa-option-optimizer/archive/refs/heads/develop.zip%22%7D,%22options%22:%7B%22activate%22:true%7D%7D%5D%7D) ![GitHub banner](/.wordpress-org/github_banner_aaaoo_pp.png) @@ -26,7 +26,7 @@ Install this plugin, and go through your entire site. Best is to use it normally ### Why the AAA prefix in the plugin name? -Because the plugin needs to measure options being loaded, it benefits from being loaded itself first. As WordPress loads plugins alphabetically, +Because the plugin needs to measure options being loaded, it benefits from being loaded itself first. As WordPress loads plugins alphabetically, starting the name with AAA made sense. ### Do I need to take precautions? @@ -35,7 +35,7 @@ Yes!! Backup your database. ### How can I add recognized plugins? -Please do a pull request via GitHub on [this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. +Please do a pull request via GitHub on [this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. ### How can I report security bugs? diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php index c568fa6..057afeb 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -2,7 +2,7 @@ /** * Plugin that tracks autoloaded options usage and allows the user to optimize them. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer * * Plugin Name: AAA Option Optimizer * Plugin URI: https://progressplanner.com/plugins/aaa-option-optimizer/ @@ -27,12 +27,16 @@ register_deactivation_hook( __FILE__, 'aaa_option_optimizer_deactivation' ); /** - * Activation hooked function to store start stats. + * Activation hooked function to store start stats and create table. * * @return void */ function aaa_option_optimizer_activation() { global $wpdb; + + // Create the custom table. + Progress_Planner\OptionOptimizer\Database::create_table(); + $autoload_values = \wp_autoload_values_to_autoload(); $placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) ); @@ -42,19 +46,23 @@ function aaa_option_optimizer_activation() { ); // phpcs:enable WordPress.DB - update_option( - 'option_optimizer', - [ - 'starting_point_kb' => ( $result->autoload_size / 1024 ), - 'starting_point_num' => $result->count, - 'starting_point_date' => current_time( 'mysql' ), - 'used_options' => [], - 'settings' => [ - 'option_tracking' => 'pre_option', + // Only set starting point if not already set (preserve existing data). + $existing = get_option( 'option_optimizer' ); + if ( empty( $existing['starting_point_date'] ) ) { + update_option( + 'option_optimizer', + [ + 'starting_point_kb' => ( $result->autoload_size / 1024 ), + 'starting_point_num' => $result->count, + 'starting_point_date' => current_time( 'mysql' ), + 'used_options' => [], // For backward compatibility. + 'settings' => [ + 'option_tracking' => 'pre_option', + ], ], - ], - false - ); + false + ); + } } /** @@ -67,13 +75,33 @@ function aaa_option_optimizer_deactivation() { update_option( 'option_optimizer', $aaa_option_value, false ); } +/** + * Ensure database table exists. + * Runs on plugins_loaded to handle existing installs that don't trigger activation. + * Migration is handled via AJAX on the plugin admin page. + * + * @return void + */ +function aaa_option_optimizer_maybe_upgrade() { + // Only run on admin pages, not on AJAX or REST requests to avoid race conditions. + if ( ! is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) { + return; + } + + // Check if table exists, create if not. + if ( ! Progress_Planner\OptionOptimizer\Database::table_exists() ) { + Progress_Planner\OptionOptimizer\Database::create_table(); + } +} +add_action( 'plugins_loaded', 'aaa_option_optimizer_maybe_upgrade' ); + /** * Initializes the plugin. * * @return void */ function aaa_option_optimizer_init() { - $optimizer = new Emilia\OptionOptimizer\Plugin(); + $optimizer = new Progress_Planner\OptionOptimizer\Plugin(); $optimizer->register_hooks(); } diff --git a/composer.json b/composer.json index 918fc4f..8ee7555 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "emilia/aaa-option-optimizer", + "name": "progress-planner/aaa-option-optimizer", "description": "Plugin that tracks autoloaded options usage and allows the user to optimize them.", "type": "wordpress-plugin", "license": "GPL-3.0-or-later", diff --git a/composer.lock b/composer.lock index 68b99dc..d186fc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b719f200626be0650504e5dddce6534d", + "content-hash": "68e73cf23844dd66f4d8b3b14eef46cd", "packages": [], "packages-dev": [ { diff --git a/js/admin-script.js b/js/admin-script.js index 45cfd25..3531a73 100644 --- a/js/admin-script.js +++ b/js/admin-script.js @@ -631,4 +631,74 @@ jQuery( document ).ready( function () { initializeDataTable( selector ); } } ); + + // Migration functionality. + jQuery( '#aaa-start-migration' ).on( 'click', function ( e ) { + e.preventDefault(); + const button = jQuery( this ); + const progressContainer = jQuery( '#aaa-migration-progress' ); + const progressBar = jQuery( '#aaa-migration-progress-bar' ); + const statusText = jQuery( '#aaa-migration-status' ); + const total = aaaOptionOptimizer.migration.total; + + button.prop( 'disabled', true ); + progressContainer.show(); + statusText.text( aaaOptionOptimizer.i18n.migrating ); + + /** + * Performs a single migration chunk via AJAX. + */ + function migrateChunk() { + jQuery.ajax( { + url: + aaaOptionOptimizer.root + + 'aaa-option-optimizer/v1/migrate', + method: 'POST', + beforeSend: ( xhr ) => + xhr.setRequestHeader( + 'X-WP-Nonce', + aaaOptionOptimizer.nonce + ), + success( response ) { + if ( ! response.success ) { + statusText.text( + response.message || + aaaOptionOptimizer.i18n.migrationError + ); + button.prop( 'disabled', false ); + return; + } + + const migrated = total - response.remaining; + const percent = Math.round( ( migrated / total ) * 100 ); + + progressBar.css( 'width', percent + '%' ); + statusText.text( + aaaOptionOptimizer.i18n.migratedOf + .replace( '%1$d', migrated ) + .replace( '%2$d', total ) + ); + + if ( response.remaining > 0 ) { + // Continue with next chunk. + migrateChunk(); + } else { + // Migration complete. + statusText.text( + aaaOptionOptimizer.i18n.migrationComplete + ); + setTimeout( function () { + window.location.reload(); + }, 1000 ); + } + }, + error() { + statusText.text( aaaOptionOptimizer.i18n.migrationError ); + button.prop( 'disabled', false ); + }, + } ); + } + + migrateChunk(); + } ); } ); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5b93576..7c4ebd2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -108,8 +108,9 @@ - + + diff --git a/readme.txt b/readme.txt index a070888..da538c8 100644 --- a/readme.txt +++ b/readme.txt @@ -29,7 +29,7 @@ Yes!! Backup your database. = Where can I report bugs? = -Please use [our GitHub](https://github.com/emilia-Capital/aaa-option-optimizer/) for reporting bugs or making code suggestions. Feel free to use the forums for asking questions too, of course. +Please use [our GitHub](https://github.com/ProgressPlanner/aaa-option-optimizer/) for reporting bugs or making code suggestions. Feel free to use the forums for asking questions too, of course. For security issues, please see the next question. @@ -39,7 +39,7 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro = How can I add recognized plugins? = -Please do a pull request via GitHub on [this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. +Please do a pull request via GitHub on [this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/develop/known-plugins/known-plugins.json) in the plugin. == Installation == 1. Search for AAA Option Optimizer on the repository. @@ -113,7 +113,7 @@ Implement the missing functionality to create an option with value `false` when = 1.1 = The plugin now recognizes plugins from which the options came (thanks to a great pull by [Rogier Lankhorst](https://profiles.wordpress.org/rogierlankhorst/)). If you're a plugin developer and want your plugin's options -properly recognized, please do a pull request [on this file](https://github.com/Emilia-Capital/aaa-option-optimizer/blob/main/known-plugins/known-plugins.json). +properly recognized, please do a pull request [on this file](https://github.com/ProgressPlanner/aaa-option-optimizer/blob/main/known-plugins/known-plugins.json). Small enhancements: diff --git a/src/autoload.php b/src/autoload.php index 502fda2..4f04541 100644 --- a/src/autoload.php +++ b/src/autoload.php @@ -2,12 +2,12 @@ /** * Autoload PHP classes for the plugin. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ spl_autoload_register( function ( $class_name ) { - $prefix = 'Emilia\\OptionOptimizer\\'; + $prefix = 'Progress_Planner\\OptionOptimizer\\'; if ( 0 !== \strpos( $class_name, $prefix ) ) { return; diff --git a/src/class-admin-page.php b/src/class-admin-page.php index 4e7d6dd..e45a243 100644 --- a/src/class-admin-page.php +++ b/src/class-admin-page.php @@ -2,10 +2,10 @@ /** * Admin page functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Admin page functionality for AAA Option Optimizer. @@ -189,9 +189,10 @@ public function enqueue_scripts( $hook ) { 'aaa-option-optimizer-admin-js', 'aaaOptionOptimizer', [ - 'root' => \esc_url_raw( \rest_url() ), - 'nonce' => \wp_create_nonce( 'wp_rest' ), - 'i18n' => [ + 'root' => \esc_url_raw( \rest_url() ), + 'nonce' => \wp_create_nonce( 'wp_rest' ), + 'migration' => Database::get_migration_status(), + 'i18n' => [ 'filterBySource' => \esc_html__( 'Filter by source', 'aaa-option-optimizer' ), 'showValue' => \esc_html__( 'Show', 'aaa-option-optimizer' ), 'addAutoload' => \esc_html__( 'Add autoload', 'aaa-option-optimizer' ), @@ -207,6 +208,11 @@ public function enqueue_scripts( $hook ) { 'apply' => \esc_html__( 'Apply', 'aaa-option-optimizer' ), 'search' => \esc_html__( 'Search:', 'aaa-option-optimizer' ), + 'migrating' => \esc_html__( 'Migrating...', 'aaa-option-optimizer' ), + 'migrationComplete' => \esc_html__( 'Migration complete! Reloading page...', 'aaa-option-optimizer' ), + 'migrationError' => \esc_html__( 'Migration error. Please try again.', 'aaa-option-optimizer' ), + /* translators: %1$d: number of migrated options, %2$d: total number of options */ + 'migratedOf' => \esc_html__( 'Migrated %1$d of %2$d options', 'aaa-option-optimizer' ), 'entries' => [ '_' => \esc_html__( 'entries', 'aaa-option-optimizer' ), '1' => \esc_html__( 'entry', 'aaa-option-optimizer' ), @@ -260,10 +266,39 @@ public function render_admin_page_ajax() { $wpdb->prepare( "SELECT count(*) AS count, SUM( LENGTH( option_value ) ) as autoload_size FROM {$wpdb->options} WHERE autoload IN ( $placeholders )", $autoload_values ) ); // phpcs:enable WordPress.DB + + // Check if migration is needed. + $migration_status = Database::get_migration_status(); ?>

+ +
+

+
+ +

+ +

+ +

+
+ +

diff --git a/src/class-database.php b/src/class-database.php new file mode 100644 index 0000000..16c5793 --- /dev/null +++ b/src/class-database.php @@ -0,0 +1,281 @@ +prefix . self::TABLE_NAME; + } + + /** + * Create the custom table. + * + * @return void + */ + public static function create_table() { + global $wpdb; + + $table_name = self::get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$table_name} ( + option_name VARCHAR(191) NOT NULL, + access_count BIGINT UNSIGNED DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (option_name) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + \dbDelta( $sql ); + } + + /** + * Drop the custom table. + * + * @return void + */ + public static function drop_table() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" ); + } + + /** + * Check if the table exists. + * + * @return bool + */ + public static function table_exists() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name; + } + + /** + * Number of options to migrate per request. + * + * @var int + */ + const MIGRATION_CHUNK_SIZE = 1000; + + /** + * Get migration status. + * + * @return array{needs_migration: bool, total: int, remaining: int} + */ + public static function get_migration_status() { + $option_data = \get_option( 'option_optimizer' ); + + if ( ! \is_array( $option_data ) || empty( $option_data['used_options'] ) ) { + return [ + 'needs_migration' => false, + 'total' => 0, + 'remaining' => 0, + ]; + } + + $remaining = \count( $option_data['used_options'] ); + + // Get total from transient or set it on first check. + $total = \get_transient( 'aaa_option_optimizer_migration_total' ); + if ( false === $total ) { + $total = $remaining; + \set_transient( 'aaa_option_optimizer_migration_total', $total, HOUR_IN_SECONDS ); + } + + return [ + 'needs_migration' => true, + 'total' => (int) $total, + 'remaining' => $remaining, + ]; + } + + /** + * Migrate a chunk of data from the old option format to the custom table. + * + * Processes in chunks to avoid timeouts on slow hosts with large datasets. + * + * @return array{success: bool, remaining: int, total: int} + */ + public static function migrate_chunk() { + $option_data = \get_option( 'option_optimizer' ); + + // No data or already migrated. + if ( ! \is_array( $option_data ) || empty( $option_data['used_options'] ) ) { + \delete_transient( 'aaa_option_optimizer_migration_total' ); + return [ + 'success' => true, + 'remaining' => 0, + 'total' => 0, + ]; + } + + // Ensure table exists. + if ( ! self::table_exists() ) { + self::create_table(); + } + + // Get total for progress tracking. + $total = \get_transient( 'aaa_option_optimizer_migration_total' ); + if ( false === $total ) { + $total = \count( $option_data['used_options'] ); + \set_transient( 'aaa_option_optimizer_migration_total', $total, HOUR_IN_SECONDS ); + } + + // Take a chunk of options to migrate. + $chunk = \array_slice( $option_data['used_options'], 0, self::MIGRATION_CHUNK_SIZE, true ); + + // Batch insert chunk to custom table. + self::batch_insert( $chunk ); + + // Remove migrated options from the array. + $option_data['used_options'] = \array_slice( $option_data['used_options'], self::MIGRATION_CHUNK_SIZE, null, true ); + + \update_option( 'option_optimizer', $option_data, false ); + + $remaining = \count( $option_data['used_options'] ); + + // Clean up total transient when done. + if ( 0 === $remaining ) { + \delete_transient( 'aaa_option_optimizer_migration_total' ); + } + + return [ + 'success' => true, + 'remaining' => $remaining, + 'total' => (int) $total, + ]; + } + + /** + * Batch insert or update option counts. + * + * Splits large datasets into chunks and wraps them in a transaction + * for optimal performance on slow hosts with large datasets. + * + * @param array $options Array of option_name => count. + * @param int $chunk_size Number of options per query. Default 500. + * + * @return void + */ + public static function batch_insert( $options, $chunk_size = 500 ) { + global $wpdb; + + if ( empty( $options ) ) { + return; + } + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'BEGIN' ); + + foreach ( array_chunk( $options, $chunk_size, true ) as $chunk ) { + $values = []; + $placeholders = []; + + foreach ( $chunk as $option_name => $count ) { + $placeholders[] = '(%s, %d, NOW())'; + $values[] = $option_name; + $values[] = (int) $count; + } + + $sql = "INSERT INTO {$table_name} (option_name, access_count, created_at) + VALUES " . implode( ', ', $placeholders ) . ' + ON DUPLICATE KEY UPDATE access_count = access_count + VALUES(access_count)'; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $wpdb->prepare( $sql, ...$values ) ); + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->query( 'COMMIT' ); + } + + /** + * Get all tracked options as an associative array. + * + * @return array Array of option_name => access_count. + */ + public static function get_tracked_options() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $results = $wpdb->get_results( "SELECT option_name, access_count FROM {$table_name}", ARRAY_A ); + + if ( empty( $results ) ) { + return []; + } + + $options = []; + foreach ( $results as $row ) { + $options[ $row['option_name'] ] = (int) $row['access_count']; + } + + return $options; + } + + /** + * Get tracked option names as a keyed array for efficient lookups. + * + * @return array Array of option_name => true. + */ + public static function get_tracked_option_keys() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $option_names = $wpdb->get_col( "SELECT option_name FROM {$table_name}" ); + + if ( empty( $option_names ) ) { + return []; + } + + return array_fill_keys( $option_names, true ); + } + + /** + * Clear all tracked options from the table. + * + * @return void + */ + public static function clear_tracked_options() { + global $wpdb; + + $table_name = self::get_table_name(); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant). + $wpdb->query( "TRUNCATE TABLE {$table_name}" ); + } +} diff --git a/src/class-map-plugin-to-options.php b/src/class-map-plugin-to-options.php index 2732f9a..799d8fe 100644 --- a/src/class-map-plugin-to-options.php +++ b/src/class-map-plugin-to-options.php @@ -2,15 +2,15 @@ /** * Functionality to map options to plugins. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Class Map_Plugin_To_Options * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ class Map_Plugin_To_Options { /** diff --git a/src/class-plugin.php b/src/class-plugin.php index 0df3325..0caf091 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -2,10 +2,10 @@ /** * Plugin functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; /** * Core functionality of AAA Option Optimizer. @@ -19,9 +19,9 @@ class Plugin { public static $instance; /** - * Holds the names of the options accessed during the request. + * Holds the options accessed during the request with their access counts. * - * @var string[] + * @var array */ protected $accessed_options = []; @@ -60,8 +60,6 @@ public static function get_instance() { * @return void */ public function register_hooks() { - $this->accessed_options = \get_option( 'option_optimizer', [ 'used_options' => [] ] )['used_options']; - if ( Admin_Page::get_option_tracking() === 'pre_option' ) { \add_filter( 'pre_option', [ $this, 'monitor_option_accesses_pre_option' ], PHP_INT_MAX, 2 ); } else { @@ -144,7 +142,7 @@ protected function add_option_usage( $option_name ) { } /** - * Update the 'option_optimizer' option with the list of used options at the end of the page load. + * Update the tracked options at the end of the page load. * * @return void */ @@ -153,19 +151,16 @@ public function update_tracked_options() { if ( isset( $_GET['page'] ) && $_GET['page'] === 'aaa-option-optimizer' ) { return; } - // Retrieve the existing option_optimizer data. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - - foreach ( $this->accessed_options as $option => $count ) { - $option_optimizer['used_options'][ $option ] = - ( $option_optimizer['used_options'][ $option ] ?? 0 ) + $count; - } + // Handle reset. if ( $this->should_reset ) { - $option_optimizer['used_options'] = []; + Database::clear_tracked_options(); + return; } - // Update the 'option_optimizer' option with the new list. - update_option( 'option_optimizer', $option_optimizer, false ); + // Write accessed options directly to the custom table. + if ( ! empty( $this->accessed_options ) ) { + Database::batch_insert( $this->accessed_options ); + } } } diff --git a/src/class-rest.php b/src/class-rest.php index 5934a77..472eea4 100644 --- a/src/class-rest.php +++ b/src/class-rest.php @@ -2,10 +2,10 @@ /** * REST functionality for AAA Option Optimizer. * - * @package Emilia\OptionOptimizer + * @package Progress_Planner\OptionOptimizer */ -namespace Emilia\OptionOptimizer; +namespace Progress_Planner\OptionOptimizer; use WP_Error; use WP_REST_Request; @@ -181,6 +181,18 @@ public function register_rest_routes() { }, ] ); + + \register_rest_route( + 'aaa-option-optimizer/v1', + '/migrate', + [ + 'methods' => 'POST', + 'callback' => [ $this, 'migrate_chunk' ], + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ] + ); } /** @@ -193,6 +205,16 @@ public function reset_stats() { return new \WP_REST_Response( [ 'success' => true ], 200 ); } + /** + * Migrate a chunk of data from old format to custom table. + * + * @return \WP_REST_Response + */ + public function migrate_chunk() { + $result = Database::migrate_chunk(); + return new \WP_REST_Response( $result, 200 ); + } + /** * Update autoload status of an option. * @@ -229,9 +251,8 @@ public function get_unused_options() { global $wpdb; - // Load used options from option_optimizer. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table. + $used_options = Database::get_tracked_option_keys(); $query = " SELECT option_name @@ -345,9 +366,8 @@ public function get_used_not_autoloaded_options() { global $wpdb; - // Load used options from option_optimizer. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table (with counts). + $used_options = Database::get_tracked_options(); if ( empty( $used_options ) ) { return new \WP_REST_Response( @@ -481,9 +501,8 @@ public function get_options_that_do_not_exist() { global $wpdb; - // Load used options. - $option_optimizer = get_option( 'option_optimizer', [ 'used_options' => [] ] ); - $used_options = $option_optimizer['used_options']; + // Load used options from custom table (with counts). + $used_options = Database::get_tracked_options(); if ( empty( $used_options ) ) { return new \WP_REST_Response( diff --git a/uninstall.php b/uninstall.php index 4fa17e2..05bf9fe 100644 --- a/uninstall.php +++ b/uninstall.php @@ -2,9 +2,9 @@ /** * Uninstall the plugin. * - * Delete the plugin option. + * Delete the plugin option and custom table. * - * @package Progress_Planner + * @package Progress_Planner\OptionOptimizer */ // If uninstall not called from WordPress, then exit. @@ -12,5 +12,15 @@ exit; } +global $wpdb; + +// Drop the custom table. +$aaa_option_optimizer_table = $wpdb->prefix . 'option_optimizer_tracked'; +// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is safe (from constant prefix). +$wpdb->query( "DROP TABLE IF EXISTS {$aaa_option_optimizer_table}" ); + +// Delete the batch transient. +delete_transient( 'option_optimizer_batch' ); + // Delete the plugin option. delete_option( 'option_optimizer' );