Skip to content

Commit 6f0d97b

Browse files
authored
Merge pull request #23128 from chrisdavidmiles/fix/ai-generator-stale-callback-urls-after-domain-change
Fix AI generator callback URLs becoming stale after site URL change #23127
2 parents 99c486a + 9724ffd commit 6f0d97b

5 files changed

Lines changed: 433 additions & 65 deletions

File tree

src/ai-authorization/application/token-manager.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,23 @@ public function token_request( WP_User $user ): void {
202202
$code_verifier = $this->code_verifier->generate( $user->user_email );
203203
$this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
204204

205+
$callback_url = $this->urls->get_callback_url();
206+
$refresh_callback_url = $this->urls->get_refresh_callback_url();
207+
205208
$request_body = [
206209
'service' => 'openai',
207210
'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ),
208211
'license_site_url' => WPSEO_Utils::get_home_url(),
209212
'user_id' => (string) $user->ID,
210-
'callback_url' => $this->urls->get_callback_url(),
211-
'refresh_callback_url' => $this->urls->get_refresh_callback_url(),
213+
'callback_url' => $callback_url,
214+
'refresh_callback_url' => $refresh_callback_url,
212215
];
213216

214217
$this->request_handler->handle( new Request( '/token/request', $request_body ) );
215218

219+
// Store a per-user hash of the callback URL to detect future site URL changes.
220+
$this->user_helper->update_meta( $user->ID, '_yoast_wpseo_ai_generator_callback_url_hash', \md5( $callback_url ) );
221+
216222
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
217223
\wp_cache_delete( $user->ID, 'user_meta' );
218224
}
@@ -306,6 +312,12 @@ public function has_token_expired( string $jwt ): bool {
306312
* @throws RuntimeException Unable to retrieve the access or refresh token.
307313
*/
308314
public function get_or_request_access_token( WP_User $user ): string {
315+
// If the site URL has changed since callback URLs were registered, delete stale tokens.
316+
if ( $this->have_callback_urls_changed( $user ) ) {
317+
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
318+
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
319+
}
320+
309321
$access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
310322
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
311323
$this->token_request( $user );
@@ -330,4 +342,30 @@ public function get_or_request_access_token( WP_User $user ): string {
330342
}
331343

332344
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
345+
346+
/**
347+
* Checks whether the callback URLs have changed since the last token request.
348+
*
349+
* Detects site URL changes (e.g., migrating from a staging URL to a production domain)
350+
* that would leave stale callback URLs registered with the Yoast AI service.
351+
* Uses a per-user hash so each user independently detects the change and re-registers.
352+
* The hash is immune to wp search-replace operations.
353+
*
354+
* When no hash is stored (first run after upgrade), returns true to force a fresh
355+
* token_request(). This ensures existing sites with stale callback URLs self-heal
356+
* without manual intervention.
357+
*
358+
* @param WP_User $user The current user.
359+
*
360+
* @return bool Whether the callback URLs may have changed.
361+
*/
362+
private function have_callback_urls_changed( WP_User $user ): bool {
363+
$registered_hash = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_callback_url_hash', true );
364+
365+
if ( ! \is_string( $registered_hash ) || $registered_hash === '' ) {
366+
return true;
367+
}
368+
369+
return $registered_hash !== \md5( $this->urls->get_callback_url() );
370+
}
333371
}

src/ai/authorization/application/token-manager.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,23 @@ public function token_request( WP_User $user ): void {
204204
$code_verifier = $this->code_verifier->generate( $user->user_email );
205205
$this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
206206

207+
$callback_url = $this->urls->get_callback_url();
208+
$refresh_callback_url = $this->urls->get_refresh_callback_url();
209+
207210
$request_body = [
208211
'service' => 'openai',
209212
'code_challenge' => \hash( 'sha256', $code_verifier->get_code() ),
210213
'license_site_url' => WPSEO_Utils::get_home_url(),
211214
'user_id' => (string) $user->ID,
212-
'callback_url' => $this->urls->get_callback_url(),
213-
'refresh_callback_url' => $this->urls->get_refresh_callback_url(),
215+
'callback_url' => $callback_url,
216+
'refresh_callback_url' => $refresh_callback_url,
214217
];
215218

216219
$this->request_handler->handle( new Request( '/token/request', $request_body ) );
217220

221+
// Store a per-user hash of the callback URL to detect future site URL changes.
222+
$this->user_helper->update_meta( $user->ID, '_yoast_wpseo_ai_generator_callback_url_hash', \md5( $callback_url ) );
223+
218224
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
219225
\wp_cache_delete( $user->ID, 'user_meta' );
220226
}
@@ -308,6 +314,12 @@ public function has_token_expired( string $jwt ): bool {
308314
* @throws RuntimeException Unable to retrieve the access or refresh token.
309315
*/
310316
public function get_or_request_access_token( WP_User $user ): string {
317+
// If the site URL has changed since callback URLs were registered, delete stale tokens.
318+
if ( $this->have_callback_urls_changed( $user ) ) {
319+
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
320+
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
321+
}
322+
311323
$access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
312324
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
313325
$this->token_request( $user );
@@ -332,4 +344,30 @@ public function get_or_request_access_token( WP_User $user ): string {
332344
}
333345

334346
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
347+
348+
/**
349+
* Checks whether the callback URLs have changed since the last token request.
350+
*
351+
* Detects site URL changes (e.g., migrating from a staging URL to a production domain)
352+
* that would leave stale callback URLs registered with the Yoast AI service.
353+
* Uses a per-user hash so each user independently detects the change and re-registers.
354+
* The hash is immune to wp search-replace operations.
355+
*
356+
* When no hash is stored (first run after upgrade), returns true to force a fresh
357+
* token_request(). This ensures existing sites with stale callback URLs self-heal
358+
* without manual intervention.
359+
*
360+
* @param WP_User $user The current user.
361+
*
362+
* @return bool Whether the callback URLs may have changed.
363+
*/
364+
private function have_callback_urls_changed( WP_User $user ): bool {
365+
$registered_hash = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_callback_url_hash', true );
366+
367+
if ( ! \is_string( $registered_hash ) || $registered_hash === '' ) {
368+
return true;
369+
}
370+
371+
return $registered_hash !== \md5( $this->urls->get_callback_url() );
372+
}
335373
}

tests/Unit/AI/Authorization/Application/Token_Manager/Abstract_Token_Manager_Test.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ protected function setUp(): void {
113113
$this->urls = Mockery::mock( WordPress_URLs::class );
114114
$this->url_helper = Mockery::mock( Url_Helper::class );
115115

116+
// Default: have_callback_urls_changed() returns false by providing a matching per-user hash.
117+
$default_callback_url = 'https://example.com/wp-json/yoast/v1/ai_generator/callback';
118+
$this->user_helper->shouldReceive( 'get_meta' )
119+
->with( Mockery::any(), '_yoast_wpseo_ai_generator_callback_url_hash', true )
120+
->andReturn( \md5( $default_callback_url ) )
121+
->byDefault();
122+
$this->user_helper->shouldReceive( 'update_meta' )->zeroOrMoreTimes()->byDefault();
123+
$this->urls->shouldReceive( 'get_callback_url' )->andReturn( $default_callback_url )->byDefault();
124+
$this->urls->shouldReceive( 'get_refresh_callback_url' )->andReturn( 'https://example.com/wp-json/yoast/v1/ai_generator/refresh_callback' )->byDefault();
125+
116126
$this->instance = new Token_Manager( $this->access_token_repository, $this->code_verifier, $this->consent_handler, $this->refresh_token_repository, $this->user_helper, $this->request_handler, $this->code_verifier_repository, $this->urls );
117127
}
118128

0 commit comments

Comments
 (0)