Skip to content

Commit

Permalink
Make wordpress.html configurable via query params
Browse files Browse the repository at this point in the history
Read startup configuration from the query params in wordpress.html.

Supported params:

mode (string): `seamless` or `browser` – whether to make the WordPress iframe fill the entire available space, or display the fake browser UI.

url (string) – the initial page URL to open, e.g. /wp-login.php

plugin (string) – the plugin to and preinstall. It must be a name of the zip file that’s available in the `https://downloads.wordpress.org/plugin/` directory, e.g. `gutenberg.14.3.1.zip`.

This commit also ships a JS and PHP versions of a minimalistic HTTP proxy to make plugins downloadable from the same domain. Unfortunately the downloads.wordpress.org URLs can’t be requested via XHR or fetch() due to cross-origin limitations. Downloading the plugins is taken into account when rendering the progress bar.

Examples:

* `wordpress.html?mode=seamless` – render WordPress.wasm in the seamless mode and fill the entire page
* `wordpress.html?mode=browser&plugin=gutenberg.14.3.1.zip` – render the fake browser UI and preinstall the Gutenberg plugin before rendering the page
  • Loading branch information
adamziel committed Oct 20, 2022
1 parent 511cfe2 commit 228ccc3
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 349 deletions.
19 changes: 14 additions & 5 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ if (argv.platform === 'browser') {
},
};

