Skip to content

Commit

Permalink
PHP: Use auto_prepend_file to preload mu-plugins (instead of creating…
Browse files Browse the repository at this point in the history
… them in wp-content/mu-plugins) (#1366)

This PR moves the Playground mu-plugins from
`/wordpress/wp-content/mu-plugins`, where they may interfere with the
user-provided files and pollute the git repository, to
`/internal/mu-plugins`, where they're invisible to the Playground
consumer and don't show up in exports or OPFS mounts.

Closes #1318

## Changes made

Technically, this PR uses the `auto_prepend_file` PHP options to
pre-load the following script:

```php
<?php
foreach (glob('/internal/preload/*.php') as $file) {
	require_once $file;
}
```

From here, preloading files is as simple as creating them in
`/internal/preload`. This PR ships a few such preloaded files:

* `consts.php`, that enables predefining PHP constants without affecting
`wp-config.php`.
* `env.php`, that preloads mu-plugins from `/internal/mu-plugins` using
[the method described
by](#1318 (comment))
@brandonpayton.
* `phpinfo.php`, that just calls `phpinfo();` when the request URL is
`/phpinfo.php` – this prevents creating an actual `phpinfo.php` file in
the document root. This involves a slight change in the path resolution
logic which will have to change even more soon to solve #1365.

From there, the SQLite integration plugin and the Playground mu-plugins
are placed in `/internal/mu-plugins` and do not affect the WordPress
file structure.

This PR also reduces code duplication by moving some parts of the
WordPress setup to the `@wp-playground/wordpress` package where they can
be imported by both the web and the CLI versions of Playground. We'll
only see more of these moving forward.

## Follow-up work

* Define clear use-cases and behaviors related to the location of
Playground-provided files. E.g. Full site export, where we want to keep
the SQLite integration plugin, or a GitHub repo, where we only want to
keep track of our changes in wp-content.
* Use `@wp-playground/cli` to build and setup WordPress in
`packages/playground/wordpress-builds/build/Dockerfile`
* Playground web: do not ship the SQLite integration plugin in the
pre-built `wp-content` directory. Source it from the
`/internal/mu-plugins` directory same way as in the CLI build.
* Do not create the drop-in plugin `wp-content/db.php`
* Do not install the WordPress importer plugin in `wp-content/plugins`
* Figure out how to avoid touching `wp-config.php` – defining PHP
constants may require rewriting it and it might be desirable to keep
those changes on export

## Testing instructions

* Confirm the tests pass
* In the browser – export Playground as zip, import it, also refresh the
page and import again, confirm it all worked.
  • Loading branch information
adamziel authored May 9, 2024
1 parent 31194d0 commit 24bcd5b
Show file tree
Hide file tree
Showing 39 changed files with 289 additions and 260 deletions.
3 changes: 3 additions & 0 deletions packages/php-wasm/compile/php/phpwasm-emscripten-library.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const LibraryExample = {
// stdout, stderr, and headers information are written for the JavaScript
// code to read later on.
FS.mkdir("/internal");
// The files from the preload directory are preloaded using the
// auto_prepend_file php.ini directive.
FS.mkdir("/internal/preload");

PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE
? require('events').EventEmitter
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_7_0.js
Original file line number Diff line number Diff line change
Expand Up @@ -5562,7 +5562,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_7_1.js
Original file line number Diff line number Diff line change
Expand Up @@ -5562,7 +5562,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_7_2.js
Original file line number Diff line number Diff line change
Expand Up @@ -5562,7 +5562,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_7_3.js
Original file line number Diff line number Diff line change
Expand Up @@ -5562,7 +5562,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_7_4.js
Original file line number Diff line number Diff line change
Expand Up @@ -5561,7 +5561,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_8_0.js
Original file line number Diff line number Diff line change
Expand Up @@ -5561,7 +5561,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_8_1.js
Original file line number Diff line number Diff line change
Expand Up @@ -5577,7 +5577,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_8_2.js
Original file line number Diff line number Diff line change
Expand Up @@ -5582,7 +5582,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/node/public/php_8_3.js
Original file line number Diff line number Diff line change
Expand Up @@ -5582,7 +5582,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
40 changes: 24 additions & 16 deletions packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,32 +358,40 @@ export abstract class BasePHP implements IsomorphicLocalPHP, Disposable {
}

#initWebRuntime() {
this.writeFile(
'/internal/auto_prepend_file.php',
`<?php
foreach (glob('/internal/preload/*.php') as $file) {
require_once $file;
}
`
);
this.setPhpIniEntry(
'auto_prepend_file',
'/internal/auto_prepend_file.php'
);
/**
* This creates a consts.php file in an in-memory
* /internal directory and sets the auto_prepend_file PHP option
* /internal/preload directory and sets the auto_prepend_file PHP option
* to always load that file.
* @see https://www.php.net/manual/en/ini.core.php#ini.auto-prepend-file
*
* Technically, this is a workaround. In the future, let's implement a
* WASM SAPI method to pass consts directly.
* @see https://github.com/WordPress/wordpress-playground/issues/750
*/
this.setPhpIniEntry('auto_prepend_file', '/internal/consts.php');
if (!this.fileExists('/internal/consts.php')) {
this.writeFile(
'/internal/consts.php',
`<?php
if(file_exists('/internal/consts.json')) {
$consts = json_decode(file_get_contents('/internal/consts.json'), true);
foreach ($consts as $const => $value) {
if (!defined($const) && is_scalar($value)) {
define($const, $value);
}
this.writeFile(
'/internal/preload/consts.php',
`<?php
if(file_exists('/internal/consts.json')) {
$consts = json_decode(file_get_contents('/internal/consts.json'), true);
foreach ($consts as $const => $value) {
if (!defined($const) && is_scalar($value)) {
define($const, $value);
}
}`
);
}

}
}`
);
if (this.#phpIniOverrides.length > 0) {
const overridesAsIni =
this.#phpIniOverrides
Expand Down
1 change: 1 addition & 0 deletions packages/php-wasm/universal/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ export {
} from './urls';

export { isExitCodeZero } from './is-exit-code-zero';
export { proxyFileSystem } from './proxy-file-system';
7 changes: 6 additions & 1 deletion packages/php-wasm/universal/src/lib/php-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,12 @@ export class PHPRequestHandler<PHP extends BasePHP> {
filePath = '/index.php';
}

const resolvedFsPath = `${this.#DOCROOT}${filePath}`;
let resolvedFsPath = `${this.#DOCROOT}${filePath}`;
// If the requested PHP file doesn't exist, let's fall back to /index.php
// as the request may need to be rewritten.
if (!php.fileExists(resolvedFsPath)) {
resolvedFsPath = `${this.#DOCROOT}/index.php`;
}
if (php.fileExists(resolvedFsPath)) {
return resolvedFsPath;
}
Expand Down
36 changes: 36 additions & 0 deletions packages/php-wasm/universal/src/lib/proxy-file-system.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { BasePHP } from './base-php';

/**
* Proxy specific paths to the parent's MEMFS instance.
* This is useful for sharing the WordPress installation
* between the parent and child processes.
*/
export function proxyFileSystem(
sourceOfTruth: BasePHP,
replica: BasePHP,
paths: string[]
) {
// We can't just import the symbol from the library because
// Playground CLI is built as ESM and php-wasm-node is built as
// CJS and the imported symbols will different in the production build.
const __private__symbol = Object.getOwnPropertySymbols(sourceOfTruth)[0];
for (const path of paths) {
if (!replica.fileExists(path)) {
replica.mkdir(path);
}
if (!sourceOfTruth.fileExists(path)) {
sourceOfTruth.mkdir(path);
}
// @ts-ignore
replica[__private__symbol].FS.mount(
// @ts-ignore
replica[__private__symbol].PROXYFS,
{
root: path,
// @ts-ignore
fs: sourceOfTruth[__private__symbol].FS,
},
path
);
}
}
2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_7_0.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_7_1.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_7_2.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_7_3.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_7_4.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_8_0.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_8_1.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_8_2.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/kitchen-sink/php_8_3.js
Original file line number Diff line number Diff line change
Expand Up @@ -5241,7 +5241,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_7_0.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_7_1.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_7_2.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_7_3.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_7_4.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_8_0.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_8_1.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_8_2.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/php-wasm/web/public/light/php_8_3.js
Original file line number Diff line number Diff line change
Expand Up @@ -5241,7 +5241,7 @@ var allocateUTF8OnStack = stringToUTF8OnStack;

var PHPWASM = {
init: function() {
FS.mkdir("/internal");
FS.mkdir("/internal");FS.mkdir("/internal/preload");
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE ? require("events").EventEmitter : class EventEmitter {
constructor() {
this.listeners = {};
Expand Down
68 changes: 20 additions & 48 deletions packages/playground/cli/src/setup-php.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { NodePHP } from '@php-wasm/node';
import {
BasePHP,
PHPRequestHandler,
SupportedPHPVersion,
proxyFileSystem,
rotatePHPRuntime,
} from '@php-wasm/universal';
import { rootCertificates } from 'tls';
import { dirname } from '@php-wasm/util';
import { envPHP_to_loadMuPlugins } from '@wp-playground/wordpress';

export async function createPhp(
requestHandler: PHPRequestHandler<NodePHP>,
Expand Down Expand Up @@ -40,72 +41,43 @@ export async function createPhp(
php.setPhpIniEntry('openssl.cafile', '/tmp/ca-bundle.crt');
php.writeFile('/tmp/ca-bundle.crt', rootCertificates.join('\n'));

php.writeFile('/internal/preload/env.php', envPHP_to_loadMuPlugins);
php.writeFile(
'/internal/preload/phpinfo.php',
`<?php
// Render PHPInfo if the requested page is /phpinfo.php
if ( '/phpinfo.php' === $_SERVER['REQUEST_URI'] ) {
phpinfo();
exit;
}`
);
php.mkdir('/internal/mu-plugins');

if (!isPrimary) {
/**
* @TODO: Consider an API similar to
*
* php.mount('/wordpress', primaryPHP.getMountPoint('/wordpress'));
*/
proxyFileSystem(
await requestHandler.getPrimaryPhp(),
php,
'/wordpress'
);
proxyFileSystem(await requestHandler.getPrimaryPhp(), php, [
'/tmp',
requestHandler.documentRoot,
'/internal/mu-plugins',
]);
}

// php.setSpawnHandler(spawnHandlerFactory(processManager));
// Rotate the PHP runtime periodically to avoid memory leak-related crashes.
// @see https://github.com/WordPress/wordpress-playground/pull/990 for more context
rotatePHPRuntime({
php,
cwd: '/wordpress',
cwd: requestHandler.documentRoot,
recreateRuntime: createPhpRuntime,
maxRequests: 400,
});
return php;
}

/**
* Share the parent's MEMFS instance with the child process.
* Only mount the document root and the /tmp directory,
* the rest of the filesystem (like the devices) should be
* private to each PHP instance.
*
* @TODO: Ship this feature in the php-wasm library. It
* will be commonly used in multi-instance Playground
* applications. The website app does something similar,
* and so will wp-now, VSCode, etc.
*/
export function proxyFileSystem(
sourceOfTruth: BasePHP,
replica: BasePHP,
documentRoot: string
) {
// We can't just import the symbol from the library because
// Playground CLI is built as ESM and php-wasm-node is built as
// CJS and the imported symbols will different in the production build.
const __private__symbol = Object.getOwnPropertySymbols(sourceOfTruth)[0];
for (const path of [documentRoot, '/tmp']) {
if (!replica.fileExists(path)) {
replica.mkdir(path);
}
if (!sourceOfTruth.fileExists(path)) {
sourceOfTruth.mkdir(path);
}
// @ts-ignore
replica[__private__symbol].FS.mount(
// @ts-ignore
replica[__private__symbol].PROXYFS,
{
root: path,
// @ts-ignore
fs: sourceOfTruth[__private__symbol].FS,
},
path
);
}
}

/**
* @TODO: Ship this feature in the php-wasm library.
*
Expand Down
Loading

0 comments on commit 24bcd5b

Please sign in to comment.