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

Remove DOMParser from Blueprints: installPlugin and installTheme #427

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
815acec
Rough draft for installPlugin
eliot-akira May 24, 2023
61e33f3
Format
eliot-akira May 24, 2023
7eb0e26
Merge remote-tracking branch 'upstream/trunk'
eliot-akira May 24, 2023
304e256
Test zip package name different from extracted plugin folder name
eliot-akira May 24, 2023
9c34cf4
Remove test plugin after install
eliot-akira May 24, 2023
89a627d
Use unzip step and find extracted plugin folder name
eliot-akira May 24, 2023
d53a55a
Format
eliot-akira May 24, 2023
98cde65
playground.documentRoot getter returns a promise
eliot-akira May 24, 2023
bfaef98
Remove temporary zip file after extraction
eliot-akira May 24, 2023
3f8eb82
Format
eliot-akira May 24, 2023
f2ed29b
activatePlugin step: await playground.documentRoot because it can ret…
eliot-akira May 24, 2023
7840907
BasePHP: Use FS.rename for method mv
eliot-akira May 24, 2023
daf74b3
Improve error message when extracted plugin folder not found
eliot-akira May 24, 2023
82ad36e
Make server-side test pass: Create plugins folder; Patch VFSResource …
eliot-akira May 24, 2023
3520de3
Format
eliot-akira May 24, 2023
579c568
Add comment about Gutenberg patch becoming unnecessary as the issue h…
eliot-akira May 24, 2023
534d10f
Rewrite installTheme step
eliot-akira May 25, 2023
0615596
Format
eliot-akira May 25, 2023
11bee55
Find plugin entry file to pass to activatePlugin step
eliot-akira May 25, 2023
6080b0d
activateTheme step: await playground.documentRoot because it can retu…
eliot-akira May 25, 2023
fa4f559
VFSResource: Clean up workaround
eliot-akira May 25, 2023
3fce74e
Tests: Clarify comment about zip file names being different from extr…
eliot-akira May 25, 2023
2b8d31b
Remove asDOM helper function, which used DOMParser available only on …
eliot-akira May 25, 2023
e5ae9b5
Remove unnecessary timeout value for tests
eliot-akira May 26, 2023
3dbd17a
Create findPluginEntryFile function for reusable logic
eliot-akira May 26, 2023
ca7b8c1
Remove comment about using plugin/theme upload form, since it's no lo…
eliot-akira May 26, 2023
9eed478
Merge branch 'trunk' into remove-dom-parser-from-blueprints
eliot-akira May 26, 2023
56cb9d3
Extract common logic between install plugin/theme into installAsset f…
eliot-akira May 27, 2023
28f7163
Format
eliot-akira May 27, 2023
dc05730
Install asset: Clean up temporary folder before throwing an error
eliot-akira May 27, 2023
81bb68f
Clarify comment about JSDOM's `File` class missing `arrayBuffer` meth…
eliot-akira May 30, 2023
a368526
installAsset: Make generic by replacing assetType with targetPath; Us…
eliot-akira May 30, 2023
7d4f233
Format
eliot-akira May 30, 2023
30d5c9a
activatePlugin step: Accept path to plugin directory and find plugin …
eliot-akira May 30, 2023
9ed3d75
installAsset: Clean up temporary folder and zip file on success or an…
eliot-akira May 30, 2023
3b67796
Polyfill the File class in JSDOM which lacks arrayBuffer() method
eliot-akira May 31, 2023
c121866
Format
eliot-akira May 31, 2023
661eda9
activatePlugin: Simplify logic to find plugin entry file
eliot-akira May 31, 2023
3cbea6a
activatePlugin: Simplify logic to find plugin entry file
eliot-akira May 31, 2023
4487c3a
activatePlugin: Clarify accepted value for pluginPath option (absolut…
eliot-akira May 31, 2023
6b0f60f
Merge branch 'trunk' into remove-dom-parser-from-blueprints
eliot-akira May 31, 2023
1671b88
Clean up after merge from trunk
eliot-akira May 31, 2023
3526f57
Format
eliot-akira May 31, 2023
6f81a34
activatePlugin: Use error code for entry file not found; Rethrow any …
eliot-akira May 31, 2023
977230f
Format
eliot-akira May 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Expand Up @@ -5,6 +5,8 @@ root = true
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
indent_size = 4

[*.md]
max_line_length = off
Expand Down
2 changes: 1 addition & 1 deletion README.md
@@ -1,4 +1,4 @@
# WordPress Playground and PHP WASM (WebAssembly)
# WordPress Playground and PHP WASM (WebAssembly)

eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
[Project Page](https://developer.wordpress.org/playground/) | [Live demo](https://playground.wordpress.net/) | [Documentation and API Reference](https://wordpress.github.io/wordpress-playground/)

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/site/docs/13-contributing/05-publishing.md
Expand Up @@ -36,7 +36,7 @@ npm run release
Internet connections drop, APIs stop responding, and GitHub rules are nasty. Stuff happens. If the publishing process fails, you may need to bump the version again and force a publish. To do so, execute the following command:

```bash
npm run release --force-publish
npm run release -- --force-publish
```

## GitHub documentation
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/universal/src/lib/base-php.ts
Expand Up @@ -486,7 +486,7 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
/** @inheritDoc */
@rethrowFileSystemError('Could not move "{path}"')
mv(fromPath: string, toPath: string) {
this[__private__dont__use].FS.mv(fromPath, toPath);
this[__private__dont__use].FS.rename(fromPath, toPath);
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}

/** @inheritDoc */
Expand Down
Expand Up @@ -19,8 +19,8 @@ export const activatePlugin: StepHandler<ActivatePluginStep> = async (
) => {
progress?.tracker.setCaption(`Activating ${pluginPath}`);
const requiredFiles = [
`${playground.documentRoot}/wp-load.php`,
`${playground.documentRoot}/wp-admin/includes/plugin.php`,
`${await playground.documentRoot}/wp-load.php`,
`${await playground.documentRoot}/wp-admin/includes/plugin.php`,
];
const requiredFilesExist = requiredFiles.every((file) =>
playground.fileExists(file)
Expand Down
@@ -0,0 +1,57 @@
import { NodePHP } from '@php-wasm/node';
import { compileBlueprint, runBlueprintSteps } from '../compile';

const phpVersion = '8.0';
describe('Blueprint step installPlugin', () => {
let php: NodePHP;
beforeEach(async () => {
php = await NodePHP.load(phpVersion, {
requestHandler: {
documentRoot: '/',
isStaticFilePath: (path) => !path.endsWith('.php'),
},
});
});

it('should install a plugin', async () => {
// Create test plugin

php.mkdir('/tmp/test-plugin');
php.writeFile(
'/tmp/test-plugin/index.php',
`/**\n * Plugin Name: Test Plugin`
);

const zipFileName = 'test-plugin-0.0.1.zip';

await php.run({
code: `<?php $zip = new ZipArchive(); $zip->open("${zipFileName}", ZIPARCHIVE::CREATE); $zip->addFile("/tmp/test-plugin/index.php"); $zip->close();`,
});

php.rmdir('/tmp/test-plugin');

// Note the package name is different from plugin folder name
expect(php.fileExists(zipFileName)).toBe(true);

await runBlueprintSteps(
compileBlueprint({
steps: [
{
step: 'installPlugin',
pluginZipFile: {
resource: 'vfs',
path: zipFileName,
},
},
],
}),
php
);

php.unlink(zipFileName);

expect(
php.fileExists(`${php.documentRoot}/wp-content/test-plugin`)
).toBe(true);
}, 30000);
});
199 changes: 113 additions & 86 deletions packages/playground/blueprints/src/lib/steps/install-plugin.ts
@@ -1,6 +1,9 @@
import { UniversalPHP } from '@php-wasm/universal';
import { StepHandler } from '.';
import { asDOM, zipNameToHumanName } from './common';
import { zipNameToHumanName } from './common';
import { writeFile } from './client-methods';
import { activatePlugin } from './activate-plugin';
import { unzip } from './import-export';

/**
* @inheritDoc installPlugin
Expand Down Expand Up @@ -58,110 +61,134 @@ export const installPlugin: StepHandler<InstallPluginStep<File>> = async (
{ pluginZipFile, options = {} },
progress?
) => {
progress?.tracker.setCaption(
`Installing the ${zipNameToHumanName(pluginZipFile?.name)} plugin`
);
const zipFileName = pluginZipFile.name.split('/').pop() || 'plugin.zip';
const zipNiceName = zipNameToHumanName(zipFileName);

progress?.tracker.setCaption(`Installing the ${zipNiceName} plugin`);
try {
const activate = 'activate' in options ? options.activate : true;
// Extract to temporary folder so we can find plugin folder name

const tmpFolder = '/tmp/plugin';
const tmpZipPath = `/tmp/${zipFileName}`;

// Upload it to WordPress
const pluginForm = await playground.request({
url: '/wp-admin/plugin-install.php?tab=upload',
if (await playground.isDir(tmpFolder)) {
await playground.unlink(tmpFolder);
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}

await writeFile(playground, {
path: tmpZipPath,
data: pluginZipFile,
});
const pluginFormPage = asDOM(pluginForm);
const pluginFormData = new FormData(
pluginFormPage.querySelector('.wp-upload-form')! as HTMLFormElement
) as any;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { pluginzip, ...postData } = Object.fromEntries(
pluginFormData.entries()
);

const pluginInstalledResponse = await playground.request({
url: '/wp-admin/update.php?action=upload-plugin',
method: 'POST',
formData: postData,
files: { pluginzip: pluginZipFile },
await unzip(playground, {
zipPath: tmpZipPath,
extractToPath: tmpFolder,
});

// Activate if needed
if (activate) {
const pluginInstalledPage = asDOM(pluginInstalledResponse);
const activateButtonHref = pluginInstalledPage
.querySelector('#wpbody-content .button.button-primary')!
.attributes.getNamedItem('href')!.value;
const activatePluginUrl = new URL(
activateButtonHref,
await playground.pathToInternalUrl('/wp-admin/')
).toString();
await playground.request({
url: activatePluginUrl,
});
await playground.unlink(tmpZipPath);

// Find extracted plugin folder name

const files = await playground.listFiles(tmpFolder);

let pluginFolderName;
let tmpPluginPath = '';

for (const file of files) {
tmpPluginPath = `${tmpFolder}/${file}`;
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
if (await playground.isDir(tmpPluginPath)) {
pluginFolderName = file;
break;
}
}

/**
* Pair the site editor's nested iframe to the Service Worker.
*
* Without the patch below, the site editor initiates network requests that
* aren't routed through the service worker. That's a known browser issue:
*
* * https://bugs.chromium.org/p/chromium/issues/detail?id=880768
* * https://bugzilla.mozilla.org/show_bug.cgi?id=1293277
* * https://github.com/w3c/ServiceWorker/issues/765
*
* The problem with iframes using srcDoc and src="about:blank" as they
* fail to inherit the root site's service worker.
*
* Gutenberg loads the site editor using <iframe srcDoc="<!doctype html">
* to force the standards mode and not the quirks mode:
*
* https://github.com/WordPress/gutenberg/pull/38855
*
* This commit patches the site editor to achieve the same result via
* <iframe src="/doctype.html"> and a doctype.html file containing just
* `<!doctype html>`. This allows the iframe to inherit the service worker
* and correctly load all the css, js, fonts, images, and other assets.
*
* Ideally this issue would be fixed directly in Gutenberg and the patch
* below would be removed.
*
* See https://github.com/WordPress/wordpress-playground/issues/42 for more details
*/
if (
(await playground.isDir(
'/wordpress/wp-content/plugins/gutenberg'
)) &&
!(await playground.fileExists('/wordpress/.gutenberg-patched'))
) {
await playground.writeFile('/wordpress/.gutenberg-patched', '1');
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.js`,
(contents) =>
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
)
);
await updateFile(
if (!pluginFolderName) {
throw new Error('Extracted plugin folder not found');
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}

// Move it to site plugins
const rootPath = await playground.documentRoot;
const pluginPath = `${rootPath}/wp-content/plugins/${pluginFolderName}`;

await playground.mv(tmpPluginPath, pluginPath);

const activate = 'activate' in options ? options.activate : true;

if (activate) {
await activatePlugin(
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.min.js`,
(contents) =>
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
)
{
pluginPath,
},
progress
);
}

await maybeApplyGutenbergPatch(playground);
} catch (error) {
console.error(
`Proceeding without the ${pluginZipFile.name} theme. Could not install it in wp-admin. ` +
`Proceeding without the ${zipNiceName} plugin. Could not install it in wp-admin. ` +
`The original error was: ${error}`
);
console.error(error);
}
};

