Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist-web/php-web.js

Large diffs are not rendered by default.

Binary file modified dist-web/php-web.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion dist-web/php-webworker.js

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/shared/php-wrapper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,35 @@ export default class PHPWrapper {
const PHPModule = Object.assign({}, defaults, args);
await new PhpBinary(PHPModule);

this.mkdirTree = PHPModule.FS.mkdirTree;
this.readFile = PHPModule.FS.readFile;
this.writeFile = PHPModule.FS.writeFile;
this.unlink = PHPModule.FS.unlink;
this.pathExists = path => {
try {
PHPModule.FS.lookupPath(path);
return true;
} catch(e) {
return false;
}
};
this.call = PHPModule.ccall;
await this.call("pib_init", NUM, [STR], []);
return PHPModule;
}

initUploadedFilesHash() {
this.call("pib_init_uploaded_files_hash", null, [], []);
}

registerUploadedFile(tmpPath) {
this.call("pib_register_uploaded_file", null, [STR], [tmpPath]);
}

destroyUploadedFilesHash() {
this.call("pib_destroy_uploaded_files_hash", null, [], []);
}

async run(code) {
if (!this.call) {
throw new Error(`Run init() first!`);
Expand Down
181 changes: 174 additions & 7 deletions src/shared/wordpress.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { getPathQueryFragment, removeURLScope } from "../web/library.js";

if (typeof XMLHttpRequest === 'undefined') {
// Polyfill missing node.js features
import('xmlhttprequest').then(({ XMLHttpRequest }) => {
Expand Down Expand Up @@ -56,12 +58,133 @@ export default class WordPress {
throw new Error('call init() first');
}

const output = await this.php.run(`<?php
${this._setupErrorReportingCode()}
${this._setupRequestCode(request)}
${this._runWordPressCode(request.path)}
`);
return this.parseResponse(output);
const unscopedPath = getPathQueryFragment(
removeURLScope(new URL(request.path, this.ABSOLUTE_URL))
);

// @TODO: Don't assume the uploads only live in /wp-content/uploads
const isStaticFileRequest = unscopedPath.startsWith('/wp-content/uploads');

if(isStaticFileRequest) {
return await this.serveStaticFile(unscopedPath);
} else {
return await this.dispatchToPHP(request);
}
}

async serveStaticFile(requestedPath) {
const fsPath = `${this.DOCROOT}${requestedPath}`;

if(!this.php.pathExists(fsPath)){
return {
body: '404 File not found',
headers: {},
statusCode: 404,
exitCode: 0,
rawError: '',
}
}
const arrayBuffer = this.php.readFile(fsPath);
return {
body: arrayBuffer,
headers: {
'Content-length': arrayBuffer.byteLength,
// @TODO: Infer the content-type from the arrayBuffer instead of the file path.
// The code below won't return the correct mime-type if the extension
// was tampered with.
'Content-type': inferMimeType(fsPath),
'Accept-Ranges': 'bytes',
'Cache-Control': 'public, max-age=0'
},
exitCode: 0,
rawError: '',
};
}

async dispatchToPHP( request) {
const _FILES = await this.prepare_FILES(request.files);
try {
const output = await this.php.run( `<?php
${ this._setupErrorReportingCode() }
${ this._setupRequestCode( {
...request,
_FILES
} ) }
${ this._runWordPressCode( request.path ) }
` );

return this.parseResponse( output );
} finally {
this.cleanup_FILES( _FILES );
}
}

/**
* Prepares an object like { file1_name: File, ... } for
* being processed as $_FILES in PHP.
*
* In particular:
* * Creates the files in the filesystem
* * Allocates a global PHP rfc1867_uploaded_files HashTable
* * Registers the files in PHP's rfc1867_uploaded_files
* * Converts the JavaScript files object to the $_FILES data format like below
*
* Array(
* [file1_name] => Array (
* [name] => file_name.jpg
* [type] => text/plain
* [tmp_name] => /tmp/php/php1h4j1o (some path in the filesystem where the tmp file is kept for processing)
* [error] => UPLOAD_ERR_OK (= 0)
* [size] => 123 (the size in bytes)
* )
* // ...
* )
*
* @param {Object} files JavaScript files keyed by their HTTP upload name.
* @return $_FILES-compatible object.
*/
async prepare_FILES(files = {}) {
if(Object.keys(files).length) {
this.php.initUploadedFilesHash();
}

const _FILES = {};
for (const [key, value] of Object.entries(files)) {
const tmpName = Math.random().toFixed(20);
const tmpPath = `/tmp/${tmpName}`;
// Need to read the blob and store it in the filesystem
this.php.writeFile(
tmpPath,
new Uint8Array(await value.arrayBuffer())
);
_FILES[key] = {
name: value.name,
type: value.type,
tmp_name: tmpPath,
error: 0,
size: value.size,
};
this.php.registerUploadedFile(tmpPath);
}
return _FILES;
}

/**
* Cleans up after prepare_FILES:
* * Frees the PHP's rfc1867_uploaded_files HashTable
* * Removes the temporary files from the filesystem
*
* @param _FILES $_FILES-compatible object.
*/
cleanup_FILES(_FILES={}) {
if(Object.keys(_FILES).length) {
this.php.destroyUploadedFilesHash();
}
for (const [, value] of Object.entries(_FILES)) {
if(this.php.pathExists(value.tmp_name)){
this.php.unlink(value.tmp_name);
}
}
}

parseResponse(result) {
Expand Down Expand Up @@ -271,6 +394,7 @@ ADMIN;
headers,
_GET = '',
_POST = {},
_FILES = {},
_COOKIE = {},
_SESSION = {},
} = {}) {
Expand All @@ -280,10 +404,10 @@ ADMIN;
headers,
_GET,
_POST,
_FILES,
_COOKIE,
_SESSION,
};

console.log('Incoming request: ', request.path);

const https = this.ABSOLUTE_URL.startsWith('https://') ? 'on' : '';
Expand All @@ -301,6 +425,7 @@ REQUEST,
parse_str(substr($request->_GET, 1), $_GET);

$_POST = $request->_POST;
$_FILES = $request->_FILES;

if ( !is_null($request->_COOKIE) ) {
foreach ($request->_COOKIE as $key => $value) {
Expand Down Expand Up @@ -382,3 +507,45 @@ REQUEST,
`;
}
}

function inferMimeType(path) {
const extension = path.split('.').pop();
switch (extension) {
case 'css':
return 'text/css';
case 'js':
return 'application/javascript';
case 'png':
return 'image/png';
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'gif':
return 'image/gif';
case 'svg':
return 'image/svg+xml';
case 'woff':
return 'font/woff';
case 'woff2':
return 'font/woff2';
case 'ttf':
return 'font/ttf';
case 'otf':
return 'font/otf';
case 'eot':
return 'font/eot';
case 'ico':
return 'image/x-icon';
case 'html':
return 'text/html';
case 'json':
return 'application/json';
case 'xml':
return 'application/xml';
case 'txt':
case 'md':
return 'text/plain';
default:
return 'application-octet-stream';
}
}
27 changes: 18 additions & 9 deletions src/web/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ self.addEventListener('fetch', (event) => {

const isPHPRequest =
unscopedUrl.pathname.endsWith('/') ||
unscopedUrl.pathname.endsWith('.php');

unscopedUrl.pathname.endsWith('.php') ||
// @TODO: Don't assume the uploads only live in /wp-content/uploads
unscopedUrl.pathname.startsWith('/wp-content/uploads');
if (isPHPRequest) {
event.preventDefault();
return event.respondWith(
Expand All @@ -60,20 +61,22 @@ self.addEventListener('fetch', (event) => {
)}`
);
console.log({ isWpOrgRequest, isPHPRequest });
const post = await parsePost(event.request);
const { post, files } = await parsePost(event.request);
const requestHeaders = {};
for (const pair of event.request.headers.entries()) {
requestHeaders[pair[0]] = pair[1];
}

const requestedPath = getPathQueryFragment(url);
let wpResponse;
try {
const message = {
type: 'httpRequest',
scope,
request: {
path: getPathQueryFragment(url),
path: requestedPath,
method: event.request.method,
files,
_POST: post,
headers: requestHeaders,
},
Expand All @@ -96,7 +99,7 @@ self.addEventListener('fetch', (event) => {
}
);
} catch (e) {
console.error(e);
console.error(e, { requestedPath });
throw e;
}

Expand Down Expand Up @@ -166,20 +169,26 @@ async function cloneRequest(request, overrides) {

async function parsePost(request) {
if (request.method !== 'POST') {
return undefined;
return { post: undefined, files: undefined };
}
// Try to parse the body as form data
try {
const formData = await request.clone().formData();
const post = {};
const files = {};

for (const key of formData.keys()) {
post[key] = formData.get(key);
const value = formData.get(key);
if (value instanceof File) {
files[key] = value;
} else {
post[key] = value;
}
}

return post;
return { post, files };
} catch (e) {}

// Try to parse the body as JSON
return await request.clone().json();
return { post: await request.clone().json(), files: {} };
}
38 changes: 31 additions & 7 deletions wasm-build/php/docker-build-files/pib_eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include "zend_globals_macros.h"
#include "zend_exceptions.h"
#include "zend_closures.h"
#include "zend_hash.h"
#include "rfc1867.h"
#include "SAPI.h"

#include "sqlite3.h"
#include "sqlite3.c"
Expand Down Expand Up @@ -78,13 +81,6 @@ int EMSCRIPTEN_KEEPALIVE pib_run(char *code)
return retVal;
}

char *pib_tokenize(char *code)
{
// tokenize_parse(zval zend_string)

return "";
}

void EMSCRIPTEN_KEEPALIVE pib_destroy()
{
return php_embed_shutdown();
Expand All @@ -97,6 +93,34 @@ int EMSCRIPTEN_KEEPALIVE pib_refresh()
return pib_init();
}

// <FILE UPLOADS SUPPORT>
static void free_filename(zval *el) {
zend_string *filename = Z_STR_P(el);
zend_string_release_ex(filename, 0);
}

void EMSCRIPTEN_KEEPALIVE pib_init_uploaded_files_hash()
{
zend_hash_init(&PG(rfc1867_protected_variables), 8, NULL, NULL, 0);

HashTable *uploaded_files = NULL;
ALLOC_HASHTABLE(uploaded_files);
zend_hash_init(uploaded_files, 8, NULL, free_filename, 0);
SG(rfc1867_uploaded_files) = uploaded_files;
}

void EMSCRIPTEN_KEEPALIVE pib_register_uploaded_file(char *tmp_path_char)
{
zend_string *tmp_path = zend_string_init(tmp_path_char, strlen(tmp_path_char), 1);
zend_hash_add_ptr(SG(rfc1867_uploaded_files), tmp_path, tmp_path);
}

void EMSCRIPTEN_KEEPALIVE pib_destroy_uploaded_files_hash()
{
destroy_uploaded_files_hash();
}
// </FILE UPLOADS SUPPORT>

#ifdef WITH_VRZNO
#include "../php-src/ext/vrzno/php_vrzno.h"

Expand Down
Loading