diff --git a/packages/php-wasm/node/src/test/php-request-handler.spec.ts b/packages/php-wasm/node/src/test/php-request-handler.spec.ts index 5710b5435e..3d372f67a9 100644 --- a/packages/php-wasm/node/src/test/php-request-handler.spec.ts +++ b/packages/php-wasm/node/src/test/php-request-handler.spec.ts @@ -765,3 +765,120 @@ describe('PHPRequestHandler – Loopback call', () => { expect(response.text).toEqual('Starting: Ran second.php! Done'); }); }); + +describe('PHPRequestHandler – Cookie strategy', () => { + it('should persist cookies internally when not defining a strategy', async () => { + const handler = new PHPRequestHandler({ + documentRoot: '/', + phpFactory: async () => + new PHP(await loadNodeRuntime(RecommendedPHPVersion)), + maxPhpInstances: 1, + }); + const php = await handler.getPrimaryPhp(); + + php.writeFile( + '/set-cookie.php', + ` { + const handler = new PHPRequestHandler({ + documentRoot: '/', + phpFactory: async () => + new PHP(await loadNodeRuntime(RecommendedPHPVersion)), + maxPhpInstances: 1, + cookieStrategy: 'internal-store', + }); + const php = await handler.getPrimaryPhp(); + + php.writeFile( + '/set-cookie.php', + ` { + const handler = new PHPRequestHandler({ + documentRoot: '/', + phpFactory: async () => + new PHP(await loadNodeRuntime(RecommendedPHPVersion)), + maxPhpInstances: 1, + cookieStrategy: 'pass-through', + }); + const php = await handler.getPrimaryPhp(); + + php.writeFile( + '/set-cookie.php', + ` FileNotFoundAction; +/** + * - `internal-store`: Persist cookies from reponses in an internal store and + * includes them in following requests. This behavior is useful in the Playground + * web app because it allows multiple sites to set cookies on the same domain + * without running into conflicts. Each site gets a separate "namespace". The + * downside is that all cookies are global and you cannot have two users + * simultaneously logged in. + * + * - `pass-through`: Typical server behavior. All cookies are passed back to the + * client via a HTTP response. This behavior is useful when Playground is running + * on the backend and can be requested by multiple browsers. This enables each + * browser to get its own cookies, which means two users may be simultaneously + * logged in to their accounts. + * + * Default value is `internal-store`. + */ +export type CookieStrategy = 'internal-store' | 'pass-through'; + interface BaseConfiguration { /** * The directory in the PHP filesystem where the server will look @@ -95,7 +113,9 @@ export type PHPRequestHandlerConfiguration = BaseConfiguration & */ maxPhpInstances?: number; } - ); + ) & { + cookieStrategy?: CookieStrategy; + }; /** * Handles HTTP requests using PHP runtime as a backend. @@ -159,7 +179,7 @@ export class PHPRequestHandler { #HOST: string; #PATHNAME: string; #ABSOLUTE_URL: string; - #cookieStore: HttpCookieStore; + #cookieStore?: HttpCookieStore; rewriteRules: RewriteRule[]; processManager: PHPProcessManager; getFileNotFoundAction: FileNotFoundGetActionCallback; @@ -198,7 +218,10 @@ export class PHPRequestHandler { maxPhpInstances: config.maxPhpInstances, }); } - this.#cookieStore = new HttpCookieStore(); + const cookieStrategy = config.cookieStrategy ?? 'internal-store'; + if (cookieStrategy === 'internal-store') { + this.#cookieStore = new HttpCookieStore(); + } this.#DOCROOT = documentRoot; const url = new URL(absoluteUrl); @@ -492,9 +515,12 @@ export class PHPRequestHandler { const headers: Record = { host: this.#HOST, ...normalizeHeaders(request.headers || {}), - cookie: this.#cookieStore.getCookieRequestHeader(), }; + if (this.#cookieStore) { + headers['cookie'] = this.#cookieStore.getCookieRequestHeader(); + } + let body = request.body; if (typeof body === 'object' && !(body instanceof Uint8Array)) { preferredMethod = 'POST'; @@ -522,7 +548,7 @@ export class PHPRequestHandler { scriptPath, headers, }); - this.#cookieStore.rememberCookiesFromResponseHeaders( + this.#cookieStore?.rememberCookiesFromResponseHeaders( response.headers ); return response; diff --git a/packages/playground/cli/src/cli.ts b/packages/playground/cli/src/cli.ts index 85084761fa..3eecbb1cf2 100644 --- a/packages/playground/cli/src/cli.ts +++ b/packages/playground/cli/src/cli.ts @@ -209,7 +209,6 @@ async function run() { php: args.php as SupportedPHPVersion, wp: args.wp, }, - login: args.login, }; } @@ -311,6 +310,7 @@ async function run() { } }, }, + cookieStrategy: 'pass-through', }); const php = await requestHandler.getPrimaryPhp(); @@ -348,6 +348,11 @@ async function run() { process.exit(0); } else { logger.log(`WordPress is running on ${absoluteUrl}`); + if (args.login) { + logger.log( + `➜ You can auto-login to the site using the query parameter "playground-auto-login=true"\n➜ Homepage: ${absoluteUrl}/?playground-auto-login=true\n➜ WP-Admin: ${absoluteUrl}/wp-admin/?playground-auto-login=true` + ); + } } }, async handleRequest(request: PHPRequest) { diff --git a/packages/playground/cli/src/server.ts b/packages/playground/cli/src/server.ts index 55e3b99376..8e7a7fcfdb 100644 --- a/packages/playground/cli/src/server.ts +++ b/packages/playground/cli/src/server.ts @@ -25,6 +25,35 @@ export async function startServer(options: ServerOptions) { }); }); + // Middleware to check if auto-login should be executed + app.use(async (req, res, next) => { + if (req.query['playground-auto-login'] === 'true') { + await options.handleRequest({ url: '/wp-login.php' }); + const response = await options.handleRequest({ + url: '/wp-login.php', + method: 'POST', + body: { + log: 'admin', + pwd: 'password', + rememberme: 'forever', + }, + }); + const cookies = response.headers['set-cookie']; + res.setHeader('set-cookie', cookies); + // Remove query parameter to avoid infinite loop + let redirectUrl = req.url.replace( + /&?playground-auto-login=true/, + '' + ); + // If no more query parameters, remove ? from URL + if (Object.keys(req.query).length === 1) { + redirectUrl = redirectUrl.substring(0, redirectUrl.length - 1); + } + return res.redirect(redirectUrl); + } + next(); + }); + app.use('/', async (req, res) => { const phpResponse = await options.handleRequest({ url: req.url, diff --git a/packages/playground/wordpress/src/boot.ts b/packages/playground/wordpress/src/boot.ts index 111a0cd75f..2ba7d06883 100644 --- a/packages/playground/wordpress/src/boot.ts +++ b/packages/playground/wordpress/src/boot.ts @@ -1,4 +1,5 @@ import { + CookieStrategy, FileNotFoundAction, FileNotFoundGetActionCallback, FileTree, @@ -91,6 +92,8 @@ export interface BootOptions { * given request URI. */ getFileNotFoundAction?: FileNotFoundGetActionCallback; + + cookieStrategy?: CookieStrategy; } /** @@ -179,6 +182,7 @@ export async function bootWordPress(options: BootOptions) { rewriteRules: wordPressRewriteRules, getFileNotFoundAction: options.getFileNotFoundAction ?? getFileNotFoundActionForWordPress, + cookieStrategy: options.cookieStrategy, }); const php = await requestHandler.getPrimaryPhp();