Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make cookie store web-only #2110

Open
wants to merge 10 commits into
base: trunk
Choose a base branch
from
7 changes: 0 additions & 7 deletions packages/php-wasm/universal/src/lib/php-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
PHPProcessManager,
SpawnedPHP,
} from './php-process-manager';
import { HttpCookieStore } from './http-cookie-store';
import mimeTypes from './mime-types.json';

export type RewriteRule = {
Expand Down Expand Up @@ -159,7 +158,6 @@ export class PHPRequestHandler {
#HOST: string;
#PATHNAME: string;
#ABSOLUTE_URL: string;
#cookieStore: HttpCookieStore;
rewriteRules: RewriteRule[];
processManager: PHPProcessManager;
getFileNotFoundAction: FileNotFoundGetActionCallback;
Expand Down Expand Up @@ -198,7 +196,6 @@ export class PHPRequestHandler {
maxPhpInstances: config.maxPhpInstances,
});
}
this.#cookieStore = new HttpCookieStore();
this.#DOCROOT = documentRoot;

const url = new URL(absoluteUrl);
Expand Down Expand Up @@ -490,7 +487,6 @@ export class PHPRequestHandler {
const headers: Record<string, string> = {
host: this.#HOST,
...normalizeHeaders(request.headers || {}),
cookie: this.#cookieStore.getCookieRequestHeader(),
};

let body = request.body;
Expand Down Expand Up @@ -520,9 +516,6 @@ export class PHPRequestHandler {
scriptPath,
headers,
});
this.#cookieStore.rememberCookiesFromResponseHeaders(
response.headers
);
return response;
} catch (error) {
const executionError = error as PHPExecutionFailureError;
Expand Down
3 changes: 3 additions & 0 deletions packages/php-wasm/web-service-worker/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export async function convertFetchEventToPHPRequest(event: FetchEvent) {
'User-agent': self.navigator.userAgent,
'Content-type': contentType,
},
// Relay credentials mode so the browser-based PHP worker
// can manage its own cookie store.
credentials: event.request.credentials,
},
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ import { loadNodeRuntime } from '@php-wasm/node';
import { readFileSync } from 'fs';
import { join } from 'path';
import { login } from './login';
import { PHPRequest, PHPRequestHandler } from '@php-wasm/universal';
import {
HttpCookieStore,
PHPRequest,
PHPRequestHandler,
} from '@php-wasm/universal';

