From f4f89fa6851db5d2b25c6bf81d971a7dc3399fb2 Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Thu, 7 May 2026 17:02:20 +0200
Subject: [PATCH 01/15] Fetch known-plugins.json from wp.org with daily refresh
Adds a Known_Plugins loader that caches a remote copy of the mapping
and falls back to the bundled JSON. A daily WP-Cron event refreshes
the cache from https://ps.w.org/aaa-option-optimizer/assets/known-plugins.json.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
aaa-option-optimizer.php | 7 +++
src/class-known-plugins.php | 98 +++++++++++++++++++++++++++++
src/class-map-plugin-to-options.php | 6 +-
src/class-plugin.php | 15 +++++
4 files changed, 123 insertions(+), 3 deletions(-)
create mode 100644 src/class-known-plugins.php
diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php
index 3bacb4d..6d5e740 100644
--- a/aaa-option-optimizer.php
+++ b/aaa-option-optimizer.php
@@ -37,6 +37,11 @@ function aaa_option_optimizer_activation() {
// Create the custom table.
Progress_Planner\OptionOptimizer\Database::create_table();
+ // Schedule daily refresh of the known-plugins mapping.
+ if ( ! wp_next_scheduled( Progress_Planner\OptionOptimizer\Known_Plugins::CRON_HOOK ) ) {
+ wp_schedule_event( time(), 'daily', Progress_Planner\OptionOptimizer\Known_Plugins::CRON_HOOK );
+ }
+
$autoload_values = \wp_autoload_values_to_autoload();
$placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) );
@@ -73,6 +78,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/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..15d781f 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,9 +28,9 @@ class Map_Plugin_To_Options {
* @return string
*/
public function get_plugin_name( string $option ): string {
- $plugins_list = [];
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.
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.
*
From 0e9a69f9c899d04ff5a35598458fb81e387df86e Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Thu, 7 May 2026 17:02:26 +0200
Subject: [PATCH 02/15] Sync known-plugins.json to wp.org SVN assets daily
Daily GitHub Action validates known-plugins.json and publishes it to
the plugin's wp.org /assets/ directory so installs can fetch updates
without waiting for a release. Validator rejects malformed JSON,
missing fields, and dangerously generic prefixes.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/workflows/sync-known-plugins.yml | 83 +++++++++++++++++++++
bin/validate-known-plugins.php | 94 ++++++++++++++++++++++++
2 files changed, 177 insertions(+)
create mode 100644 .github/workflows/sync-known-plugins.yml
create mode 100644 bin/validate-known-plugins.php
diff --git a/.github/workflows/sync-known-plugins.yml b/.github/workflows/sync-known-plugins.yml
new file mode 100644
index 0000000..5f6b8b1
--- /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:
+ - develop
+ - 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
+ uses: actions/checkout@v4
+
+ - 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 || svn mkdir --parents svn-repo/assets
+
+ - name: Copy JSON into assets and detect changes
+ id: diff
+ run: |
+ mkdir -p svn-repo/assets
+ 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/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 );
From 85b4d792278cddbc99b613f73f6912c88fd3295d Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Thu, 7 May 2026 17:54:10 +0200
Subject: [PATCH 03/15] Exclude bin/ CLI scripts from PHPCS
The validator runs in GitHub Actions, not in WordPress, so
WordPress-specific sniffs (escaping, $-prefix globals, WP_Filesystem)
don't apply.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
phpcs.xml.dist | 3 +++
1 file changed, 3 insertions(+)
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
From 267468ed07e539ad877e224a5822e02b425c1294 Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Fri, 8 May 2026 09:52:08 +0200
Subject: [PATCH 04/15] Drop redundant activation-hook scheduling, clean up on
uninstall
The runtime guard in Plugin::register_hooks() already self-heals the
schedule for every install. Activation-time scheduling was redundant.
Uninstall now also removes the cached mapping option and clears the
cron event as defense-in-depth.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
aaa-option-optimizer.php | 5 -----
uninstall.php | 6 ++++++
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/aaa-option-optimizer.php b/aaa-option-optimizer.php
index 6d5e740..0cd997b 100644
--- a/aaa-option-optimizer.php
+++ b/aaa-option-optimizer.php
@@ -37,11 +37,6 @@ function aaa_option_optimizer_activation() {
// Create the custom table.
Progress_Planner\OptionOptimizer\Database::create_table();
- // Schedule daily refresh of the known-plugins mapping.
- if ( ! wp_next_scheduled( Progress_Planner\OptionOptimizer\Known_Plugins::CRON_HOOK ) ) {
- wp_schedule_event( time(), 'daily', Progress_Planner\OptionOptimizer\Known_Plugins::CRON_HOOK );
- }
-
$autoload_values = \wp_autoload_values_to_autoload();
$placeholders = implode( ',', array_fill( 0, count( $autoload_values ), '%s' ) );
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' );
From 931786e88dbb066e0715e38d3f932863def55e3f Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Fri, 8 May 2026 10:21:03 +0200
Subject: [PATCH 05/15] Add Report origin button + popover for unknown options
For rows where no plugin matched, the Source column becomes a button
that opens a popover. User pastes a wp.org slug or URL; the plugin
verifies it via api.wordpress.org and shows the official plugin name.
On submit, the report is POSTed to the configurable submission endpoint
(filterable via aaa_option_optimizer_report_url) for the maintainer
to triage.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.gitignore | 1 +
css/style.css | 25 +++
js/admin-script.js | 242 +++++++++++++++++++++++++++-
src/class-admin-page.php | 23 +++
src/class-map-plugin-to-options.php | 32 +++-
src/class-rest.php | 61 ++++---
6 files changed, 349 insertions(+), 35 deletions(-)
diff --git a/.gitignore b/.gitignore
index 92fecf3..7197ee9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/coverage/
.phpunit.result.cache
node_modules
+/submission-worker/
diff --git a/css/style.css b/css/style.css
index 21e90b6..3b2d1ee 100644
--- a/css/style.css
+++ b/css/style.css
@@ -91,6 +91,31 @@ div.dt-container .dt-input {
.aaa-option-optimizer-popover__close:hover {
cursor: pointer;
}
+.aaa-report-trigger {
+ background: none;
+ border: 1px dashed #c3c4c7;
+ color: #2271b1;
+ padding: 2px 8px;
+ font: inherit;
+ cursor: pointer;
+ border-radius: 3px;
+}
+.aaa-report-trigger:hover {
+ background: #f6f7f7;
+ border-color: #2271b1;
+}
+.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..4c49d0b 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,220 @@ 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.
+ */
+ function renderSourceColumn( row ) {
+ const label = escapeHtml( row.plugin );
+ if ( row.plugin_known ) {
+ return label;
+ }
+ const popoverId = `report_${ row.name.replace(
+ /[^a-zA-Z0-9_-]/g,
+ '_'
+ ) }`;
+ 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.
+ let reportInputTimer = null;
+ 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 );
+ clearTimeout( reportInputTimer );
+ reportInputTimer = 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 +882,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/src/class-admin-page.php b/src/class-admin-page.php
index e45a243..28f6c22 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 origin', '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-map-plugin-to-options.php b/src/class-map-plugin-to-options.php
index 15d781f..ffeaae1 100644
--- a/src/class-map-plugin-to-options.php
+++ b/src/class-map-plugin-to-options.php
@@ -28,22 +28,42 @@ class Map_Plugin_To_Options {
* @return string
*/
public function get_plugin_name( string $option ): string {
+ $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 ) ) {
$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-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.
*
From b1a65e3db5baec14bb6f7804d2fd36be01c26ad1 Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Fri, 8 May 2026 10:32:06 +0200
Subject: [PATCH 06/15] Tighten Report trigger styling
Drop dashed-border button look. Render label and action as plain text
with the action subtly underlined. Force single-line so the row
doesn't grow when the trigger is shown.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
css/style.css | 23 ++++++++++++++++-------
js/admin-script.js | 6 +++---
src/class-admin-page.php | 2 +-
3 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/css/style.css b/css/style.css
index 3b2d1ee..da0bd42 100644
--- a/css/style.css
+++ b/css/style.css
@@ -93,16 +93,25 @@ div.dt-container .dt-input {
}
.aaa-report-trigger {
background: none;
- border: 1px dashed #c3c4c7;
- color: #2271b1;
- padding: 2px 8px;
+ border: 0;
+ padding: 0;
font: inherit;
+ color: inherit;
cursor: pointer;
- border-radius: 3px;
+ white-space: nowrap;
+ line-height: inherit;
}
-.aaa-report-trigger:hover {
- background: #f6f7f7;
- border-color: #2271b1;
+.aaa-report-trigger .aaa-report-trigger__action {
+ color: #2271b1;
+ margin-left: 4px;
+ text-decoration: underline;
+ text-decoration-style: dotted;
+ text-underline-offset: 2px;
+}
+.aaa-report-trigger:hover .aaa-report-trigger__action,
+.aaa-report-trigger:focus-visible .aaa-report-trigger__action {
+ color: #135e96;
+ text-decoration-style: solid;
}
.aaa-report-popover {
min-width: 380px;
diff --git a/js/admin-script.js b/js/admin-script.js
index 4c49d0b..0b408f4 100644
--- a/js/admin-script.js
+++ b/js/admin-script.js
@@ -435,9 +435,9 @@ jQuery( document ).ready( function () {
return `${ renderReportPopover( row, popoverId ) }
`;
+ ) }">${ label }${
+ aaaOptionOptimizer.i18n.reportOrigin
+ }`;
}
/**
diff --git a/src/class-admin-page.php b/src/class-admin-page.php
index 28f6c22..046bb82 100644
--- a/src/class-admin-page.php
+++ b/src/class-admin-page.php
@@ -208,7 +208,7 @@ public function enqueue_scripts( $hook ) {
'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 origin', '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' ),
From a43c89c291c6f437765c8262836d1a47b312bcf2 Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Fri, 8 May 2026 10:33:30 +0200
Subject: [PATCH 07/15] Stack Report link under the Unknown label
Co-Authored-By: Claude Opus 4.7 (1M context)
---
css/style.css | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/css/style.css b/css/style.css
index da0bd42..39c4442 100644
--- a/css/style.css
+++ b/css/style.css
@@ -98,20 +98,18 @@ div.dt-container .dt-input {
font: inherit;
color: inherit;
cursor: pointer;
- white-space: nowrap;
- line-height: inherit;
+ line-height: 1.4;
+ text-align: left;
}
.aaa-report-trigger .aaa-report-trigger__action {
+ display: block;
color: #2271b1;
- margin-left: 4px;
text-decoration: underline;
- text-decoration-style: dotted;
text-underline-offset: 2px;
}
.aaa-report-trigger:hover .aaa-report-trigger__action,
.aaa-report-trigger:focus-visible .aaa-report-trigger__action {
color: #135e96;
- text-decoration-style: solid;
}
.aaa-report-popover {
min-width: 380px;
From dc7198f6dc07a00ad37304e9de1aa6a2777fc6ab Mon Sep 17 00:00:00 2001
From: Filip Ilic
Date: Fri, 8 May 2026 11:50:53 +0200
Subject: [PATCH 08/15] Tighten Report popover and SVN sync workflow
- Per-popover verify-debounce timer instead of a single shared one,
so concurrently-open popovers don't cancel each other's lookup.
- Stable popover ids via a counter, eliminating collisions when two
unknown option names normalize to the same slug.
- Drop dead svn-mkdir fallback and redundant mkdir in the SVN sync
workflow; assets/ always exists on wp.org plugin SVN.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/workflows/sync-known-plugins.yml | 3 +--
js/admin-script.js | 21 +++++++++++----------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/sync-known-plugins.yml b/.github/workflows/sync-known-plugins.yml
index 5f6b8b1..b1231a4 100644
--- a/.github/workflows/sync-known-plugins.yml
+++ b/.github/workflows/sync-known-plugins.yml
@@ -43,12 +43,11 @@ jobs:
--non-interactive \
--no-auth-cache \
https://plugins.svn.wordpress.org/aaa-option-optimizer/ svn-repo
- svn update --set-depth infinity svn-repo/assets || svn mkdir --parents svn-repo/assets
+ svn update --set-depth infinity svn-repo/assets
- name: Copy JSON into assets and detect changes
id: diff
run: |
- mkdir -p svn-repo/assets
cp known-plugins/known-plugins.json svn-repo/assets/known-plugins.json
cd svn-repo
# Add the file if it's new
diff --git a/js/admin-script.js b/js/admin-script.js
index 0b408f4..b4ef281 100644
--- a/js/admin-script.js
+++ b/js/admin-script.js
@@ -423,15 +423,13 @@ jQuery( document ).ready( function () {
*
* @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 = `report_${ row.name.replace(
- /[^a-zA-Z0-9_-]/g,
- '_'
- ) }`;
+ const popoverId = `aaa_report_${ ++reportPopoverSeq }`;
return `${ renderReportPopover( row, popoverId ) }
${ escapeHtml( i18n.reportPrivacyNote ) }
+ ${ renderConsentField() }