function copyToDist(path) {
const filename = path.split('/').pop();
const outPath = `${options.outdir}/${filename}`;
fs.copyFileSync(path, outPath);
return outPath;
}
function buildHTMLFile(path) {
let content = fs.readFileSync(path).toString();
content = content.replace(
Expand All @@ -112,16 +118,19 @@ if (argv.platform === 'browser') {
return outPath;
}

function logBuiltFile(outPath) {
const outPathToLog = outPath.replace(/^\.\//, '');
console.log(` ${outPathToLog}`)
}

if (options.watch) {
chokidar.watch('src/web/*.html').on('change', buildHTMLFile);
chokidar.watch('src/web/*.php').on('change', copyToDist);
} else {
console.log("HTML files built: ")
console.log("")
for (const file of glob.sync('src/web/*.html')) {
const outPath = buildHTMLFile(file);
const outPathToLog = outPath.replace(/^\.\//, '');
console.log(` ${outPathToLog}`)
}
glob.sync('src/web/*.html').map(buildHTMLFile).forEach(logBuiltFile);
glob.sync('src/web/*.php').map(copyToDist).forEach(logBuiltFile);
console.log("")
console.log("Esbuild output: ")
}
Expand Down
3 changes: 2 additions & 1 deletion dist-web/.htaccess
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
AddType application/wasm .wasm

RewriteEngine on
RewriteRule ^scope:.*?/(.*)$ $1
RewriteRule ^scope:.*?/(.*)$ $1 [NC]
RewriteRule ^plugin-proxy$ plugin-proxy.php [NC]

<FilesMatch "iframe-worker.html$">
Header set Origin-Agent-Cluster: ?1
Expand Down
48 changes: 48 additions & 0 deletions dist-web/plugin-proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

function download_file($url){
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);

$response = curl_exec($ch);

$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = array_map('trim', explode("\n", substr($response, 0, $header_size)));
$body = substr($response, $header_size);

return [$headers, $body];
}


$plugin_name = preg_replace('#[^a-zA-Z0-9\.\-_]#', '', $_GET['plugin']);
$zip_url = 'https://downloads.wordpress.org/plugin/' . $plugin_name;

[$received_headers, $bytes] = download_file($zip_url);

$forward_headers = [
'content-length',
'content-type',
'content-disposition',
'x-frame-options',
'last-modified',
'etag',
'date',
'age',
'vary',
'cache-Control'
];

foreach($received_headers as $received_header) {
$comparable_header = strtolower($received_header);
foreach($forward_headers as $sought_header) {
if(substr($comparable_header, 0, strlen($sought_header)) === $sought_header){
header($received_header);
break;
}
}
}

echo $bytes;
6 changes: 6 additions & 0 deletions liveServer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const liveServer = require('live-server');
const request = require('request');

liveServer.start({
port: 8777,
Expand All @@ -9,6 +10,11 @@ liveServer.start({
(req, res, next) => {
if (req.url.startsWith('/scope:')) {
req.url = '/' + req.url.split('/').slice(2).join('/');
} else if (req.url.startsWith('/plugin-proxy')) {
const url = new URL(req.url, 'http://127.0.0.1:8777');
const pluginName = url.searchParams.get('plugin').replace(/[^a-zA-Z0-9\.\-_]/, '');
request(`https://downloads.wordpress.org/plugin/${pluginName}`).pipe(res);
return;
}
next();
},
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"lint-staged": "^13.0.3",
"live-server": "^1.2.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1"
"prettier": "^2.7.1",
"request": "^2.88.2"
}
}
4 changes: 3 additions & 1 deletion src/web/app.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { runWordPress } from "./library";
import { runWordPress, cloneResponseMonitorProgress } from "./library";
import {
wasmWorkerUrl,
wasmWorkerBackend,
Expand All @@ -21,3 +21,5 @@ window.startWordPress = async function (options = {}) {
console.log("[Main] WordPress is running");
return wasmWorker;
};

window.cloneResponseMonitorProgress = cloneResponseMonitorProgress;
52 changes: 45 additions & 7 deletions src/web/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,12 @@ export async function createWordPressWorker({
},
});
},
async installPlugin(zipUrl, options = {}) {
async installPlugin(pluginZipFile, options = {}) {
options = {
activate: true,
...options
};

// Download the plugin file
const filename = new URL(zipUrl, 'http://example.com').pathname.split('/').pop();
const pluginResponse = await fetch(zipUrl);
const pluginFile = new File([await pluginResponse.blob()], filename);

// Upload it to WordPress
const pluginForm = await this.HTTPRequest({
path: this.pathToInternalUrl('/wp-admin/plugin-install.php?tab=upload')
Expand All @@ -225,7 +220,7 @@ export async function createWordPressWorker({
path: this.pathToInternalUrl('/wp-admin/update.php?action=upload-plugin'),
method: 'POST',
_POST: postData,
files: { pluginzip: pluginFile },
files: { pluginzip: pluginZipFile },
});
const pluginInstalledPage = new DOMParser().parseFromString(pluginInstalledResponse.body, "text/html");
const activateButtonHref = pluginInstalledPage.querySelector('#wpbody-content .button.button-primary').attributes.href.value;
Expand Down Expand Up @@ -361,3 +356,46 @@ export function removeURLScope(url) {
}

// </URL UTILS>

export function cloneResponseMonitorProgress(response, onProgress) {
const contentLength = response.headers.get('content-length');
// When no content-length is provided, assume total is 5MB to
// at least show some progress, even if not completely accurate.
let total = parseInt(contentLength, 10) || 5 * 1024 * 1024;

return new Response(
new ReadableStream(
{
async start(controller) {
const reader = response.body.getReader();
let loaded = 0;
for (; ;) {
try {
const { done, value } = await reader.read();
if (value) {
loaded += value.byteLength;
}
if (done) {
onProgress({ loaded, total: loaded, done });
controller.close();
break;
} else {
onProgress({ loaded, total, done });
controller.enqueue(value);
}
} catch (e) {
console.error({ e });
controller.error(e);
break;
}
}
},
}
),
{
status: response.status,
statusText: response.statusText,
headers: response.headers
}
);
}
48 changes: 48 additions & 0 deletions src/web/plugin-proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

function download_file($url){
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);

$response = curl_exec($ch);

$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = array_map('trim', explode("\n", substr($response, 0, $header_size)));
$body = substr($response, $header_size);

return [$headers, $body];
}


$plugin_name = preg_replace('#[^a-zA-Z0-9\.\-_]#', '', $_GET['plugin']);
$zip_url = 'https://downloads.wordpress.org/plugin/' . $plugin_name;

[$received_headers, $bytes] = download_file($zip_url);

$forward_headers = [
'content-length',
'content-type',
'content-disposition',
'x-frame-options',
'last-modified',
'etag',
'date',
'age',
'vary',
'cache-Control'
];

foreach($received_headers as $received_header) {
$comparable_header = strtolower($received_header);
foreach($forward_headers as $sought_header) {
if(substr($comparable_header, 0, strlen($sought_header)) === $sought_header){
header($received_header);
break;
}
}
}

echo $bytes;
37 changes: 5 additions & 32 deletions src/web/wasm-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WPBrowser from '../shared/wp-browser.mjs';
import { responseTo } from '../shared/messaging.mjs';

import { phpWebWasmSize, wpDataSize, cacheBuster, phpWasmCacheBuster, wpDataCacheBuster } from './config';
import { cloneResponseMonitorProgress } from './library';

console.log('[WASM Worker] Spawned');

Expand Down Expand Up @@ -248,40 +249,12 @@ class WASMDownloadMonitor extends EventTarget {
const file = response.url.substring(
new URL(response.url).origin.length + 1
);
const contentLength = response.headers.get('content-length');
let total = parseInt(contentLength, 10);

const reportingResponse = new Response(
new ReadableStream(
{
async start(controller) {

const reader = response.body.getReader();
let loaded = 0;
for (;;) {
const { done, value } = await reader.read();
loaded += value.byteLength;

if (done) {
self._notify(file, loaded, loaded);
break;
}
self._notify(file, loaded, total);
controller.enqueue(value);
}
controller.close();
},
},
{
status: response.status,
statusText: response.statusText,
}
)

const reportingResponse = cloneResponseMonitorProgress(
response,
({ loaded, total }) => self._notify(file, loaded, total)
);

for (const pair of response.headers.entries()) {
reportingResponse.headers.set(pair[0], pair[1]);
}
return _instantiateStreaming(reportingResponse, ...args);
};
}
Expand Down
Loading

0 comments on commit 228ccc3

Please sign in to comment.