diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9e0fcdb..ba843cf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -67,3 +67,6 @@ jobs: - name: Lint blueprint file run: composer lint-blueprint + + - name: Lint known plugins file + run: composer lint-known-plugins diff --git a/.wordpress-org/github_banner_aaaoo_pp.png b/.wordpress-org/github_banner_aaaoo_pp.png new file mode 100644 index 0000000..32efae9 Binary files /dev/null and b/.wordpress-org/github_banner_aaaoo_pp.png differ diff --git a/README.md b/README.md index dd86c7d..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,9 @@ [![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) # AAA Option Optimizer This plugin tracks which of the autoloaded options are used on a page, and stores that data at the end of page render. It keeps an array of options that it has seen as being used. On the admin page, it compares all the autoloaded options to the array of stored options, and shows the autoloaded options that have not been used as you were browsing the site. If you've been to every page on your site, or you've kept the plugin around for a week or so, this means that those options probably don't need to be autoloaded. @@ -24,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? @@ -33,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 ef82da5..75866c9 100644 --- a/aaa-option-optimizer.php +++ b/aaa-option-optimizer.php @@ -2,15 +2,15 @@ /** * 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://joost.blog/plugins/aaa-option-optimizer/ + * Plugin URI: https://progressplanner.com/plugins/aaa-option-optimizer/ * Description: Tracks autoloaded options usage and allows the user to optimize them. - * Version: 1.5.1 + * Version: 1.6.0 * License: GPL-3.0+ - * Author: Joost de Valk - * Author URI: https://joost.blog/ + * Author: Team Prospress Planner + * Author URI: https://prospressplanner.com/ * Text Domain: 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,16 +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' => [], - ], - false - ); + // 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 + ); + } } /** @@ -64,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 68a8d8f..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", @@ -16,7 +16,7 @@ "phpstan/phpstan": "^1.10", "szepeviktor/phpstan-wordpress": "^1.3", "phpstan/extension-installer": "^1.3", - "phpcompatibility/php-compatibility": "dev-develop as 9.99.99" + "phpcompatibility/php-compatibility": "^9.3" }, "config": { "allow-plugins": { @@ -37,6 +37,9 @@ "lint-blueprint": [ "@php -r \"exit( intval( is_null( json_decode( file_get_contents( './.wordpress-org/blueprints/blueprint.json' ) ) ) ) );\"" ], + "lint-known-plugins": [ + "@php -r \"exit( intval( is_null( json_decode( file_get_contents( './known-plugins/known-plugins.json' ) ) ) ) );\"" + ], "phpstan": [ "@php ./vendor/bin/phpstan analyse --memory-limit=2048M" ] diff --git a/composer.lock b/composer.lock index caaaa3c..d186fc9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,34 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fd7786df413fbfa0ec1e1564845fe06c", + "content-hash": "68e73cf23844dd66f4d8b3b14eef46cd", "packages": [], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", + "composer-plugin-api": "^2.2", "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", + "composer/composer": "^2.2", "ext-json": "*", "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", @@ -50,9 +50,9 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", @@ -60,7 +60,6 @@ } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -81,9 +80,28 @@ ], "support": { "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" }, { "name": "php-parallel-lint/php-parallel-lint", @@ -148,27 +166,30 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.6.0", + "version": "v6.9.0", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "86e8753e89d59849276dcdd91b9a7dd78bb4abe2" + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/86e8753e89d59849276dcdd91b9a7dd78bb4abe2", - "reference": "86e8753e89d59849276dcdd91b9a7dd78bb4abe2", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", + "reference": "5171cb6650e6c583a96943fd6ea0dfa3e1089a8a", "shasum": "" }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", + "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", "phpdocumentor/reflection-docblock": "^5.4.1", - "phpstan/phpstan": "^1.10.49", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { @@ -190,51 +211,39 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.6.0" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.0" }, - "time": "2024-07-17T08:50:38+00:00" + "time": "2025-12-03T23:06:24+00:00" }, { "name": "phpcompatibility/php-compatibility", - "version": "dev-develop", + "version": "9.3.5", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "d9ae4b030f174c8f01d400244107e28ad65ec5e1" + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/d9ae4b030f174c8f01d400244107e28ad65ec5e1", - "reference": "d9ae4b030f174c8f01d400244107e28ad65ec5e1", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.12", - "squizlabs/php_codesniffer": "^3.10.0" + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" }, - "replace": { - "wimg/php-compatibility": "*" + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.3", - "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.1.0", - "yoast/phpunit-polyfills": "^1.0.5 || ^2.0.0" + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, - "default-branch": true, "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "9.x-dev", - "dev-develop": "10.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0-or-later" @@ -260,54 +269,38 @@ "keywords": [ "compatibility", "phpcs", - "standards", - "static analysis" + "standards" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibility" }, - "funding": [ - { - "url": "https://github.com/PHPCompatibility", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - } - ], - "time": "2024-08-31T21:55:43+00:00" + "time": "2019-12-27T09:44:58+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.2", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", - "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "paragonie/random_compat": "dev-master", "paragonie/sodium_compat": "dev-master" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, "type": "phpcodesniffer-standard", @@ -337,27 +330,47 @@ ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" }, - "time": "2022-10-25T01:46:02+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.5", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", "shasum": "" }, "require": { "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^1.0" @@ -407,35 +420,39 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:37:59+00:00" + "time": "2025-10-18T00:05:59+00:00" }, { "name": "phpcsstandards/phpcsextra", - "version": "1.2.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + "reference": "b598aa890815b8df16363271b659d73280129101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.8.0" + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "type": "phpcodesniffer-standard", "extra": { @@ -485,35 +502,39 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2023-12-08T16:49:07+00:00" + "time": "2025-11-12T23:06:57+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" }, "require-dev": { "ext-filter": "*", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" }, "type": "phpcodesniffer-standard", "extra": { @@ -550,6 +571,7 @@ "phpcodesniffer-standard", "phpcs", "phpcs3", + "phpcs4", "standards", "static analysis", "tokens", @@ -573,9 +595,13 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2025-12-08T14:27:58+00:00" }, { "name": "phpstan/extension-installer", @@ -627,16 +653,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.2", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "0ca1c7bb55fca8fe6448f16fff0f311ccec960a1" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0ca1c7bb55fca8fe6448f16fff0f311ccec960a1", - "reference": "0ca1c7bb55fca8fe6448f16fff0f311ccec960a1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -681,20 +702,20 @@ "type": "github" } ], - "time": "2024-09-05T16:09:28+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.2", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -711,11 +732,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -759,32 +775,36 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "time": "2024-07-21T23:26:44+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.29.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -821,7 +841,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" }, "funding": [ { @@ -832,12 +852,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -904,16 +928,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.1.0", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7" + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/9333efcbff231f10dfd9c56bb7b65818b4733ca7", - "reference": "9333efcbff231f10dfd9c56bb7b65818b4733ca7", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", "shasum": "" }, "require": { @@ -921,17 +945,17 @@ "ext-libxml": "*", "ext-tokenizer": "*", "ext-xmlreader": "*", - "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.10", - "squizlabs/php_codesniffer": "^3.9.0" + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^8.0 || ^9.0" }, "suggest": { "ext-iconv": "For improved results", @@ -966,24 +990,15 @@ "type": "custom" } ], - "time": "2024-03-25T16:39:00+00:00" - } - ], - "aliases": [ - { - "package": "phpcompatibility/php-compatibility", - "version": "dev-develop", - "alias": "9.99.99", - "alias_normalized": "9.99.99.0" + "time": "2025-11-25T12:08:04+00:00" } ], + "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "phpcompatibility/php-compatibility": 20 - }, + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/css/style.css b/css/style.css index 5baaa58..21e90b6 100644 --- a/css/style.css +++ b/css/style.css @@ -37,7 +37,6 @@ .aaa_option_table .actions .button span.dashicons, .aaa_option_table .button.dashicon span.dashicons { padding: 0; margin: 0 3px 0 0; - vertical-align: middle; } .aaa_option_table td span.num { font-family: monospace; @@ -92,9 +91,6 @@ div.dt-container .dt-input { .aaa-option-optimizer-popover__close:hover { cursor: pointer; } -.aaa-option-optimizer-reset { - float: right; -} .aaa-option-optimizer-tabs { clear: both; display: flex; @@ -150,3 +146,18 @@ div.dt-container .dt-input { .aaa_option_table .source { width: 20%; } + +/* Settings page */ +.aaa-option-optimizer-tracking-fieldset { + display: flex; + flex-direction: column; + gap: 10px; +} + +/* Version 6.9 compatibility */ +.branch-6-9 { + .aaa_option_table .actions .button span.dashicons, + .aaa_option_table .button.dashicon span.dashicons { + vertical-align: middle; + } +} \ No newline at end of file diff --git a/js/admin-script.js b/js/admin-script.js index cbe412a..3531a73 100644 --- a/js/admin-script.js +++ b/js/admin-script.js @@ -39,12 +39,33 @@ jQuery( document ).ready( function () { return 'option_' + optionName.replace( /\./g, '_' ); } + /** + * Store sources (plugin names) for each table from AJAX responses. + * + * @type {Object} + */ + const tableSources = {}; + + /** + * Creates a column filter setup function bound to a specific table selector. + * + * @param {string} tableSelector - The table selector. + * @return {Function} The filter setup function. + */ + function createColumnFilterSetup( tableSelector ) { + return function () { + setupColumnFilters.call( this, tableSelector ); + }; + } + /** * Initializes the DataTable for the given selector. * * @param {string} selector - The table selector. */ function initializeDataTable( selector ) { + const filterSetup = createColumnFilterSetup( selector ); + const options = { pageLength: 25, autoWidth: false, @@ -54,7 +75,7 @@ jQuery( document ).ready( function () { return generateRowId( data.name ); }, initComplete() { - this.api().columns( 'source:name' ).every( setupColumnFilters ); + this.api().columns( 'source:name' ).every( filterSetup ); }, language: aaaOptionOptimizer.i18n, }; @@ -66,7 +87,10 @@ jQuery( document ).ready( function () { 'aaa-option-optimizer/v1/unused-options', headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, type: 'GET', - dataSrc: 'data', + dataSrc( json ) { + tableSources[ selector ] = json.sources || []; + return json.data; + }, }; options.serverSide = true; options.processing = true; @@ -75,7 +99,7 @@ jQuery( document ).ready( function () { }; options.initComplete = function () { getBulkActionsForm( selector, [ 'autoload-off' ] ).call( this ); - this.api().columns( 'source:name' ).every( setupColumnFilters ); + this.api().columns( 'source:name' ).every( filterSetup ); }; options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. } @@ -87,7 +111,10 @@ jQuery( document ).ready( function () { 'aaa-option-optimizer/v1/used-not-autoloaded-options', headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, type: 'GET', - dataSrc: 'data', + dataSrc( json ) { + tableSources[ selector ] = json.sources || []; + return json.data; + }, }; options.serverSide = true; options.processing = true; @@ -96,7 +123,7 @@ jQuery( document ).ready( function () { }; options.initComplete = function () { getBulkActionsForm( selector, [ 'autoload-on' ] ).call( this ); - this.api().columns( 'source:name' ).every( setupColumnFilters ); + this.api().columns( 'source:name' ).every( filterSetup ); }; options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. } @@ -106,10 +133,16 @@ jQuery( document ).ready( function () { url: `${ aaaOptionOptimizer.root }aaa-option-optimizer/v1/options-that-do-not-exist`, headers: { 'X-WP-Nonce': aaaOptionOptimizer.nonce }, type: 'GET', - dataSrc: 'data', + dataSrc( json ) { + tableSources[ selector ] = json.sources || []; + return json.data; + }, }; options.serverSide = true; options.processing = true; + options.initComplete = function () { + this.api().columns( 'source:name' ).every( filterSetup ); + }; } if ( selector === '#all_options_table' ) { @@ -124,7 +157,7 @@ jQuery( document ).ready( function () { 'autoload-on', 'autoload-off', ] ).call( this ); - this.api().columns( 'source:name' ).every( setupColumnFilters ); + this.api().columns( 'source:name' ).every( filterSetup ); }; options.order = [ [ 1, 'asc' ] ]; // Order by 2nd column, first column is checkbox. } @@ -253,8 +286,10 @@ jQuery( document ).ready( function () { /** * Sets up the column filters for the DataTable. + * + * @param {string} tableSelector - The table selector to get sources from. */ - function setupColumnFilters() { + function setupColumnFilters( tableSelector ) { const column = this; const select = document.createElement( 'select' ); select.add( @@ -266,13 +301,22 @@ jQuery( document ).ready( function () { column.search( select.value, { exact: true } ).draw(); } ); - column - .data() - .unique() - .sort() - .each( function ( d ) { - select.add( new Option( d ) ); + // Use sources from AJAX response if available (for server-side processing), + // otherwise fall back to column data (for client-side processing). + const sources = tableSources[ tableSelector ]; + if ( sources && sources.length > 0 ) { + sources.forEach( function ( source ) { + select.add( new Option( source ) ); } ); + } else { + column + .data() + .unique() + .sort() + .each( function ( d ) { + select.add( new Option( d ) ); + } ); + } } /** @@ -587,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/known-plugins/known-plugins.json b/known-plugins/known-plugins.json index 0dc3a69..a0d3d9f 100644 --- a/known-plugins/known-plugins.json +++ b/known-plugins/known-plugins.json @@ -7,6 +7,10 @@ "name": "Equalize Digital Accessibility Checker", "option_prefixes": ["edac_"] }, + "admin-columns": { + "name": "Admin Columns (Pro)", + "option_prefixes": ["ac_columns_","acp_","ac-","ac_","_ac_","_acp_"] + }, "advanced-custom-fields": { "name": "Advanced Custom Fields", "option_prefixes": ["acf_"] @@ -15,6 +19,10 @@ "name": "Affiliate WP", "option_prefixes": ["affwp_"] }, + "akismet": { + "name": "Akismet", + "option_prefixes": ["akismet_","wordpress_api_key"] + }, "all-in-one-seo-pack": { "name": "AIOSEO - The Best WordPress SEO Plugin & Toolkit", "option_prefixes": ["aioseo_","widget_aioseo"] @@ -47,6 +55,10 @@ "name": "Cookie banner plugin for WordPress – Cookiebot CMP by Usercentrics", "option_prefixes": ["cookiebot_", "cookiebot-"] }, + "disable-comments": { + "name": "Disable Comments", + "option_prefixes": ["disable_comment_","disable_comments_"] + }, "duplicate-post": { "name": "Yoast Duplicate Post", "option_prefixes": ["duplicate_post_"] @@ -63,6 +75,10 @@ "name": "Conversios: Google Analytics 4 (GA4), Google Ads, Microsoft Ads, and Multi-Channel Conversion Tracking", "option_prefixes": ["ee_"] }, + "fair": { + "name": "FAIR Connect", + "option_prefixes": ["fair_","_transient_timeout_fair"] + }, "fewer-tags": { "name": "Fewer Tags", "option_prefixes": ["fewer_tags"] @@ -73,7 +89,11 @@ }, "generateblocks": { "name": "GenerateBlocks", - "option_prefixes": ["generateblocks_"] + "option_prefixes": ["generateblocks", "generateblocks_", "gb_","default_term_gblocks_"] + }, + "generatepress": { + "name": "GeneratePress", + "option_prefixes": ["_generatepress_","generate_","generatepress_","gen_premium_"] }, "google-analytics-for-wordpress": { "name": "Google Analytics for WordPress by MonsterInsights", @@ -85,8 +105,16 @@ }, "gravity-forms": { "name": "Gravity Forms", - "option_prefixes": ["gf_","gforms_"] + "option_prefixes": ["gf_","gform_","gforms_","rg_form_","rg_gforms","_transient_timeout_GFCache_","_transient_timeout_rg_gforms_","_transient_rg_gforms_","_site_transient_t15s-registry-gforms"] + }, + "gravity-forms-addon": { + "name": "Gravity Forms Addons", + "option_prefixes": ["gravityformsaddon_"] }, + "gravity-perks": { + "name": "Gravity :Perks", + "option_prefixes": ["gperks_","gwp_"] + }, "woo-billing-with-invoicexpress": { "name": "Invoicing with InvoiceXpress for WooCommerce", "option_prefixes": ["hd_wc_ie_"] @@ -107,6 +135,10 @@ "name": "LiteSpeed Cache", "option_prefixes": ["litespeed-", "_litespeed_", "litespeed."] }, + "log-http-requests": { + "name": "Log HTTP Requests", + "option_prefixes": ["lhr_"] + }, "loginizer": { "name": "Loginizer", "option_prefixes": ["loginizer_"] @@ -119,6 +151,14 @@ "name": "Multibanco, MB WAY, Credit card, Apple Pay, Google Pay, Payshop, Cofidis Pay, and PIX (ifthenpay) for WooCommerce", "option_prefixes": ["cofidispay_ifthen_", "creditcard_ifthen_", "gateway_ifthen_", "mbway_ifthen_", "multibanco_ifthen_", "payshop_"] }, + "next-gen-gallery": { + "name": "Next Gen Gallery", + "option_prefixes": ["ngg_"] + }, + "patchstack": { + "name": "Patchstack Security", + "option_prefixes": ["patchstack_"] + }, "perfmatters": { "name": "Perfmatters", "option_prefixes": ["perfmatters_"] @@ -127,6 +167,10 @@ "name": "Pixel Caffeine", "option_prefixes": ["aepc_"] }, + "plausible": { + "name": "Plausible Analytics", + "option_prefixes": ["plausible_","_transient_timeout_plausible_"] + }, "pojo-accessibility": { "name": "One Click Accessibility", "option_prefixes": ["pojo_a11y"] @@ -141,7 +185,7 @@ }, "progress-planner": { "name": "Progress Planner", - "option_prefixes": ["progress_planner_"] + "option_prefixes": ["progress_planner_", "_transient_timeout_progress_planner_", "_transient_timeout_prpl_","_transient_progress_planner","_transient_prpl"] }, "really-simple-ssl": { "name": "Really Simple SSL", @@ -179,18 +223,34 @@ "name": "SliceWP Affiliates", "option_prefixes": ["slicewp_"] }, + "smtp-mailer": { + "name": "SMTP Mailer", + "option_prefixes": ["smtp_mailer_"] + }, "updraft": { "name": "Updraft Plus", "option_prefixes": ["updraft_", "updraftplus_"] }, "yoast": { "name": "Yoast SEO", - "option_prefixes": ["wpseo", "yoast_"] + "option_prefixes": ["wpseo", "yoast_","_transient_timeout_wpseo_","_transient_wpseo_","_site_transient_yoast_","_transient_total_unindexed_prominent_words"] }, "feed-kuantokusta-for-woocommerce": { "name": "Feed KuantoKusta for WooCommerce", "option_prefixes": ["wc_feed_kuantokusta_"] }, + "sg-optimizer": { + "name": "Speed Optimizer by Siteground", + "option_prefixes": ["siteground_optimizer_","siteground_data_","sgo_","sg_cachepress"] + }, + "sg-security": { + "name": "Security Optimizer by Siteground", + "option_prefixes": ["sg_security_", "siteground_security_","siteground_data_","siteground_settings_security","sgs_"] + }, + "svg-support": { + "name": "SVG Support", + "option_prefixes": ["bodhi_svgs_"] + }, "woo-dpd-pickup": { "name": "DPD / Geopost Pickup and Lockers network for WooCommerce", "option_prefixes": ["woo_dpd_pickup"] @@ -214,6 +274,7 @@ "wordpress": { "name": "WordPress", "option_prefixes": [ + "_transient_wp_", "_site_transient_timeout_available_translations", "_site_transient_timeout_theme_roots", "_site_transient_update_core", @@ -227,6 +288,7 @@ "auto_update_core_dev", "auto_update_core_major", "auto_update_core_minor", + "auto_update_plugins", "auto_updater.lock", "avatar_default", "avatar_rating", @@ -295,6 +357,7 @@ "medium_size_w", "moderation_keys", "moderation_notify", + "nav_menu_options", "new_admin_email", "nonce_key", "nonce_salt", @@ -306,7 +369,9 @@ "posts_per_page", "posts_per_rss", "recently_activated", + "recently_edited", "recovery_keys", + "recovery_mode_email_last_sent", "require_name_email", "rewrite_rules", "rss_use_excerpt", @@ -344,9 +409,11 @@ "users_can_register", "widget_", "wp_attachment_pages_enabled", + "wp_notes_notify", "wp_force_deactivated_plugins", "wp_page_for_privacy_policy", "wp_user_roles", + "wpins_block_notice", "WPLANG" ] }, @@ -366,6 +433,10 @@ "name": "WPML", "option_prefixes": ["_wpml_", "wpml_", "otgs-", "otgs_", "_icl_"] }, + "wp-mail-smtp": { + "name": "WP Mail SMTP", + "option_prefixes": ["wp_mail_smtp_"] + }, "wp-optimize": { "name": "WP Optimize", "option_prefixes": ["wpo_", "wp-optimize"] diff --git a/phpcs.xml.dist b/phpcs.xml.dist index fffbfd5..7c4ebd2 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -108,12 +108,18 @@ - + + + + + 0 + + diff --git a/readme.txt b/readme.txt index 1a0eb9d..d434e3a 100644 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === AAA Option Optimizer === -Contributors: joostdevalk +Contributors: joostdevalk, aristath, filipi Tags: options, database, cleanup -Requires at least: 6.6 -Tested up to: 6.8 +Requires at least: 6.7 +Tested up to: 7.0 Requires PHP: 7.4 -Stable tag: 1.5.1 +Stable tag: 1.6.0 License: GPL3+ License URI: https://www.gnu.org/licenses/gpl-3.0.en.html @@ -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. @@ -54,6 +54,11 @@ Please do a pull request via GitHub on [this file](https://github.com/Emilia-Cap == Changelog == += 1.6.0 + +* Replace using 'all' filter for monitoring option usage with 'pre_option' filter for better performance. +* Migrate tracked options data from a single wp_option to a custom database table for improved performance and reliability. + = 1.5.1 = * Add "select all" checkbox. @@ -109,7 +114,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 ea476fe..e45a243 100644 --- a/src/class-admin-page.php +++ b/src/class-admin-page.php @@ -2,16 +2,21 @@ /** * 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. */ class Admin_Page { + /** + * Option name for settings. + */ + const OPTION_NAME = 'option_optimizer'; + /** * Register hooks. * @@ -23,6 +28,79 @@ public function register_hooks() { \add_action( 'admin_menu', [ $this, 'add_admin_page' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); + \add_action( 'admin_init', [ $this, 'register_settings' ] ); + } + + /** + * Register settings. + * + * @return void + */ + public function register_settings(): void { + \register_setting( + 'aaa_option_optimizer_settings_group', + self::OPTION_NAME, + [ + 'sanitize_callback' => [ $this, 'sanitize_settings' ], + ] + ); + } + + /** + * Sanitize settings. + * + * This merges the settings into the existing option_optimizer option structure. + * + * @param array $input Settings input. + * @return array Sanitized settings merged with existing option data. + */ + public function sanitize_settings( $input ): array { + // Get the existing option_optimizer data to preserve other keys. + $existing = \get_option( self::OPTION_NAME, [] ); + + // Initialize settings array if it doesn't exist. + if ( ! isset( $existing['settings'] ) ) { + $existing['settings'] = []; + } + + // Sanitize the option_tracking setting. + $option_tracking = 'pre_option'; + if ( isset( $input['settings']['option_tracking'] ) ) { + $input_option_tracking = \sanitize_text_field( $input['settings']['option_tracking'] ); + if ( \in_array( $input_option_tracking, [ 'pre_option', 'legacy' ], true ) ) { + $option_tracking = $input_option_tracking; + } + } + $existing['settings']['option_tracking'] = $option_tracking; + + // Return the full option structure with merged settings. + return $existing; + } + + /** + * Get settings. + * + * @return array Settings from the settings subarray. + */ + public static function get_settings(): array { + $defaults = [ + 'option_tracking' => 'pre_option', + ]; + + $option_optimizer = \get_option( self::OPTION_NAME, [] ); + $settings = isset( $option_optimizer['settings'] ) ? $option_optimizer['settings'] : []; + + return \wp_parse_args( $settings, $defaults ); + } + + /** + * Get option tracking. + * + * @return string Option tracking. + */ + public static function get_option_tracking(): string { + $settings = self::get_settings(); + return $settings['option_tracking'] ?? 'pre_option'; } /** @@ -111,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' ), @@ -129,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' ), @@ -182,42 +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(); ?>

-

-

- count ), - \number_format( ( $result->autoload_size / 1024 ), 1 ) - ); - ?> -

