@@ -28,13 +28,17 @@ import {
2828 CIAB_DASHBOARD_SECTION_DEFINITION ,
2929 CIAB_DASHBOARD_SECTION_PATHS ,
3030} from 'calypso/dashboard/app-ciab/section' ;
31- import { isAllowedDotcomDashboardHostname } from 'calypso/dashboard/app-dotcom/routing' ;
31+ import {
32+ buildDotcomDashboardLink ,
33+ isAllowedDotcomDashboardHostname ,
34+ } from 'calypso/dashboard/app-dotcom/routing' ;
3235import {
3336 DOTCOM_DASHBOARD_SECTION_DEFINITION ,
3437 DOTCOM_DASHBOARD_SECTION_PATHS ,
3538} from 'calypso/dashboard/app-dotcom/section' ;
3639import { A4A_SIGNUP_PATHS } from 'calypso/dashboard/section' ;
3740import isDashboardEnv from 'calypso/dashboard/utils/is-dashboard-env' ;
41+ import { wpcomLink } from 'calypso/dashboard/utils/link' ;
3842import wooDnaConfig from 'calypso/jetpack-connect/woo-dna-config' ;
3943import { STEPPER_SECTION_DEFINITION } from 'calypso/landing/stepper/section' ;
4044import { SUBSCRIPTIONS_SECTION_DEFINITION } from 'calypso/landing/subscriptions/section' ;
@@ -1212,6 +1216,62 @@ function wpcomPages( app ) {
12121216 } ) ;
12131217}
12141218
1219+ /**
1220+ * Resolve the counterpart origin's `/logout` URL for the cross-origin data clear.
1221+ *
1222+ * `Clear-Site-Data` for "storage"/"cache" is scoped to the origin that returns
1223+ * it, and the classic Calypso app and the Dashboard are served from separate
1224+ * origins (e.g. wordpress.com vs. my.wordpress.com). To clear both, `/logout`
1225+ * embeds the counterpart origin's `/logout?embed=1` in a hidden iframe. Returns
1226+ * `null` when there is no distinct counterpart (e.g. single-origin calypso.live).
1227+ */
1228+ function getCounterpartLogoutUrl ( req ) {
1229+ if ( req . hostname . endsWith ( '.calypso.live' ) ) {
1230+ return null ;
1231+ }
1232+
1233+ if ( isAllowedDotcomDashboardHostname ( req . hostname ) ) {
1234+ // On the Dashboard host, the classic Calypso origin is the counterpart.
1235+ // `wpcomLink` resolves it from the `wpcom_url` config (wordpress.com in
1236+ // production, the local Calypso dev server in development).
1237+ return wpcomLink ( '/logout?embed=1' ) ;
1238+ }
1239+
1240+ if ( [ 'wordpress.com' , 'calypso.localhost' ] . includes ( req . hostname ) ) {
1241+ return buildDotcomDashboardLink ( '/logout?embed=1' ) ;
1242+ }
1243+
1244+ return null ;
1245+ }
1246+
1247+ /**
1248+ * Resolve the destination `/logout` sends the user to once data is cleared.
1249+ *
1250+ * Honors a `redirect_to` query param, but only for same-origin destinations —
1251+ * returned as a relative path so the client-side redirect can never leave this
1252+ * origin (guards against open redirects). Anything else falls back to `/log-in`.
1253+ */
1254+ function getLogoutRedirectTo ( req ) {
1255+ const target = req . query . redirect_to ;
1256+ if ( typeof target !== 'string' || ! target ) {
1257+ return '/log-in' ;
1258+ }
1259+
1260+ const host = req . get ( 'host' ) ;
1261+ try {
1262+ const resolved = new URL ( target , `https://${ host } ` ) ;
1263+ const destination = resolved . pathname + resolved . search + resolved . hash ;
1264+ // Reject cross-origin and protocol-relative (`//host`) destinations.
1265+ if ( resolved . host === host && ! destination . startsWith ( '//' ) ) {
1266+ return destination ;
1267+ }
1268+ } catch {
1269+ // Fall through to the default.
1270+ }
1271+
1272+ return '/log-in' ;
1273+ }
1274+
12151275export default function pages ( ) {
12161276 const app = express ( ) ;
12171277
@@ -1224,6 +1284,30 @@ export default function pages() {
12241284 app . use ( setupLoggedInContext ) ;
12251285 app . use ( middlewareUnsupportedBrowser ( ) ) ;
12261286
1287+ // `/logout` clears browser-stored site data for the current origin via the
1288+ // `Clear-Site-Data` response header, then best-effort clears the counterpart
1289+ // origin (classic Calypso <-> Dashboard) through a hidden iframe before
1290+ // redirecting to the login page. Actual session invalidation is handled by the
1291+ // client logout flow. Registered before section routing so it responds on both
1292+ // the classic Calypso and Dashboard hostnames.
1293+ app . get ( '/logout' , ( req , res ) => {
1294+ res . set ( 'Clear-Site-Data' , '"storage", "cache", "cookies"' ) ;
1295+
1296+ if ( req . query . embed ) {
1297+ // Loaded inside the counterpart origin's iframe: this response only needs
1298+ // to carry the header, so render an empty page and stop here.
1299+ res . send ( renderJsx ( 'logout' , { embed : true } ) ) ;
1300+ return ;
1301+ }
1302+
1303+ res . send (
1304+ renderJsx ( 'logout' , {
1305+ iframeSrc : getCounterpartLogoutUrl ( req ) ,
1306+ redirectTo : getLogoutRedirectTo ( req ) ,
1307+ } )
1308+ ) ;
1309+ } ) ;
1310+
12271311 if ( ! ( isJetpackCloud ( ) || isA8CForAgencies ( ) || isDashboardEnv ( ) ) ) {
12281312 wpcomPages ( app ) ;
12291313 }
0 commit comments