Skip to content

Make cookie store web-only #2110

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

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

const url = new URL(absoluteUrl);
@@ -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;
@@ -520,9 +516,6 @@ export class PHPRequestHandler {
scriptPath,
headers,
});
this.#cookieStore.rememberCookiesFromResponseHeaders(
response.headers
);
return response;
} catch (error) {
const executionError = error as PHPExecutionFailureError;
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
@@ -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,
},
],
};
Original file line number Diff line number Diff line change
@@ -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 () =>
@@ -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;
@@ -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);
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
@@ -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';
@@ -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 () =>
@@ -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);
@@ -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);
@@ -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);
@@ -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');
72 changes: 71 additions & 1 deletion packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
@@ -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,
@@ -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;

@@ -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);
}
@@ -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.