- -

- -
-

+ +
+

+
+ +

+ - - +

+ +

+
-
- -

@@ -375,8 +456,85 @@ public function render_admin_page_ajax() {
+ + + +
+ render_settings_tab( $option_optimizer, $result ); ?> +
+
+
+ $option_optimizer The option optimizer data. + * @param object $result The database query result with current stats. + * + * @return void + */ + private function render_settings_tab( $option_optimizer, $result ): void { + $settings = self::get_settings(); + + // Check if settings were saved. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce check not needed here. + if ( isset( $_GET['settings-updated'] ) ) { + ?> +
+

+ +
+

+
+ + + + +

+

+ count ), + \number_format( ( $result->autoload_size / 1024 ), 1 ) + ); + ?> +

+ +
+
+ +

+
+ +

+
+ + +
+ +
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 37c73fe..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,11 +60,13 @@ public static function get_instance() { * @return void */ public function register_hooks() { - $this->accessed_options = \get_option( 'option_optimizer', [ 'used_options' => [] ] )['used_options']; - - // Hook into all actions and filters to monitor option accesses. - // @phpstan-ignore-next-line -- The 'all' hook does not need a return. - \add_filter( 'all', [ $this, 'monitor_option_accesses' ] ); + if ( Admin_Page::get_option_tracking() === 'pre_option' ) { + \add_filter( 'pre_option', [ $this, 'monitor_option_accesses_pre_option' ], PHP_INT_MAX, 2 ); + } else { + // Hook into all actions and filters to monitor option accesses. + // @phpstan-ignore-next-line -- The 'all' hook does not need a return. + \add_filter( 'all', [ $this, 'monitor_option_accesses_legacy' ] ); + } // Use the shutdown action to update the option with tracked data. \add_action( 'shutdown', [ $this, 'update_tracked_options' ] ); @@ -98,7 +100,7 @@ public function reset( $should_reset = true ) { * * @return void */ - public function monitor_option_accesses( $tag ) { + public function monitor_option_accesses_legacy( $tag ) { // Check if the tag is related to an option access. if ( str_starts_with( $tag, 'option_' ) || str_starts_with( $tag, 'default_option_' ) ) { $option_name = preg_replace( '#^(default_)?option_#', '', $tag ); @@ -106,6 +108,24 @@ public function monitor_option_accesses( $tag ) { } } + /** + * Add an option to the list of used options if it's not already there. + * + * @param mixed $pre The value to return instead of the option value. + * @param string $option_name Name of the option being accessed. + * + * @return mixed + */ + public function monitor_option_accesses_pre_option( $pre, $option_name ) { + + // If the $pre is false the get_option() will not be short-circuited. + if ( ! defined( 'WP_SETUP_CONFIG' ) && false === $pre ) { + $this->add_option_usage( $option_name ); + } + + return $pre; + } + /** * Add an option to the list of used options if it's not already there. * @@ -114,16 +134,15 @@ public function monitor_option_accesses( $tag ) { * @return void */ protected function add_option_usage( $option_name ) { - // Check if this option hasn't been tracked yet and add it to the array. if ( ! array_key_exists( $option_name, $this->accessed_options ) ) { - $this->accessed_options[ $option_name ] = 1; - return; + $this->accessed_options[ $option_name ] = 0; } + ++$this->accessed_options[ $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 */ @@ -132,16 +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' => [] ] ); - - $option_optimizer['used_options'] = $this->accessed_options; + // 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 9310487..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 @@ -255,6 +276,15 @@ public function get_unused_options() { $autoload_option_keys = array_fill_keys( $autoloaded_option_names, true ); $unused_keys = array_diff_key( $autoload_option_keys, $used_options ); + // Collect all unique sources (plugin names) before any filtering. + $all_sources = []; + foreach ( array_keys( $unused_keys ) as $option_name ) { + $plugin_name = $this->get_plugin_name( $option_name ); + $all_sources[ $plugin_name ] = true; + } + $all_sources = array_keys( $all_sources ); + sort( $all_sources ); + // Apply source filter to unused keys if specified. $filter_by_source = isset( $_GET['columns'][2]['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['columns'][2]['search']['value'] ) ) ) : ''; if ( '' !== $filter_by_source ) { @@ -315,6 +345,7 @@ public function get_unused_options() { 'recordsTotal' => $total_unused, 'recordsFiltered' => $total_unused, 'data' => $response_data, + 'sources' => $all_sources, ], 200 ); @@ -335,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( @@ -366,6 +396,15 @@ public function get_used_not_autoloaded_options() { // Find used options that are not autoloaded. $non_autoloaded_used_keys = array_diff_key( $used_options, $autoload_option_keys ); + // Collect all unique sources (plugin names) before any filtering. + $all_sources = []; + foreach ( array_keys( $non_autoloaded_used_keys ) as $option_name ) { + $plugin_name = $this->get_plugin_name( $option_name ); + $all_sources[ $plugin_name ] = true; + } + $all_sources = array_keys( $all_sources ); + sort( $all_sources ); + // Filter by source (plugin). $filter_by_source = isset( $_GET['columns'][2]['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['columns'][2]['search']['value'] ) ) ) : ''; if ( '' !== $filter_by_source ) { @@ -391,6 +430,7 @@ function ( $option_name ) use ( $search ) { 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => [], + 'sources' => $all_sources, ], 200 ); @@ -442,6 +482,7 @@ function ( $option_name ) use ( $search ) { 'recordsTotal' => $total_filtered, 'recordsFiltered' => $total_filtered, 'data' => $response_data, + 'sources' => $all_sources, ], 200 ); @@ -460,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( @@ -490,24 +530,6 @@ public function get_options_that_do_not_exist() { // Get used options that are not autoloaded. $non_autoloaded_keys = array_diff_key( $used_options, $autoload_option_keys ); - // Filter by source (plugin). - $filter_by_source = isset( $_GET['columns'][1]['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['columns'][1]['search']['value'] ) ) ) : ''; - if ( '' !== $filter_by_source ) { - $non_autoloaded_keys = $this->filter_by_source( $non_autoloaded_keys, $filter_by_source ); - } - - // Search. - $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; - if ( '' !== $search ) { - $non_autoloaded_keys = array_filter( - $non_autoloaded_keys, - function ( $option_name ) use ( $search ) { - return stripos( $option_name, $search ) !== false; - }, - ARRAY_FILTER_USE_KEY - ); - } - if ( empty( $non_autoloaded_keys ) ) { return new \WP_REST_Response( [ @@ -515,6 +537,7 @@ function ( $option_name ) use ( $search ) { 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => [], + 'sources' => [], ], 200 ); @@ -532,11 +555,11 @@ function ( $option_name ) use ( $search ) { ); $existing_keys = array_fill_keys( $existing_option_names, true ); - // Filter only those that do NOT exist. - $response_data = []; + // Build array of non-existing options (before any filtering). + $non_existing_options = []; foreach ( $non_autoloaded_keys as $option => $count ) { if ( ! isset( $existing_keys[ $option ] ) ) { - $response_data[] = [ + $non_existing_options[ $option ] = [ 'name' => $option, 'plugin' => $this->get_plugin_name( $option ), 'count' => $count, @@ -545,6 +568,37 @@ function ( $option_name ) use ( $search ) { } } + // Collect all unique sources (plugin names) before any filtering. + $all_sources = []; + foreach ( $non_existing_options as $row ) { + $all_sources[ $row['plugin'] ] = true; + } + $all_sources = array_keys( $all_sources ); + sort( $all_sources ); + + // Filter by source (plugin). + $filter_by_source = isset( $_GET['columns'][1]['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['columns'][1]['search']['value'] ) ) ) : ''; + if ( '' !== $filter_by_source ) { + $non_existing_options = array_filter( + $non_existing_options, + function ( $row ) use ( $filter_by_source ) { + return false !== stripos( $row['plugin'], $filter_by_source ); + } + ); + } + + // Search. + $search = isset( $_GET['search']['value'] ) ? trim( \sanitize_text_field( \wp_unslash( $_GET['search']['value'] ) ) ) : ''; + if ( '' !== $search ) { + $non_existing_options = array_filter( + $non_existing_options, + function ( $row ) use ( $search ) { + return stripos( $row['name'], $search ) !== false; + } + ); + } + + $response_data = array_values( $non_existing_options ); $total_filtered = count( $response_data ); // Pagination. @@ -564,6 +618,7 @@ function ( $option_name ) use ( $search ) { 'recordsTotal' => $total_filtered, 'recordsFiltered' => $total_filtered, 'data' => $response_data, + 'sources' => $all_sources, ], 200 ); 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' );