async function maybeApplyGutenbergPatch(playground: UniversalPHP) {
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
/**
* Pair the site editor's nested iframe to the Service Worker.
*
* Without the patch below, the site editor initiates network requests that
* aren't routed through the service worker. That's a known browser issue:
*
* * https://bugs.chromium.org/p/chromium/issues/detail?id=880768
* * https://bugzilla.mozilla.org/show_bug.cgi?id=1293277
* * https://github.com/w3c/ServiceWorker/issues/765
*
* The problem with iframes using srcDoc and src="about:blank" as they
* fail to inherit the root site's service worker.
*
* Gutenberg loads the site editor using <iframe srcDoc="<!doctype html">
* to force the standards mode and not the quirks mode:
*
* https://github.com/WordPress/gutenberg/pull/38855
*
* This commit patches the site editor to achieve the same result via
* <iframe src="/doctype.html"> and a doctype.html file containing just
* `<!doctype html>`. This allows the iframe to inherit the service worker
* and correctly load all the css, js, fonts, images, and other assets.
*
* Ideally this issue would be fixed directly in Gutenberg and the patch
* below would be removed.
*
* See https://github.com/WordPress/wordpress-playground/issues/42 for more details
*/
if (
(await playground.isDir('/wordpress/wp-content/plugins/gutenberg')) &&
!(await playground.fileExists('/wordpress/.gutenberg-patched'))
) {
await playground.writeFile('/wordpress/.gutenberg-patched', '1');
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.js`,
(contents) =>
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
)
);
await updateFile(
playground,
`/wordpress/wp-content/plugins/gutenberg/build/block-editor/index.min.js`,
(contents) =>
contents.replace(
/srcDoc:("[^"]+"|[^,]+)/g,
'src:"/wp-includes/empty.html"'
)
);
}
}

async function updateFile(
playground: UniversalPHP,
path: string,
Expand Down
2 changes: 1 addition & 1 deletion packages/wp-now/README.md
Expand Up @@ -75,7 +75,7 @@ Of these, `wp-now php` currently supports the `--path=<path>`, `--php=<version>`

If you are migrating from Laravel Valet, you should be aware of the differences it has with `wp-now`:

- `wp-now` does not require you to install WordPress separately, create a database, connect WordPress to that database or create a user account. All of these steps are handled by the `wp now start` command and are running under the hood;
- `wp-now` does not require you to install WordPress separately, create a database, connect WordPress to that database or create a user account. All of these steps are handled by the `wp-now start` command and are running under the hood;
- `wp-now` works across all platforms (Mac, Linux, Windows);
- `wp-now` does not support custom domains or SSL (yet!);
- `wp-now` works with WordPress themes and plugins even if you don't have WordPress installed;
Expand Down
22 changes: 12 additions & 10 deletions packages/wp-now/src/tests/wp-now.spec.ts
@@ -1,5 +1,5 @@
import startWPNow, { inferMode } from '../wp-now';
import getWpNowConfig, { CliOptions, WPNowMode, WPNowOptions } from '../config';

Check warning on line 2 in packages/wp-now/src/tests/wp-now.spec.ts

View workflow job for this annotation

GitHub Actions / main

'WPNowOptions' is defined but never used
import fs from 'fs-extra';
import path from 'path';
import {
Expand All @@ -19,6 +19,14 @@

const exampleDir = __dirname + '/mode-examples';

async function downloadWithTimer(name, fn) {
console.log(`Downloading ${name}...`);
console.time(name);
await fn();
console.log(`${name} downloaded.`);
console.timeEnd(name);
}

// Options
test('getWpNowConfig with default options', async () => {
const rawOptions: CliOptions = {
Expand Down Expand Up @@ -176,16 +184,10 @@
*/
beforeAll(async () => {
fs.rmSync(getWpNowTmpPath(), { recursive: true, force: true });
console.log('Downloading WordPress...');
console.time('wordpress');
await downloadWordPress();
console.log('WordPress downloaded.');
console.timeEnd('wordpress');
console.log('Downloading SQLite...');
console.time('sqlite');
await downloadSqliteIntegrationPlugin();
console.log('SQLite downloaded.');
console.timeEnd('sqlite');
await Promise.all([
downloadWithTimer('wordpresss', downloadWordPress),
downloadWithTimer('sqlite', downloadSqliteIntegrationPlugin),
]);
});

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/wp-now/src/wp-now.ts
Expand Up @@ -88,9 +88,11 @@ export default async function startWPNow(
});
return { php, phpInstances, options };
}
await downloadWordPress(options.wordPressVersion);
await downloadSqliteIntegrationPlugin();
await downloadMuPlugins();
await Promise.all([
downloadWordPress(options.wordPressVersion),
downloadSqliteIntegrationPlugin(),
downloadMuPlugins(),
]);
const isFirstTimeProject = !fs.existsSync(options.wpContentPath);
await applyToInstances(phpInstances, async (_php) => {
switch (options.mode) {
Expand Down