diff --git a/src/Auth/OAuth.php b/src/Auth/OAuth.php index afe21ca5..bcc5ca96 100644 --- a/src/Auth/OAuth.php +++ b/src/Auth/OAuth.php @@ -219,7 +219,10 @@ final class OAuth { 'name' => $provider, 'label' => OAuthProviderCatalog::label($provider), 'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '', - 'url' => esc_url($url), + // Raw, matching get_frontend_bindings_data(): every surface escapes + // once on output. esc_url() is idempotent on its own output, so + // escaping here too was redundant rather than broken. + 'url' => $url, ]; } return $result; diff --git a/tests/widget-system/auth-source-contract.test.cjs b/tests/widget-system/auth-source-contract.test.cjs index 0e915734..7da6f5d2 100644 --- a/tests/widget-system/auth-source-contract.test.cjs +++ b/tests/widget-system/auth-source-contract.test.cjs @@ -294,3 +294,22 @@ test('the top-nav user popover reproduces the reference quick-login block', () = assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/); assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/); }); + +test('provider URLs stay raw in the data layer and are escaped once per surface', () => { + const oauth = read('src/Auth/OAuth.php'); + const shared = read('templates/parts/auth-content.php'); + const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php'); + const comments = read('inc/helpers/aksrt-comments.php'); + + // get_auth_providers_data() hands out raw URLs, exactly like its sibling + // get_frontend_bindings_data() does. esc_url() is idempotent on its own + // output, so escaping here as well was redundant rather than broken. + assert.doesNotMatch(oauth, /'url'\s*=>\s*esc_url\(\$url\)/); + assert.match(oauth, /'url'\s*=>\s*\$url,/); + + // Every consumer still escapes on output, so the rendered href is unchanged. + assert.match(shared, /esc_url\(\(string\) \(\$provider\['url'\] \?\? '#'\)\)/); + assert.match(userRenderer, /href="' \. esc_url\(\$url\)/); + assert.match(oauth, /'" class="but hollow social-login-item ml10">/); +});