Fix AI generator callback URLs becoming stale after site URL change #23127#23128
Conversation
…23127) When a site's URL changes after AI generator tokens are initially requested (e.g., migrating from a staging URL to a production domain), the callback URLs registered with Yoast's API become permanently stale because token_refresh() never re-sends them. This adds self-healing URL mismatch detection: on each token retrieval, compare an md5 hash of the current callback URL against a stored hash from the last token_request(). If they differ (or no hash exists yet), stale tokens are deleted and a fresh token_request() re-registers the correct callback URLs. Uses an md5 hash rather than storing the URL directly so the value is immune to wp search-replace operations that would otherwise update the stored URL in lockstep with the site URL, masking the change.
|
Hey @chrisdavidmiles! Thanks for the PR, sounds like a reasonable solution! |
- Use Brain\Monkey stubs() for update_option in setUp to avoid enforcing
call counts on tests that never trigger token_request()
- Add default get_refresh_callback_url mock in setUp
- Remove get_callback_url/get_refresh_callback_url overrides from
Get_Or_Request_Access_Token_Test that conflicted with the setUp hash
default (different URL caused have_callback_urls_changed() to return
true unexpectedly, and once() constraint broke with the additional call)
- Replace expect('update_option') with when()->alias() capture pattern
in Callback_Url_Change_Test since stubs and expect are incompatible
for the same function in Brain\Monkey
- Remove unused $callback_hash variable to fix CS warning threshold
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@enricobattocchi Tests updated. |
|
Thanks @chrisdavidmiles! Just one question: in case there are 2+ users using AI generate, I think the fix would unblock only the first one... But maybe it's a good enough trade-off for a real-world case where just one user is working on a staging env before migration? |
Fixes multi-user scenario where only the first user to trigger the URL change detection would re-register callback URLs. Now each user independently detects the change via their own user meta hash and re-registers on their next "Generate with AI" click. Also improves multisite: since wp_usermeta is network-wide, users switching between sites automatically detect the different REST API endpoints and re-register with the correct callback URLs.
|
Thank you for catching that @enricobattocchi . Please review this new version that stores the hash per-user in user meta. Trying to think through more edge cases in advance, this new approach should also help multisite network sites (wp_usermeta is network-wide). |
|
@enricobattocchi I noticed |
|
Hi @chrisdavidmiles The user based changes look good! and we do still rely on this double code for now. So could you also apply your change there? Than I will review it one more time and merge it! |
Same fix as src/ai/authorization/ — both copies are active in the compiled DI container and register identical REST routes.
|
@thijsoo Updated, please review. |
|
Thank you! I will start the acceptance test tomorrow morning and than hopefully merge it tomorrow as well! |
thijsoo
left a comment
There was a problem hiding this comment.
CR looks good. I like the user specific meta solution. I checked the solution with only free and also with premium.
Context
When a WordPress site is provisioned at a temporary/staging URL (common with Bluehost, Kinsta, WP Engine, and many other managed hosts) and the user later points a real domain to the site, the AI Generator's "Generate with AI" feature fails silently. The
callback_urlandrefresh_callback_urlare sent to Yoast's API only duringtoken_request(), buttoken_refresh()never re-sends them. The API continues using the original (stale) staging URLs for all subsequent token delivery callbacks, which fail due to DNS/TLS mismatches.This is not host-specific — any site that changes its URL after first using the AI Generator is affected. Yoast SEO does hook into
update_option_home(viaIndexable_HomeUrl_Watcher), but this only fires when thehomeoption is updated through WordPress'supdate_option()API. If hosting providers change site URLs usingwp search-replaceor direct MySQL updates, those hooks do not fire, so Yoast never detects the change.See #23127 for full reproduction steps and technical analysis.
Summary
This PR can be summarized in the following changelog entry:
Relevant technical choices:
get_or_request_access_token(), the current callback URL is compared (via md5 hash) against a stored hash from the last successfultoken_request(). If they differ, stale tokens are deleted and a freshtoken_request()re-registers the correct URLs.wp search-replacewould update a stored URL in lockstep with the site URL, masking the change. A hash is immune to this because search-replace won't find the old domain string inside a hex digest.have_callback_urls_changed()returnstrue, forcing a one-time freshtoken_request(). This is an intentional tradeoff — it ensures all existing affected sites self-heal on their next "Generate with AI" click without any manual intervention (no SQL, no CLI, no user action). The cost is one extratoken_request()per user on their first use after the upgrade. Subsequent uses are unaffected because the hash is stored after the first successful request.get_option/update_option) instead ofOptions_Helpervia DI: The compiled DI container (src/generated/container.php) hardcodes theToken_Managerconstructor signature. Adding a new constructor parameter would require recompiling the container. Usingget_option/update_optiondirectly avoids this and keeps the change minimal. The Yoast team may prefer to refactor this to useOptions_Helperwith a recompiled container in a follow-up — the runtime behavior is identical either way.Test instructions
Test instructions for the acceptance test before the PR gets merged
This PR can be acceptance tested by following these steps:
wp option update home, orwp search-replace)To verify the self-healing on upgrade (simulating an existing affected site):
Relevant test scenarios
Test instructions for QA when the code is in the RC
Impact check
This PR affects the following parts of the plugin, which may require extra testing:
Token_Manager::get_or_request_access_token()now checks for URL changes before proceeding. This is the entry point for all AI Generator features (suggestions, usage).token_request()regardless of whether their URL actually changed. This is by design to catch existing stale sites but should be verified to not cause issues at scale.Other environments
[shopify-seo], added test instructions for Shopify and attached theShopifylabel to this PR.[yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached theGoogle Docs Add-onlabel to this PR.Documentation
Quality assurance
grunt build:imagesand commited the results, if my PR introduces new images or SVGs.Innovation
innovationlabel.Fixes #23127