describe('Blueprint step enableMultisite', () => {
let handler: PHPRequestHandler;
let cookieStore: HttpCookieStore;
async function doBootWordPress(options: { absoluteUrl: string }) {
handler = await bootWordPress({
createPhpRuntime: async () =>
Expand All @@ -28,16 +33,23 @@ describe('Blueprint step enableMultisite', () => {
),
},
});
cookieStore = new HttpCookieStore();
const php = await handler.getPrimaryPhp();

return { php, handler };
}

const requestFollowRedirects = async (request: PHPRequest) => {
const requestFollowRedirectsWithCookies = async (request: PHPRequest) => {
let response = await handler.request(request);
while (response.httpStatusCode === 302) {
cookieStore.rememberCookiesFromResponseHeaders(response.headers);

const cookieHeader = cookieStore.getCookieRequestHeader();
response = await handler.request({
url: response.headers['location'][0],
headers: {
...(cookieHeader && { cookie: cookieHeader }),
},
});
}
return response;
Expand Down Expand Up @@ -81,7 +93,7 @@ describe('Blueprint step enableMultisite', () => {
* the admin bar includes the multisite menu.
*/
await login(php, {});
const response = await requestFollowRedirects({
const response = await requestFollowRedirectsWithCookies({
url: '/',
});
expect(response.httpStatusCode).toEqual(200);
Expand Down
20 changes: 14 additions & 6 deletions packages/playground/blueprints/src/lib/steps/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
getWordPressModule,
} from '@wp-playground/wordpress-builds';
import { login } from './login';
import { PHPRequestHandler } from '@php-wasm/universal';
import { PHPRequestHandler, HttpCookieStore } from '@php-wasm/universal';
import { bootWordPress } from '@wp-playground/wordpress';
import { loadNodeRuntime } from '@php-wasm/node';
import { defineWpConfigConsts } from './define-wp-config-consts';
Expand All @@ -14,6 +14,7 @@ import { joinPaths, phpVar } from '@php-wasm/util';
describe('Blueprint step login', () => {
let php: PHP;
let handler: PHPRequestHandler;
let cookieStore: HttpCookieStore;
beforeEach(async () => {
handler = await bootWordPress({
createPhpRuntime: async () =>
Expand All @@ -23,22 +24,29 @@ describe('Blueprint step login', () => {
wordPressZip: await getWordPressModule(),
sqliteIntegrationPluginZip: await getSqliteDatabaseModule(),
});
cookieStore = new HttpCookieStore();
php = await handler.getPrimaryPhp();
});

const requestFollowRedirects = async (request: PHPRequest) => {
const requestFollowRedirectsWithCookies = async (request: PHPRequest) => {
let response = await handler.request(request);
while (response.httpStatusCode === 302) {
cookieStore.rememberCookiesFromResponseHeaders(response.headers);

const cookieHeader = cookieStore.getCookieRequestHeader();
response = await handler.request({
url: response.headers['location'][0],
headers: {
...(cookieHeader && { cookie: cookieHeader }),
},
});
}
return response;
};

it('should log the user in', async () => {
await login(php, {});
const response = await requestFollowRedirects({
const response = await requestFollowRedirectsWithCookies({
url: '/',
});
expect(response.httpStatusCode).toBe(200);
Expand All @@ -47,7 +55,7 @@ describe('Blueprint step login', () => {

it('should log the user into wp-admin', async () => {
await login(php, {});
const response = await requestFollowRedirects({
const response = await requestFollowRedirectsWithCookies({
url: '/wp-admin/',
});
expect(response.httpStatusCode).toBe(200);
Expand All @@ -60,7 +68,7 @@ describe('Blueprint step login', () => {
PLAYGROUND_FORCE_AUTO_LOGIN_ENABLED: true,
},
});
const response = await requestFollowRedirects({
const response = await requestFollowRedirectsWithCookies({
url: '/?playground_force_auto_login_as_user=admin',
});
expect(response.httpStatusCode).toBe(200);
Expand All @@ -81,7 +89,7 @@ describe('Blueprint step login', () => {
}
`
);
const response = await requestFollowRedirects({
const response = await requestFollowRedirectsWithCookies({
url: '/nonce-test.php',
});
expect(response.text).toBe('1');
Expand Down
72 changes: 71 additions & 1 deletion packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d
/* @ts-ignore */
import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw';
import {
HttpCookieStore,
PHPRequest,
PHPResponse,
PHPWorker,
SupportedPHPVersion,
Expand Down Expand Up @@ -78,7 +80,15 @@ export type WorkerBootOptions = {
corsProxyUrl?: string;
};

/** @inheritDoc PHPClient */
export interface PHPRequestWithCredentialsMode extends PHPRequest {
/**
* The fetch credentials mode to use for the request.
* Default: 'same-origin'.
*/
credentials?: RequestCredentials;
}

/** @inheritDoc PHPWorker */
export class PlaygroundWorkerEndpoint extends PHPWorker {
booted = false;

Expand All @@ -99,6 +109,16 @@ export class PlaygroundWorkerEndpoint extends PHPWorker {

unmounts: Record<string, () => any> = {};

/**
* A cookie store to remember cookies between requests.
*
* Web browsers don't permit relaying `Set-Cookie` headers
* via Response objects so the browser can store cookies from
* PHP responses. So we need to remember cookies ourselves.
* Ref: https://fetch.spec.whatwg.org/#forbidden-response-header-name
*/
#cookieStore: HttpCookieStore = new HttpCookieStore();

constructor(monitor: EmscriptenDownloadMonitor) {
super(undefined, monitor);
}
Expand Down Expand Up @@ -448,6 +468,56 @@ export class PlaygroundWorkerEndpoint extends PHPWorker {
}
}

/** @inheritDoc @php-wasm/universal!PHPRequestHandler.request */
override async request(
request: PHPRequestWithCredentialsMode
): Promise<PHPResponse> {
const credentialsMode: RequestCredentials =
// Default to same-origin.
// https://fetch.spec.whatwg.org/#concept-request-credentials-mode
request.credentials ?? 'same-origin';
const credentialsAllowed =
credentialsMode === 'include' || credentialsMode === 'same-origin';

const incomingHeaders = request.headers ?? {};
const headers: Record<string, string> = {};
let incomingCookies = '';
for (const [name, value] of Object.entries(incomingHeaders)) {
if (name.toLowerCase() === 'cookie') {
incomingCookies = value;
} else {
headers[name] = value;
}
}

if (credentialsAllowed) {
const storedCookies = this.#cookieStore.getCookieRequestHeader();
const cookieSegments = [];
storedCookies && cookieSegments.push(storedCookies);
incomingCookies && cookieSegments.push(incomingCookies);
const cookieHeader = cookieSegments.join('; ');

if (cookieHeader) {
headers['cookie'] = cookieHeader;
}
}

const phpResponse = await super.request({
...request,
headers,
});

// Paraphrased from https://fetch.spec.whatwg.org/#http-network-fetch:
// > If `includeCredentials` is true, then apply set-cookie headers.
if (credentialsAllowed) {
this.#cookieStore.rememberCookiesFromResponseHeaders(
phpResponse.headers
);
}

return phpResponse;
}

// These methods are only here for the time traveling Playground demo.
// Let's consider removing them in the future.

Expand Down
Loading