diff --git a/.github/workflows/sync-known-plugins.yml b/.github/workflows/sync-known-plugins.yml
new file mode 100644
index 0000000..c5af8a7
--- /dev/null
+++ b/.github/workflows/sync-known-plugins.yml
@@ -0,0 +1,83 @@
+name: Sync known-plugins.json to wp.org SVN
+
+on:
+ schedule:
+ - cron: '0 3 * * *'
+ push:
+ branches:
+ - main
+ paths:
+ - 'known-plugins/known-plugins.json'
+ workflow_dispatch:
+
+jobs:
+ sync:
+ name: Publish known-plugins.json to wp.org assets
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository (main only)
+ uses: actions/checkout@v4
+ with:
+ ref: main
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.2'
+ coverage: none
+
+ - name: Validate known-plugins.json
+ run: php bin/validate-known-plugins.php
+
+ - name: Install Subversion
+ run: sudo apt-get update && sudo apt-get install -y subversion
+
+ - name: Sparse-checkout SVN assets
+ env:
+ SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
+ SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
+ run: |
+ svn checkout \
+ --depth immediates \
+ --username "$SVN_USERNAME" \
+ --password "$SVN_PASSWORD" \
+ --non-interactive \
+ --no-auth-cache \
+ https://plugins.svn.wordpress.org/aaa-option-optimizer/ svn-repo
+ svn update --set-depth infinity svn-repo/assets
+
+ - name: Copy JSON into assets and detect changes
+ id: diff
+ run: |
+ cp known-plugins/known-plugins.json svn-repo/assets/known-plugins.json
+ cd svn-repo
+ # Add the file if it's new
+ if ! svn info assets/known-plugins.json >/dev/null 2>&1; then
+ svn add assets/known-plugins.json
+ fi
+ # Capture status; empty means nothing to commit
+ STATUS=$(svn status assets/known-plugins.json)
+ echo "svn-status: '$STATUS'"
+ if [ -z "$STATUS" ]; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Commit to SVN
+ if: steps.diff.outputs.changed == 'true'
+ env:
+ SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
+ SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
+ run: |
+ cd svn-repo
+ svn commit assets/known-plugins.json \
+ --username "$SVN_USERNAME" \
+ --password "$SVN_PASSWORD" \
+ --non-interactive \
+ --no-auth-cache \
+ -m "Update assets/known-plugins.json"
+
+ - name: No changes
+ if: steps.diff.outputs.changed != 'true'
+ run: echo "known-plugins.json on SVN is already up to date."
diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php
index 3bacb4d..0cd997b 100644
--- a/aaa-option-optimizer.php
+++ b/aaa-option-optimizer.php
@@ -73,6 +73,8 @@ function aaa_option_optimizer_activation() {
function aaa_option_optimizer_deactivation() {
$aaa_option_value = get_option( 'option_optimizer' );
update_option( 'option_optimizer', $aaa_option_value, false );
+
+ wp_clear_scheduled_hook( Progress_Planner\OptionOptimizer\Known_Plugins::CRON_HOOK );
}
/**
diff --git a/bin/validate-known-plugins.php b/bin/validate-known-plugins.php
new file mode 100644
index 0000000..52d86f0
--- /dev/null
+++ b/bin/validate-known-plugins.php
@@ -0,0 +1,94 @@
+ $entry ) {
+ if ( ! is_string( $slug ) || '' === $slug ) {
+ $errors[] = 'Slug must be a non-empty string.';
+ continue;
+ }
+
+ if ( ! is_array( $entry ) ) {
+ $errors[] = "Entry for '{$slug}' must be an object.";
+ continue;
+ }
+
+ if ( empty( $entry['name'] ) || ! is_string( $entry['name'] ) ) {
+ $errors[] = "Entry '{$slug}' missing 'name' string.";
+ }
+
+ if ( empty( $entry['option_prefixes'] ) || ! is_array( $entry['option_prefixes'] ) ) {
+ $errors[] = "Entry '{$slug}' missing non-empty 'option_prefixes' array.";
+ continue;
+ }
+
+ foreach ( $entry['option_prefixes'] as $prefix ) {
+ if ( ! is_string( $prefix ) || '' === $prefix ) {
+ $errors[] = "Entry '{$slug}' has empty/non-string prefix.";
+ continue;
+ }
+
+ // Reject dangerously generic prefixes that would match thousands of options.
+ if ( strlen( $prefix ) < 3 ) {
+ $errors[] = "Entry '{$slug}' prefix '{$prefix}' is too short (<3 chars).";
+ }
+
+ if ( in_array( $prefix, [ 'wp_', 'option_', '_transient_' ], true ) ) {
+ $errors[] = "Entry '{$slug}' prefix '{$prefix}' is reserved/dangerous.";
+ }
+
+ if ( isset( $seen_prefixes[ $prefix ] ) && $seen_prefixes[ $prefix ] !== $slug ) {
+ $warnings[] = "Prefix '{$prefix}' claimed by both '{$seen_prefixes[ $prefix ]}' and '{$slug}'.";
+ }
+ $seen_prefixes[ $prefix ] = $slug;
+ }
+}
+
+foreach ( $warnings as $warn ) {
+ fwrite( STDERR, "WARNING: {$warn}\n" );
+}
+
+if ( ! empty( $errors ) ) {
+ fwrite( STDERR, "Validation failed:\n" );
+ foreach ( $errors as $err ) {
+ fwrite( STDERR, " - {$err}\n" );
+ }
+ exit( 1 );
+}
+
+$count = count( $data );
+echo "OK: {$count} entries validated.\n";
+exit( 0 );
diff --git a/css/style.css b/css/style.css
index 21e90b6..39c4442 100644
--- a/css/style.css
+++ b/css/style.css
@@ -91,6 +91,38 @@ div.dt-container .dt-input {
.aaa-option-optimizer-popover__close:hover {
cursor: pointer;
}
+.aaa-report-trigger {
+ background: none;
+ border: 0;
+ padding: 0;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+ line-height: 1.4;
+ text-align: left;
+}
+.aaa-report-trigger .aaa-report-trigger__action {
+ display: block;
+ color: #2271b1;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+.aaa-report-trigger:hover .aaa-report-trigger__action,
+.aaa-report-trigger:focus-visible .aaa-report-trigger__action {
+ color: #135e96;
+}
+.aaa-report-popover {
+ min-width: 380px;
+ min-height: auto;
+}
+.aaa-report-popover .aaa-report-input {
+ width: 100%;
+ margin-top: 4px;
+}
+.aaa-report-popover .aaa-report-status {
+ min-height: 1.2em;
+ margin: 8px 0;
+}
.aaa-option-optimizer-tabs {
clear: both;
display: flex;
diff --git a/js/admin-script.js b/js/admin-script.js
index 3531a73..b4ef281 100644
--- a/js/admin-script.js
+++ b/js/admin-script.js
@@ -183,7 +183,11 @@ jQuery( document ).ready( function () {
className: 'select-all',
},
{ name: 'name', data: 'name' },
- { name: 'source', data: 'plugin' },
+ {
+ name: 'source',
+ data: 'plugin',
+ render: ( data, type, row ) => renderSourceColumn( row ),
+ },
{ name: 'size', data: 'size', searchable: false },
{
name: 'autoload',
@@ -204,7 +208,12 @@ jQuery( document ).ready( function () {
if ( selector === '#requested_do_not_exist_table' ) {
return [
{ name: 'option', data: 'name' },
- { name: 'source', data: 'plugin', searchable: false },
+ {
+ name: 'source',
+ data: 'plugin',
+ searchable: false,
+ render: ( data, type, row ) => renderSourceColumn( row ),
+ },
{ name: 'calls', data: 'count', searchable: false },
{
name: 'option_name',
@@ -227,7 +236,11 @@ jQuery( document ).ready( function () {
className: 'select-all',
},
{ name: 'name', data: 'name' },
- { name: 'source', data: 'plugin' },
+ {
+ name: 'source',
+ data: 'plugin',
+ render: ( data, type, row ) => renderSourceColumn( row ),
+ },
{ name: 'size', data: 'size', searchable: false },
{
name: 'autoload',
@@ -257,7 +270,11 @@ jQuery( document ).ready( function () {
className: 'select-all',
},
{ name: 'name', data: 'name' },
- { name: 'source', data: 'plugin' },
+ {
+ name: 'source',
+ data: 'plugin',
+ render: ( data, type, row ) => renderSourceColumn( row ),
+ },
{
name: 'size',
data: 'size',
@@ -383,6 +400,221 @@ jQuery( document ).ready( function () {
`;
}
+ /**
+ * Escape HTML for safe insertion as text.
+ *
+ * @param {string} unsafe - The string to escape.
+ * @return {string} - The escaped string.
+ */
+ function escapeHtml( unsafe ) {
+ return String( unsafe )
+ .replace( /&/g, '&' )
+ .replace( //g, '>' )
+ .replace( /"/g, '"' )
+ .replace( /'/g, ''' );
+ }
+
+ /**
+ * Renders the Source column. For unknown sources, wraps the label
+ * in a button that opens a Report Origin popover.
+ *
+ * @param {Object} row - The row data.
+ *
+ * @return {string} - The HTML for the source column.
+ */
+ let reportPopoverSeq = 0;
+ function renderSourceColumn( row ) {
+ const label = escapeHtml( row.plugin );
+ if ( row.plugin_known ) {
+ return label;
+ }
+ const popoverId = `aaa_report_${ ++reportPopoverSeq }`;
+ return `${ renderReportPopover( row, popoverId ) }
+ `;
+ }
+
+ /**
+ * Renders the Report Origin popover for an unknown row.
+ *
+ * @param {Object} row - The row data.
+ * @param {string} popoverId - The popover element id.
+ *
+ * @return {string} - The popover HTML.
+ */
+ function renderReportPopover( row, popoverId ) {
+ const i18n = aaaOptionOptimizer.i18n;
+ const optionName = escapeHtml( row.name );
+ return `
+
+
${ i18n.reportOriginOf } ${ optionName }
+
+
+
+
+
${ escapeHtml( i18n.reportPrivacyNote ) }
+
+
+
+
+
`;
+ }
+
+ /**
+ * Extract a wp.org plugin slug from a slug or URL.
+ *
+ * @param {string} input - User input.
+ * @return {string} - Normalized slug, or empty string if invalid.
+ */
+ function normalizeSlug( input ) {
+ const trimmed = String( input || '' ).trim();
+ if ( ! trimmed ) {
+ return '';
+ }
+ // Try to pull a slug out of a wordpress.org URL.
+ const urlMatch = trimmed.match(
+ /wordpress\.org\/plugins\/([a-z0-9-]+)/i
+ );
+ if ( urlMatch ) {
+ return urlMatch[ 1 ].toLowerCase();
+ }
+ // Otherwise treat input as a slug.
+ if ( /^[a-z0-9-]+$/i.test( trimmed ) ) {
+ return trimmed.toLowerCase();
+ }
+ return '';
+ }
+
+ // Per-popover state for the wp.org verification step.
+ const reportState = {};
+
+ /**
+ * Verify a slug against the wordpress.org plugin directory and update
+ * the popover UI accordingly.
+ *
+ * @param {jQuery} $popover - The popover jQuery element.
+ * @param {string} slug - The slug to verify.
+ */
+ function verifyReportSlug( $popover, slug ) {
+ const optionName = $popover.data( 'option' );
+ const i18n = aaaOptionOptimizer.i18n;
+ const $status = $popover.find( '.aaa-report-status' );
+ const $submit = $popover.find( '.aaa-report-submit' );
+
+ reportState[ optionName ] = { slug: '', verifiedName: '' };
+ $submit.prop( 'disabled', true );
+
+ if ( ! slug ) {
+ $status.text( '' );
+ return;
+ }
+
+ $status.text( i18n.reportVerifying );
+
+ jQuery
+ .ajax( {
+ url: `https://api.wordpress.org/plugins/info/1.0/${ encodeURIComponent(
+ slug
+ ) }.json`,
+ method: 'GET',
+ dataType: 'json',
+ timeout: 8000,
+ } )
+ .done( function ( data ) {
+ if ( ! data || data.error || ! data.name ) {
+ $status.text( i18n.reportNotFound );
+ return;
+ }
+ reportState[ optionName ] = {
+ slug,
+ verifiedName: data.name,
+ };
+ $status.html(
+ `✓ ${ i18n.reportVerified } ${ escapeHtml(
+ data.name
+ ) }`
+ );
+ $submit.prop( 'disabled', false );
+ } )
+ .fail( function () {
+ $status.text( i18n.reportVerifyError );
+ } );
+ }
+
+ /**
+ * Submit a report to the configured endpoint.
+ *
+ * @param {jQuery} $popover - The popover jQuery element.
+ */
+ function submitReport( $popover ) {
+ const optionName = $popover.data( 'option' );
+ const i18n = aaaOptionOptimizer.i18n;
+ const state = reportState[ optionName ];
+ if ( ! state || ! state.slug ) {
+ return;
+ }
+
+ const $status = $popover.find( '.aaa-report-status' );
+ const $submit = $popover.find( '.aaa-report-submit' );
+ $submit.prop( 'disabled', true );
+ $status.text( i18n.reportSubmitting );
+
+ jQuery
+ .ajax( {
+ url: aaaOptionOptimizer.reportUrl,
+ method: 'POST',
+ contentType: 'application/json',
+ data: JSON.stringify( {
+ option_name: optionName,
+ slug: state.slug,
+ site: window.location.hostname,
+ } ),
+ timeout: 10000,
+ } )
+ .done( function () {
+ $status.text( i18n.reportThanks );
+ } )
+ .fail( function () {
+ $status.text( i18n.reportFailed );
+ $submit.prop( 'disabled', false );
+ } );
+ }
+
+ // Debounced wp.org verification on input change. Per-popover timer so
+ // concurrently-open popovers don't cancel each other's verification.
+ jQuery( document ).on( 'input', '.aaa-report-input', function () {
+ const $popover = jQuery( this ).closest( '.aaa-report-popover' );
+ const raw = jQuery( this ).val();
+ const slug = normalizeSlug( raw );
+ const previous = $popover.data( 'verifyTimer' );
+ if ( previous ) {
+ clearTimeout( previous );
+ }
+ $popover.data(
+ 'verifyTimer',
+ setTimeout( () => verifyReportSlug( $popover, slug ), 350 )
+ );
+ } );
+
+ // Submit handler.
+ jQuery( document ).on( 'click', '.aaa-report-submit', function () {
+ const $popover = jQuery( this ).closest( '.aaa-report-popover' );
+ submitReport( $popover );
+ } );
+
jQuery( '#aaa-option-reset-data' ).on( 'click', function ( e ) {
e.preventDefault();
jQuery.ajax( {
@@ -651,8 +883,7 @@ jQuery( document ).ready( function () {
function migrateChunk() {
jQuery.ajax( {
url:
- aaaOptionOptimizer.root +
- 'aaa-option-optimizer/v1/migrate',
+ aaaOptionOptimizer.root + 'aaa-option-optimizer/v1/migrate',
method: 'POST',
beforeSend: ( xhr ) =>
xhr.setRequestHeader(
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 7c4ebd2..16b2f59 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -22,6 +22,9 @@
/coverage/*
/js/vendor/*
+
+ /bin/*
+
*.js
*.css
diff --git a/readme.txt b/readme.txt
index d0ae23a..8bfb6ff 100644
--- a/readme.txt
+++ b/readme.txt
@@ -52,8 +52,31 @@ Please do a pull request via GitHub on [this file](https://github.com/ProgressPl
1. Screenshot of the admin screen, initial tab.
2. Screenshot of the "All options" screen, showing you can browse all the options.
+== External services ==
+
+This plugin connects to two external services to identify the source plugins of WordPress options.
+
+= WordPress.org plugin directory (api.wordpress.org and ps.w.org) =
+
+Once a day, the plugin fetches an updated list of recognized plugins from `https://ps.w.org/aaa-option-optimizer/assets/known-plugins.json`. This lets the plugin identify newly-added plugins as the maintained list grows, without requiring a plugin update. Only the JSON file is requested; no data is sent.
+
+When you click "Report" on an option whose source is "Unknown" and type a plugin slug, the plugin queries `https://api.wordpress.org/plugins/info/1.0/{slug}.json` to verify the slug exists and to display the official plugin name for confirmation. Only the slug you type is sent.
+
+WordPress.org terms of service: https://wordpress.org/about/privacy/
+
+= Origin reporting endpoint (option-optimizer-api.progressplanner.com) =
+
+When you submit a "Report origin" form, the plugin sends the option name you reported, the wp.org plugin slug you supplied, and your site's hostname to `https://option-optimizer-api.progressplanner.com/submit`. The submission is recorded as a GitHub issue for a maintainer to review and add to the recognized plugins list. The site hostname is hashed before storage and never published. The option name and slug appear in the public GitHub issue. Submissions only happen when you click Submit on the Report form; nothing is sent automatically.
+
+The endpoint is operated by the plugin maintainers. The submission URL can be overridden via the `aaa_option_optimizer_report_url` filter for users who want to disable or redirect the feature.
+
== Changelog ==
+= 1.7.0 =
+
+* Add "Report origin" feature: for options whose source plugin is unknown, users can submit the matching wp.org slug to help maintainers expand the recognized plugins list. Submissions land as GitHub issues for maintainer review; no auto-merge.
+* Recognized-plugins list now refreshes from wp.org once a day in the background, so the list grows for users without requiring plugin updates.
+
= 1.6.1 =
* Fix infinite recursion in option access monitoring that could cause a fatal error in certain hosting environments.
diff --git a/src/class-admin-page.php b/src/class-admin-page.php
index e45a243..046bb82 100644
--- a/src/class-admin-page.php
+++ b/src/class-admin-page.php
@@ -185,6 +185,13 @@ public function enqueue_scripts( $hook ) {
true // In footer.
);
+ /**
+ * Filter the URL the plugin POSTs unknown-option reports to.
+ *
+ * @param string $url The submission endpoint URL.
+ */
+ $report_url = \apply_filters( 'aaa_option_optimizer_report_url', 'https://option-optimizer-api.progressplanner.com/submit' );
+
\wp_localize_script(
'aaa-option-optimizer-admin-js',
'aaaOptionOptimizer',
@@ -192,6 +199,7 @@ public function enqueue_scripts( $hook ) {
'root' => \esc_url_raw( \rest_url() ),
'nonce' => \wp_create_nonce( 'wp_rest' ),
'migration' => Database::get_migration_status(),
+ 'reportUrl' => \esc_url_raw( $report_url ),
'i18n' => [
'filterBySource' => \esc_html__( 'Filter by source', 'aaa-option-optimizer' ),
'showValue' => \esc_html__( 'Show', 'aaa-option-optimizer' ),
@@ -199,6 +207,21 @@ public function enqueue_scripts( $hook ) {
'removeAutoload' => \esc_html__( 'Remove autoload', 'aaa-option-optimizer' ),
'deleteOption' => \esc_html__( 'Delete', 'aaa-option-optimizer' ),
'createOptionFalse' => \esc_html__( 'Create option with value false', 'aaa-option-optimizer' ),
+ 'unknownLabel' => \esc_html__( 'Unknown', 'aaa-option-optimizer' ),
+ 'reportOrigin' => \esc_html__( 'Report', 'aaa-option-optimizer' ),
+ 'reportOriginOf' => \esc_html__( 'Report origin of', 'aaa-option-optimizer' ),
+ 'reportSlugOrUrlLabel' => \esc_html__( 'wp.org slug or URL', 'aaa-option-optimizer' ),
+ 'reportSlugPlaceholder' => \esc_html__( 'e.g. wp125 or https://wordpress.org/plugins/wp125/', 'aaa-option-optimizer' ),
+ 'reportVerifying' => \esc_html__( 'Checking wp.org…', 'aaa-option-optimizer' ),
+ 'reportNotFound' => \esc_html__( 'Plugin not found on wordpress.org.', 'aaa-option-optimizer' ),
+ 'reportVerifyError' => \esc_html__( 'Could not verify with wordpress.org.', 'aaa-option-optimizer' ),
+ 'reportVerified' => \esc_html__( 'Verified:', 'aaa-option-optimizer' ),
+ 'reportSubmit' => \esc_html__( 'Submit', 'aaa-option-optimizer' ),
+ 'reportCancel' => \esc_html__( 'Cancel', 'aaa-option-optimizer' ),
+ 'reportSubmitting' => \esc_html__( 'Submitting…', 'aaa-option-optimizer' ),
+ 'reportThanks' => \esc_html__( 'Thanks! Your report has been submitted.', 'aaa-option-optimizer' ),
+ 'reportFailed' => \esc_html__( 'Submission failed. Please try again.', 'aaa-option-optimizer' ),
+ 'reportPrivacyNote' => \esc_html__( 'We send the option name and the slug you provide. Never the option value.', 'aaa-option-optimizer' ),
'noAutoloadedButNotUsed' => \esc_html__( 'All autoloaded options are in use.', 'aaa-option-optimizer' ),
'noUsedButNotAutoloaded' => \esc_html__( 'All options that are used are autoloaded.', 'aaa-option-optimizer' ),
'noOptionsSelected' => \esc_html__( 'No options selected.', 'aaa-option-optimizer' ),
diff --git a/src/class-known-plugins.php b/src/class-known-plugins.php
new file mode 100644
index 0000000..9c20e70
--- /dev/null
+++ b/src/class-known-plugins.php
@@ -0,0 +1,98 @@
+>|null
+ */
+ private $list;
+
+ /**
+ * Get the known-plugins mapping.
+ *
+ * @return array>
+ */
+ public function get(): array {
+ if ( null !== $this->list ) {
+ return $this->list;
+ }
+
+ $cached = \get_option( self::CACHE_KEY );
+ if ( \is_array( $cached ) && ! empty( $cached ) ) {
+ $this->list = $cached;
+ return $this->list;
+ }
+
+ $this->list = $this->load_bundled();
+ return $this->list;
+ }
+
+ /**
+ * Refresh the cached mapping from the remote URL.
+ *
+ * @return bool True when a fresh copy was stored, false otherwise.
+ */
+ public function refresh(): bool {
+ $response = \wp_remote_get(
+ self::REMOTE_URL,
+ [
+ 'timeout' => 10,
+ ]
+ );
+
+ if ( \is_wp_error( $response ) ) {
+ return false;
+ }
+
+ if ( 200 !== \wp_remote_retrieve_response_code( $response ) ) {
+ return false;
+ }
+
+ $body = \wp_remote_retrieve_body( $response );
+ $data = \json_decode( $body, true );
+
+ if ( ! \is_array( $data ) || empty( $data ) ) {
+ return false;
+ }
+
+ \update_option( self::CACHE_KEY, $data, false );
+ $this->list = $data;
+ return true;
+ }
+
+ /**
+ * Load the JSON file bundled with the plugin.
+ *
+ * @return array>
+ */
+ private function load_bundled(): array {
+ $path = \plugin_dir_path( AAA_OPTION_OPTIMIZER_FILE ) . 'known-plugins/known-plugins.json';
+ if ( ! \file_exists( $path ) ) {
+ return [];
+ }
+
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a file bundled with the plugin.
+ $data = \json_decode( (string) \file_get_contents( $path ), true );
+ return \is_array( $data ) ? $data : [];
+ }
+}
diff --git a/src/class-map-plugin-to-options.php b/src/class-map-plugin-to-options.php
index 799d8fe..ffeaae1 100644
--- a/src/class-map-plugin-to-options.php
+++ b/src/class-map-plugin-to-options.php
@@ -16,7 +16,7 @@ class Map_Plugin_To_Options {
/**
* List of plugins we can recognize.
*
- * @var object[]
+ * @var array>
*/
private $plugins_list = [];
@@ -28,22 +28,42 @@ class Map_Plugin_To_Options {
* @return string
*/
public function get_plugin_name( string $option ): string {
- $plugins_list = [];
+ $match = $this->find_match( $option );
+ return null !== $match ? $match : __( 'Unknown', 'aaa-option-optimizer' );
+ }
+
+ /**
+ * Whether the option's source plugin is known.
+ *
+ * @param string $option The option name.
+ *
+ * @return bool
+ */
+ public function is_known( string $option ): bool {
+ return null !== $this->find_match( $option );
+ }
+
+ /**
+ * Look up the plugin name for an option, or null if no match.
+ *
+ * @param string $option The option name.
+ *
+ * @return string|null
+ */
+ private function find_match( string $option ): ?string {
if ( empty( $this->plugins_list ) ) {
- $this->plugins_list = json_decode( file_get_contents( plugin_dir_path( AAA_OPTION_OPTIMIZER_FILE ) . 'known-plugins/known-plugins.json' ), true );
+ $known = new Known_Plugins();
+ $this->plugins_list = $known->get();
}
- // for each plugin in the list, check if the option starts with the prefix.
foreach ( $this->plugins_list as $plugin ) {
foreach ( $plugin['option_prefixes'] as $prefix ) {
- if ( strpos( $option, $prefix ) === 0 ) {
- if ( isset( $plugin['name'] ) ) {
- return $plugin['name'];
- }
+ if ( strpos( $option, $prefix ) === 0 && isset( $plugin['name'] ) ) {
+ return $plugin['name'];
}
}
}
- return __( 'Unknown', 'aaa-option-optimizer' );
+ return null;
}
}
diff --git a/src/class-plugin.php b/src/class-plugin.php
index 7213d73..5d91f48 100644
--- a/src/class-plugin.php
+++ b/src/class-plugin.php
@@ -79,6 +79,12 @@ public function register_hooks() {
// Use the shutdown action to update the option with tracked data.
\add_action( 'shutdown', [ $this, 'update_tracked_options' ] );
+ // Daily refresh of the known-plugins mapping.
+ \add_action( Known_Plugins::CRON_HOOK, [ $this, 'refresh_known_plugins' ] );
+ if ( ! \wp_next_scheduled( Known_Plugins::CRON_HOOK ) ) {
+ \wp_schedule_event( \time(), 'daily', Known_Plugins::CRON_HOOK );
+ }
+
// Register the REST routes.
$rest = new REST();
$rest->register_hooks();
@@ -160,6 +166,15 @@ protected function add_option_usage( $option_name ) {
++$this->accessed_options[ $option_name ];
}
+ /**
+ * Refresh the cached known-plugins mapping from wp.org.
+ *
+ * @return void
+ */
+ public function refresh_known_plugins() {
+ ( new Known_Plugins() )->refresh();
+ }
+
/**
* Update the tracked options at the end of the page load.
*
diff --git a/src/class-rest.php b/src/class-rest.php
index 472eea4..26ecc50 100644
--- a/src/class-rest.php
+++ b/src/class-rest.php
@@ -228,12 +228,13 @@ public function get_all_options() {
$options = $wpdb->get_results( "SELECT option_name, option_value, autoload FROM $wpdb->options" );
foreach ( $options as $option ) {
$output[] = [
- 'name' => $option->option_name,
- 'plugin' => $this->get_plugin_name( $option->option_name ),
- 'value' => htmlentities( $option->option_value, ENT_QUOTES | ENT_SUBSTITUTE ),
- 'size' => $this->get_length( $option->option_value ),
- 'raw_size' => strlen( $option->option_value ),
- 'autoload' => $option->autoload,
+ 'name' => $option->option_name,
+ 'plugin' => $this->get_plugin_name( $option->option_name ),
+ 'plugin_known' => $this->is_plugin_known( $option->option_name ),
+ 'value' => htmlentities( $option->option_value, ENT_QUOTES | ENT_SUBSTITUTE ),
+ 'size' => $this->get_length( $option->option_value ),
+ 'raw_size' => strlen( $option->option_value ),
+ 'autoload' => $option->autoload,
];
}
return new \WP_REST_Response( [ 'data' => $output ], 200 );
@@ -320,12 +321,13 @@ public function get_unused_options() {
// Format output.
foreach ( $results as $row ) {
$response_data[] = [
- 'name' => $row->option_name,
- 'plugin' => $this->get_plugin_name( $row->option_name ),
- 'value' => htmlentities( $row->option_value, ENT_QUOTES | ENT_SUBSTITUTE ),
- 'size' => $this->get_length( $row->option_value ),
- 'raw_size' => strlen( $row->option_value ),
- 'autoload' => 'yes',
+ 'name' => $row->option_name,
+ 'plugin' => $this->get_plugin_name( $row->option_name ),
+ 'plugin_known' => $this->is_plugin_known( $row->option_name ),
+ 'value' => htmlentities( $row->option_value, ENT_QUOTES | ENT_SUBSTITUTE ),
+ 'size' => $this->get_length( $row->option_value ),
+ 'raw_size' => strlen( $row->option_value ),
+ 'autoload' => 'yes',
];
}
@@ -459,13 +461,14 @@ function ( $option_name ) use ( $search ) {
foreach ( $results as $row ) {
$response_data[] = [
- 'name' => $row->option_name,
- 'plugin' => $this->get_plugin_name( $row->option_name ),
- 'value' => htmlentities( maybe_serialize( $row->option_value ), ENT_QUOTES | ENT_SUBSTITUTE ),
- 'size' => $this->get_length( $row->option_value ),
- 'raw_size' => strlen( $row->option_value ),
- 'autoload' => 'no',
- 'count' => $used_options[ $row->option_name ] ?? 0,
+ 'name' => $row->option_name,
+ 'plugin' => $this->get_plugin_name( $row->option_name ),
+ 'plugin_known' => $this->is_plugin_known( $row->option_name ),
+ 'value' => htmlentities( maybe_serialize( $row->option_value ), ENT_QUOTES | ENT_SUBSTITUTE ),
+ 'size' => $this->get_length( $row->option_value ),
+ 'raw_size' => strlen( $row->option_value ),
+ 'autoload' => 'no',
+ 'count' => $used_options[ $row->option_name ] ?? 0,
];
}
@@ -560,10 +563,11 @@ public function get_options_that_do_not_exist() {
foreach ( $non_autoloaded_keys as $option => $count ) {
if ( ! isset( $existing_keys[ $option ] ) ) {
$non_existing_options[ $option ] = [
- 'name' => $option,
- 'plugin' => $this->get_plugin_name( $option ),
- 'count' => $count,
- 'option_name' => $option,
+ 'name' => $option,
+ 'plugin' => $this->get_plugin_name( $option ),
+ 'plugin_known' => $this->is_plugin_known( $option ),
+ 'count' => $count,
+ 'option_name' => $option,
];
}
}
@@ -866,6 +870,17 @@ private function get_plugin_name( $option ) {
return $this->map_plugin_to_options->get_plugin_name( $option );
}
+ /**
+ * Whether the option's source plugin is known.
+ *
+ * @param string $option The option name.
+ *
+ * @return bool
+ */
+ private function is_plugin_known( $option ) {
+ return $this->map_plugin_to_options->is_known( $option );
+ }
+
/**
* Filter options array by source (plugin) name.
*
diff --git a/uninstall.php b/uninstall.php
index 05bf9fe..630eb9d 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -24,3 +24,9 @@
// Delete the plugin option.
delete_option( 'option_optimizer' );
+
+// Delete the cached known-plugins mapping.
+delete_option( 'aaa_option_optimizer_known_plugins' );
+
+// Clear the daily refresh cron event (defense-in-depth; deactivation already does this).
+wp_clear_scheduled_hook( 'aaa_option_optimizer_refresh_known_plugins' );