Fetch known-plugins.json from our own server with daily refresh + stats - #78
Draft
ilicfilip wants to merge 29 commits into
Draft
Fetch known-plugins.json from our own server with daily refresh + stats#78ilicfilip wants to merge 29 commits into
ilicfilip wants to merge 29 commits into
Conversation
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
|
Test on Playground |
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
The submission worker is now maintained in its own separate repository, so it no longer needs a local exclusion in this repo's gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cron, push, and workflow_dispatch all check out main now. Develop work no longer propagates to wp.org SVN — main is the source of truth for what installs fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "External services" section disclosing wp.org and the report-submission endpoint, as required by wp.org plugin review. Adds a 1.7.0 changelog entry covering the Report-origin feature and the daily known-plugins refresh. Version bump itself lands in the release PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ilicfilip
force-pushed
the
filip/remote-known-plugins
branch
from
June 1, 2026 08:29
9df50a8 to
95e5595
Compare
The WP.org plugin team asked us not to fetch the data file from wordpress.org (ps.w.org). Move the fetch to our own Cloudflare worker at option-optimizer-api.progressplanner.com, and POST the plugin and WordPress versions with the request so the maintainers can keep anonymous usage statistics. No site identity is sent. Add the aaa_option_optimizer_known_plugins_url filter so the fetch can be redirected or disabled, and update the External services disclosure in readme.txt accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The known-plugins mapping is now served from our own Cloudflare worker (option-optimizer-api.progressplanner.com), not from wordpress.org, so the workflow that published the file to the wp.org SVN /assets/ directory is no longer used. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the aaa_option_optimizer_plugin_name filter to Map_Plugin_To_Options so integrations can recognize options the bundled known-plugins list does not, or override an existing match. Returning null leaves the option unrecognized. The filter is applied in find_match(), the single lookup used by both get_plugin_name() and is_known(), so a filtered match is treated as known everywhere (e.g. it suppresses the "Report origin" prompt). The filter name follows the existing aaa_option_optimizer_* convention. Requested by a user. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daily known-plugins refresh contacts our server and sends the plugin and WordPress version for anonymous stats. The WP.org plugin team allows this only with the user's knowledgeable consent, so make it opt-in: - Add a remote_data_consent setting (default off). Known_Plugins::refresh() bails without it, and the daily cron is only scheduled while it is on (and cleared when it is turned off). - Add a checkbox on the settings tab to grant/revoke consent, with a description of what is fetched and sent. - In the Report popover (which also contacts our server), show an inline consent checkbox when consent has not been granted; Submit stays disabled until it is ticked. Granting it there persists consent via a new /set-consent REST route, so the daily refresh starts and the user is not asked again. - Update the readme External services disclosure and changelog to note the fetch is opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous Playground ZIP artifact expired and was removed by GitHub. Empty commit to re-trigger the Playground Comment workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
The slug field required typing a wp.org slug from memory, which is the hardest part of reporting an unknown option. Attach a datalist of the plugins installed on the site so the slug can be picked from a list. The option value is the slug (what gets submitted) and the label is the plugin name, so the list reads well while filling in the right value. Suggestions are derived from each plugin's directory name and are read locally; nothing about the installed plugins is transmitted. The input stays free text on purpose. Unknown options are frequently orphans left behind by a plugin that has since been deleted, so the plugin cannot be offered as a suggestion and must still be reportable by typing a slug or pasting a wp.org URL. A datalist pick fires the same input event as typing, so both routes go through the existing debounced wordpress.org verification unchanged. A directory name that differs from the wp.org slug simply fails that check rather than producing a bad report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
A report identified a single option, so a maintainer had to generalize to a prefix by hand before the mapping could recognize the plugin's other options. Submit a suggested prefix as well, letting one report cover every option sharing it. Both fields are sent. The prefix is a guess and the option name is ground truth, so the maintainer needs the latter to verify the former. The suggestion strips transient wrappers and keeps everything through the first separator, matching how prefixes in the mapping are written -- 108 of the 139 non-core entries end at an underscore, 7 at a hyphen. It cannot be trusted blindly: "wordpress_api_key" belongs to Akismet, not core, and yields an overly broad "wordpress_". So the field is editable and may be cleared, which reports only the option that was clicked. The privacy note now enumerates the fuller payload, including the site address it has been sending all along. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
The prefix field was always visible and always prefilled, which asked the user about a prefix before they had said which plugin the option belongs to. Hide the field until wp.org verification succeeds, then reveal it. The suggestion itself was also wrong for a whole class of options. Core keys several transients by plugin slug, so `_site_transient_wp_plugin_dependencies_plugin_timeout_progress-planner` yielded `wp_` -- core's prefix, not the plugin's, and broad enough to mismap a large share of core options. The plugin's actual identity sits at the end of that name, not the start. The suggestion is now derived against the confirmed slug and only offered when the two share a leading stem, with core's own prefixes rejected outright. No confident suggestion leaves the field empty, which reports just the one option. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
Three problems, all visible while typing a slug by hand. The popover was sized by its content, so revealing the prefix field widened it mid-interaction. Give it a fixed width and lay the prefix input out like the slug input above it. Verification fired 350 ms after every keystroke, so a slug typed by hand was checked at each prefix and reported "Plugin not found" for a name still being written. Wait 900 ms after a typed character; a datalist pick or a paste arrives whole, so those still verify promptly. Typing "progress-planner" passes through "progress", which is itself a real plugin, so verification succeeded mid-word and the prefix field appeared for the wrong plugin. Responses now write only when the input still holds the slug they were for, and the reveal is deferred and re-checked, so it follows a settled answer rather than flickering through the intermediate ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
wordpress.org returns plugin names with HTML entities already encoded, so escaping one for display encoded the ampersand a second time and rendered a literal "&" -- "Rankings & Traffic" instead of "Rankings & Traffic". Decode the entities before escaping. Escaping still happens, so markup in a plugin name is still inert; the worker already decodes these server-side for the same reason. The popover also carried `max-height: 80%` with `overflow: visible`, so on a short viewport the content ran past the edge and the Cancel and Submit buttons were unreachable. Scroll the popover body instead, and make the close button sticky so it does not scroll away with the content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
The native datalist rendered as an unstyleable OS popup, positioned inconsistently across browsers, and led with the slug when the plugin name is what people actually recognize. Build the list in the page instead: plugin name first with the slug alongside, filtered on both as you type, prefix matches ranked ahead of substring ones, and styled with the rest of the popover. Keyboard support is the usual combobox contract -- arrows move a highlight, Enter commits it, Escape closes the list without disturbing the popover -- announced through role="combobox" and aria-activedescendant. Typing a slug by hand stays first class, which matters because unknown options are often orphans of a plugin that is no longer installed and so can never appear in the list. Arrowing highlights without overwriting the field, Enter with nothing highlighted verifies what was typed rather than being swallowed by the list, and a query matching no installed plugin closes the list to leave a plain text field. The list markup is a div, not a p: a paragraph cannot contain a list, and the browser was silently reparenting the ul out to the popover, where it picked up the popover's padding box and no longer lined up with the field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
The known-plugins mapping is maintained per prefix, not per plugin, so a plugin is often half-recognized: `wpseo_titles` resolves to Yoast while `indexables_indexation_reason` beside it shows a bare "Unknown" and asks the user to identify a plugin that is already named several rows above. The site knows which plugins are installed, so an unrecognized option naming one of them says where it most likely came from. Prefill the Report form with that slug and verify it as the popover opens, so the common case becomes agreeing with an answer rather than supplying one. Deliberately kept out of the table: the Source column already stacks a label and an action, and a "probably X" label is several times longer than "Unknown" -- names in the mapping run to 122 characters. The guess lives only in the form, where there is room for it. The guess is evidence, never a mapping. It prefills a field that still goes through the same wordpress.org verification, and can be changed or cleared. Slugs under five characters are ignored because short tokens collide with ordinary words, and the longest matching slug wins so `elementor-pro` is preferred over `elementor`. Bound to the trigger's click rather than the popover's `toggle` event, which does not bubble and so is invisible to delegated handlers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kTB3EpJtSszyevnSWd3yB
The endpoint explains why it turned a submission down ("Invalid
option_name", "Rate limit exceeded"), but the failure handler replaced
that with a generic "please try again". That hides the one piece of
information the user needs and invites a retry that cannot succeed.
Prefer the server's reason when there is one, parenthesised after the
generic message so it stays visibly attributed to the endpoint rather
than reading as the plugin's own wording.
A successful submission only swapped the status line to "Thanks" and left the popover open with its Submit button disabled, so the sole remaining action was "Cancel" -- which reads like undoing the thing that was just done. Testers reported it as the modal failing to close. Dismiss it a beat after the confirmation instead. The delay keeps the thanks message readable and gives the aria-live status time to start being announced, and the popover is closed through hidePopover() so the browser's own open state stays in step. The element is reused across opens, so a pending close is cancelled when the popover is reopened, and the timer re-checks that the popover is still open before firing -- the user may have dismissed it first, and hidePopover() throws on an element that is not open.
Reports are POSTed to an external endpoint and nothing was kept locally, so the only trace a report had happened was an in-memory JS object that died on reload. After refreshing, an already-reported option offered "Report" again as though nothing had been sent, and reopening the form showed none of what was submitted. A tester hit exactly this. Record the submission site-side and use it to say so: the trigger reads "Reported" instead of "Report", and the form notes which plugin it was reported as and prefills the slug that was actually sent rather than re-guessing from the option name. The wording claims only what we know. The endpoint is fire-and-forget into a moderation queue and never reports an outcome back, so the record means "we sent this", never "this was accepted", and the note says awaiting review rather than implying a decision. Re-reporting stays open, because a report that named the wrong plugin needs a way to be corrected; a new report overwrites the old record. The data sits beside `settings` rather than inside it, since the settings page saves that subarray wholesale and would otherwise drop it.
Recognition matches option prefixes against the shared known-plugins mapping, and a submitted report cannot change that: the endpoint is an inbox, and a maintainer publishes the mapping. So after reporting one option, its siblings stayed unrecognized and their forms stayed empty. The guess that fills those forms compares the option name against installed plugin slugs, which cannot bridge an abbreviation: `csmgr_` shares no substring with `squadeno-club-sports-manager`, so neither the plugin nor the prefix was ever offered for that plugin's options. A report that names a prefix is the user telling us which plugin owns a family of options -- first-hand evidence about this site. Keep it, and let it fill in the siblings: it outranks the slug guess, and it lets the prefix field offer a prefix the slug could never corroborate. Longest prefix wins, so a specific report beats a broader one, and transient wrappers are stripped so `_transient_csmgr_...` matches too. The popovers are built once when the table draws, so the ones already on the page are updated in place as well -- reporting an option is exactly when its siblings become knowable, and making the user reload to see it would waste what they just supplied. Inputs they have already typed into are left alone. This fills forms; it does not mark rows known. A user's own claim is good enough to prefill a field they can still correct, not to assert a verified origin, so the Source column still waits on the mapping.
Opening the form left the caret nowhere, so filling in the one field it asks about started with a click or a tab. Hand focus to it on open. The trigger's click runs before the browser shows the popover and focusing a hidden element does nothing, so the focus is deferred a frame and re-checks that the popover really opened. The field is selected rather than merely focused: a prefilled slug is a suggestion, so typing replaces it while editing it still works. Focus also opens the suggestion list, so the installed plugins are now visible on open instead of only after the user guesses that typing reveals them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related changes that let the recognized-plugins list grow without waiting for a plugin release, plus a way for users to help expand it.
1. Remote known-plugins list with daily refresh
https://option-optimizer-api.progressplanner.com/known-plugins.json, with the JSON bundled in the plugin as a fallback when no fresh copy is available.POSTthat sends the plugin version and WordPress version so the maintainers can keep anonymous usage statistics. No site identity is sent.aaa_option_optimizer_known_plugins_url, so installs can redirect or disable the refresh.Map_Plugin_To_Optionsreads through the newKnown_Pluginsloader and exposesis_known()so callers can tell a recognized source from an unknown one.2. "Report origin" for unknown options
aaa_option_optimizer_report_url, so installs can redirect or disable the feature.plugin_knownflag per option so the UI knows which rows get the Report action.Server side (separate repo)
option-optimizer-apiCloudflare worker. It servesknown-plugins.jsonfrom R2 and records each fetch (plugin + WordPress version, no site identity) to Analytics Engine. SeeProgressPlanner/aaa-option-optimizer-apibranchfilip/known-plugins-endpoint.External services / privacy
readme.txt's External services section discloses: the daily mapping fetch fromoption-optimizer-api.progressplanner.com(sends plugin + WP version, no site identity), the wp.org slug-verification call (api.wordpress.org, sends only the typed slug), and the report-submission endpoint.1.7.0(version bump itself lands in the release PR).Test plan
php bin/validate-known-plugins.phpexits 0 against current JSONMap_Plugin_To_Options::get_plugin_name()still resolves a known prefix (e.g.aioseo_*→ AIOSEO)POSTreaches the worker and a fresh mapping is cachedOutstanding before merge
.github/workflows/sync-known-plugins.yml— it still pushes the file to the wp.org SVN/assets/directory, which is exactly the hosting the WP.org team asked us to stop. The publish target is now the worker's R2 bucket, handled in the worker repo.known-plugins.json,wrangler deploy) so the endpoint is live before this ships. Until then, installs use the bundled fallback.Notes for reviewer
siteground_data_is claimed by bothsg-optimizerandsg-security. Treated as a warning, not an error.🤖 Generated with Claude Code