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

Populate php://input and other streams – JSON POST requests are broken #103

Closed
adamziel opened this issue Jan 4, 2023 · 2 comments
Closed

Comments

@adamziel
Copy link
Collaborator

adamziel commented Jan 4, 2023

In PHP, parsing JSON data from POST is often handled with file_get_contents( 'php://input' );. That stream is read-only and can't be pre-populated by a PHP code in php-server.ts. Instead, it must happen somewhere in the C code in php_wasm.c.

For example, submitting a POST request with {"foo": "bar"} as body and a content-type: application/json should yield bar in the following code:

$raw_request_body = file_get_contents('php://input');
echo json_decode($raw_request_body)['foo'];

At the moment, it does not.

@adamziel
Copy link
Collaborator Author

adamziel commented Jan 4, 2023

The trick is:

int EMSCRIPTEN_KEEPALIVE phpwasm_init_context()
{
	putenv("USE_ZEND_ALLOC=0");

	SG(request_info).request_method = "POST";
	SG(request_info).content_type = "application/json";
	php_embed_module.read_post = *php__wasm_read_post;
	return php_embed_init(0, NULL);
}

static int php__wasm_read_post(char *buffer, uint count_bytes)
{
	sprintf (buffer, "{\"test\": \"foo\"}");
	return 15;
}

adamziel added a commit that referenced this issue Jan 12, 2023
# What is this PR all about?

Introduces a PHP SAPI module that enables setting `$_POST`, `$_SERVER`, `php://input` and all the other PHP values from JavaScript.

## What problem does it solve?

Before this PR, most superglobal values were set by prepending code snippets like `$_SERVER['DOCUMENT_ROOT'] = ${JSON.stringify(documentRoot)};` every time some code was evaluated. Unfortunately, that technique couldn't populate everything, e.g. `php://input` remained empty.

## How does it work?

PHP SAPI is used to integrate PHP with webservers and runtimes. A few SAPIs you might be familiar with are `php-cgi`, `php-fpm`, and `php-cli`. A SAPI consumes the request information from the runtime, passes it to PHP, triggers the code execution, and passes the response back to the runtime.

This PR introduces a WASM SAPI that accepts input information from JavaScript, sets up a PHP request, and passes a response back to JS. The most important changes are in the `php_wasm.c` file. The rest of the PR is adjusting the existing codebase to the new way of working with PHP.

Briefly speaking, the SAPI module exposes a few setters like `wasm_set_query_string` or `wasm_add_SERVER_entry` and a `wasm_sapi_handle_request()` function that triggers the request execution. The output information are written to `/tmp/stdout`, `/tmp/stderr`, and `/tmp/headers.json` by the C module and read by the PHP JavaScript class.

Because the request body and the query string are parsed by the same PHP internal functions as they would on a webserver, array syntax like `settings[newsletter]=1` is handled correctly.

One surprising thing is the ability to set arbitrary `$_FILES` entries with `wasm_add_uploaded_file`. This is because JavaScript typically has access to any uploaded `File` objects and it would be wasteful to re-serialize them only so that PHP can parse them all over again. With `wasm_add_uploaded_file` you can first write the uploaded files to the filesystem and then simply let PHP know about their existence.

Solves #103
@adamziel
Copy link
Collaborator Author

As of #107, php://input is populated with the request body.

High-level API:

const response = php.run({
	method: 'POST',
	body: '{"foo": "bar"}',
	code: `<?php echo file_get_contents('php://input');`,
});
const bodyText = new TextDecoder().decode(response.body);
expect(bodyText).toEqual('{"foo": "bar"}');

Low-level API:

this.#Runtime.ccall('wasm_set_request_body', null, [STR], ['{"test": "foo"}']);
this.#Runtime.ccall('wasm_set_request_method', null, [STR], ['POST']);
this.#Runtime.ccall('wasm_set_content_type', null, [STR], ["application/x-www-form-urlencoded"]);
this.#Runtime.ccall('wasm_set_php_code', null, [STR], ['<?php echo file_get_contents("php://input"); ']);
this.#Runtime.ccall('wasm_sapi_handle_request', NUM, [], []);
console.log(this.readFileAsBuffer('/tmp/stdout'));
// {"test": "foo"}

Pookie717 added a commit to Pookie717/wordpress-playground that referenced this issue Oct 1, 2023
# What is this PR all about?

Introduces a PHP SAPI module that enables setting `$_POST`, `$_SERVER`, `php://input` and all the other PHP values from JavaScript.

## What problem does it solve?

Before this PR, most superglobal values were set by prepending code snippets like `$_SERVER['DOCUMENT_ROOT'] = ${JSON.stringify(documentRoot)};` every time some code was evaluated. Unfortunately, that technique couldn't populate everything, e.g. `php://input` remained empty.

## How does it work?

PHP SAPI is used to integrate PHP with webservers and runtimes. A few SAPIs you might be familiar with are `php-cgi`, `php-fpm`, and `php-cli`. A SAPI consumes the request information from the runtime, passes it to PHP, triggers the code execution, and passes the response back to the runtime.

This PR introduces a WASM SAPI that accepts input information from JavaScript, sets up a PHP request, and passes a response back to JS. The most important changes are in the `php_wasm.c` file. The rest of the PR is adjusting the existing codebase to the new way of working with PHP.

Briefly speaking, the SAPI module exposes a few setters like `wasm_set_query_string` or `wasm_add_SERVER_entry` and a `wasm_sapi_handle_request()` function that triggers the request execution. The output information are written to `/tmp/stdout`, `/tmp/stderr`, and `/tmp/headers.json` by the C module and read by the PHP JavaScript class.

Because the request body and the query string are parsed by the same PHP internal functions as they would on a webserver, array syntax like `settings[newsletter]=1` is handled correctly.

One surprising thing is the ability to set arbitrary `$_FILES` entries with `wasm_add_uploaded_file`. This is because JavaScript typically has access to any uploaded `File` objects and it would be wasteful to re-serialize them only so that PHP can parse them all over again. With `wasm_add_uploaded_file` you can first write the uploaded files to the filesystem and then simply let PHP know about their existence.

Solves WordPress/wordpress-playground#103
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant