-
Notifications
You must be signed in to change notification settings - Fork 272
/
boot.ts
309 lines (283 loc) · 8 KB
/
boot.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
import {
FileNotFoundAction,
FileNotFoundGetActionCallback,
FileTree,
PHP,
PHPProcessManager,
PHPRequestHandler,
SpawnHandler,
proxyFileSystem,
rotatePHPRuntime,
setPhpIniEntries,
withPHPIniValues,
writeFiles,
} from '@php-wasm/universal';
import {
preloadPhpInfoRoute,
setupPlatformLevelMuPlugins,
preloadSqliteIntegration,
unzipWordPress,
wordPressRewriteRules,
} from '.';
import { joinPaths } from '@php-wasm/util';
import { logger } from '@php-wasm/logger';
export type PhpIniOptions = Record<string, string>;
export type Hook = (php: PHP) => void | Promise<void>;
export interface Hooks {
beforeWordPressFiles?: Hook;
beforeDatabaseSetup?: Hook;
}
export type DatabaseType = 'sqlite' | 'mysql' | 'custom';
export interface BootOptions {
createPhpRuntime: () => Promise<number>;
/**
* Mounting and Copying is handled via hooks for starters.
*
* In the future we could standardize the
* browser-specific and node-specific mounts
* in the future.
*/
hooks?: Hooks;
/**
* PHP SAPI name to be returned by get_sapi_name(). Overriding
* it is useful for running programs that check for this value,
* e.g. WP-CLI
*/
sapiName?: string;
/**
* URL to use as the site URL. This is used to set the WP_HOME
* and WP_SITEURL constants in WordPress.
*/
siteUrl: string;
documentRoot?: string;
/** SQL file to load instead of installing WordPress. */
dataSqlPath?: string;
/** Zip with the WordPress installation to extract in /wordpress. */
wordPressZip?: File | Promise<File> | undefined;
/** Preloaded SQLite integration plugin. */
sqliteIntegrationPluginZip?: File | Promise<File>;
spawnHandler?: (processManager: PHPProcessManager) => SpawnHandler;
/**
* PHP.ini entries to define before running any code. They'll
* be used for all requests.
*/
phpIniEntries?: PhpIniOptions;
/**
* PHP constants to define for every request.
*/
constants?: Record<string, string | number | boolean | null>;
/**
* Files to create in the filesystem before any mounts are applied.
*
* Example:
*
* ```ts
* {
* createFiles: {
* '/tmp/hello.txt': 'Hello, World!',
* '/internal/preload': {
* '1-custom-mu-plugin.php': '<?php echo "Hello, World!";',
* }
* }
* }
* ```
*/
createFiles?: FileTree;
/**
* A callback that decides how to handle a file-not-found condition for a
* given request URI.
*/
getFileNotFoundAction?: FileNotFoundGetActionCallback;
}
/**
* Boots a WordPress instance with the given options.
*
* High-level overview:
*
* * Boot PHP instances and PHPRequestHandler
* * Setup VFS, run beforeWordPressFiles hook
* * Setup WordPress files (if wordPressZip is provided)
* * Run beforeDatabaseSetup hook
* * Setup the database – SQLite, MySQL (@TODO), or rely on a mounted database
* * Run WordPress installer, if the site isn't installed yet
*
* @param options Boot configuration options
* @return PHPRequestHandler instance with WordPress installed.
*/
export async function bootWordPress(options: BootOptions) {
async function createPhp(
requestHandler: PHPRequestHandler,
isPrimary: boolean
) {
const php = new PHP(await options.createPhpRuntime());
if (options.sapiName) {
php.setSapiName(options.sapiName);
}
if (requestHandler) {
php.requestHandler = requestHandler;
}
if (options.phpIniEntries) {
setPhpIniEntries(php, options.phpIniEntries);
}
/**
* Set up mu-plugins in /internal/shared/mu-plugins
* using auto_prepend_file to provide platform-level
* customization without altering the installed WordPress
* site.
*
* We only do that in the primary PHP instance –
* the filesystem there is the source of truth
* for all other PHP instances.
*/
if (isPrimary) {
await setupPlatformLevelMuPlugins(php);
await writeFiles(php, '/', options.createFiles || {});
await preloadPhpInfoRoute(
php,
joinPaths(new URL(options.siteUrl).pathname, 'phpinfo.php')
);
} else {
// Proxy the filesystem for all secondary PHP instances to
// the primary one.
proxyFileSystem(await requestHandler.getPrimaryPhp(), php, [
'/tmp',
requestHandler.documentRoot,
'/internal/shared',
]);
}
// Spawn handler is responsible for spawning processes for all the
// `popen()`, `proc_open()` etc. calls.
if (options.spawnHandler) {
await php.setSpawnHandler(
options.spawnHandler(requestHandler.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: requestHandler.documentRoot,
recreateRuntime: options.createPhpRuntime,
maxRequests: 400,
});
return php;
}
const requestHandler: PHPRequestHandler = new PHPRequestHandler({
phpFactory: async ({ isPrimary }) =>
createPhp(requestHandler, isPrimary),
documentRoot: options.documentRoot || '/wordpress',
absoluteUrl: options.siteUrl,
rewriteRules: wordPressRewriteRules,
getFileNotFoundAction:
options.getFileNotFoundAction ?? getFileNotFoundActionForWordPress,
});
const php = await requestHandler.getPrimaryPhp();
if (options.hooks?.beforeWordPressFiles) {
await options.hooks.beforeWordPressFiles(php);
}
if (options.wordPressZip) {
await unzipWordPress(php, await options.wordPressZip);
}
if (options.constants) {
for (const key in options.constants) {
php.defineConstant(key, options.constants[key] as string);
}
}
php.defineConstant('WP_HOME', options.siteUrl);
php.defineConstant('WP_SITEURL', options.siteUrl);
// Run "before database" hooks to mount/copy more files in
if (options.hooks?.beforeDatabaseSetup) {
await options.hooks.beforeDatabaseSetup(php);
}
// @TODO Assert WordPress core files are in place
if (options.sqliteIntegrationPluginZip) {
await preloadSqliteIntegration(
php,
await options.sqliteIntegrationPluginZip
);
}
if (!(await isWordPressInstalled(php))) {
await installWordPress(php);
}
if (!(await isWordPressInstalled(php))) {
throw new Error('WordPress installation has failed.');
}
return requestHandler;
}
async function isWordPressInstalled(php: PHP) {
const result = await php.run({
code: `<?php
$wp_load = getenv('DOCUMENT_ROOT') . '/wp-load.php';
if (!file_exists($wp_load)) {
echo '0';
exit;
}
require $wp_load;
echo is_blog_installed() ? '1' : '0';
`,
env: {
DOCUMENT_ROOT: php.documentRoot,
},
});
return result.text === '1';
}
async function installWordPress(php: PHP) {
// Disables networking for the installation wizard
// to avoid loopback requests and also speed it up.
await withPHPIniValues(
php,
{
disable_functions: 'fsockopen',
allow_url_fopen: '0',
},
async () =>
await php.request({
url: '/wp-admin/install.php?step=2',
method: 'POST',
body: {
language: 'en',
prefix: 'wp_',
weblog_title: 'My WordPress Website',
user_name: 'admin',
admin_password: 'password',
// The installation wizard demands typing the same password twice
admin_password2: 'password',
Submit: 'Install WordPress',
pw_weak: '1',
admin_email: 'admin@localhost.com',
},
})
);
const defaultedToPrettyPermalinks = await php.run({
code: `<?php
$wp_load = getenv('DOCUMENT_ROOT') . '/wp-load.php';
if (!file_exists($wp_load)) {
echo '0';
exit;
}
require $wp_load;
$option_result = update_option(
'permalink_structure',
'/%year%/%monthnum%/%day%/%postname%/'
);
echo $option_result ? '1' : '0';
`,
env: {
DOCUMENT_ROOT: php.documentRoot,
},
});
if (defaultedToPrettyPermalinks.text !== '1') {
logger.warn('Failed to default to pretty permalinks after WP install.');
}
}
export function getFileNotFoundActionForWordPress(
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- maintain consistent FileNotFoundGetActionCallback signature
relativeUri: string
): FileNotFoundAction {
// Delegate unresolved requests to WordPress. This makes WP magic possible,
// like pretty permalinks and dynamically generated sitemaps.
return {
type: 'internal-redirect',
uri: '/index.php',
};
}