@php-wasm/universal 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,555 +0,0 @@
1
- import { PHPBrowser } from './php-browser';
2
- import {
3
- PHPRequestHandler,
4
- PHPRequestHandlerConfiguration,
5
- } from './php-request-handler';
6
- import { PHPResponse } from './php-response';
7
- import { rethrowFileSystemError } from './rethrow-file-system-error';
8
- import { getLoadedRuntime } from './load-php-runtime';
9
- import type { PHPRuntimeId } from './load-php-runtime';
10
- import {
11
- FileInfo,
12
- IsomorphicLocalPHP,
13
- PHPRequest,
14
- PHPRequestHeaders,
15
- PHPRunOptions,
16
- RmDirOptions,
17
- } from './universal-php';
18
- import {
19
- getFunctionsMaybeMissingFromAsyncify,
20
- improveWASMErrorReporting,
21
- UnhandledRejectionsTarget,
22
- } from './wasm-error-reporting';
23
-
24
- const STRING = 'string';
25
- const NUMBER = 'number';
26
-
27
- export const __private__dont__use = Symbol('__private__dont__use');
28
- /**
29
- * An environment-agnostic wrapper around the Emscripten PHP runtime
30
- * that universals the super low-level API and provides a more convenient
31
- * higher-level API.
32
- *
33
- * It exposes a minimal set of methods to run PHP scripts and to
34
- * interact with the PHP filesystem.
35
- */
36
- export abstract class BasePHP implements IsomorphicLocalPHP {
37
- protected [__private__dont__use]: any;
38
- #phpIniOverrides: [string, string][] = [];
39
- #webSapiInitialized = false;
40
- #wasmErrorsTarget: UnhandledRejectionsTarget | null = null;
41
- requestHandler?: PHPBrowser;
42
-
43
- /**
44
- * Initializes a PHP runtime.
45
- *
46
- * @internal
47
- * @param PHPRuntime - Optional. PHP Runtime ID as initialized by loadPHPRuntime.
48
- * @param serverOptions - Optional. Options for the PHPRequestHandler. If undefined, no request handler will be initialized.
49
- */
50
- constructor(
51
- PHPRuntimeId?: PHPRuntimeId,
52
- serverOptions?: PHPRequestHandlerConfiguration
53
- ) {
54
- if (PHPRuntimeId !== undefined) {
55
- this.initializeRuntime(PHPRuntimeId);
56
- }
57
- if (serverOptions) {
58
- this.requestHandler = new PHPBrowser(
59
- new PHPRequestHandler(this, serverOptions)
60
- );
61
- }
62
- }
63
-
64
- /** @inheritDoc */
65
- get absoluteUrl() {
66
- return this.requestHandler!.requestHandler.absoluteUrl;
67
- }
68
-
69
- /** @inheritDoc */
70
- get documentRoot() {
71
- return this.requestHandler!.requestHandler.documentRoot;
72
- }
73
-
74
- /** @inheritDoc */
75
- pathToInternalUrl(path: string): string {
76
- return this.requestHandler!.requestHandler.pathToInternalUrl(path);
77
- }
78
-
79
- /** @inheritDoc */
80
- internalUrlToPath(internalUrl: string): string {
81
- return this.requestHandler!.requestHandler.internalUrlToPath(
82
- internalUrl
83
- );
84
- }
85
-
86
- initializeRuntime(runtimeId: PHPRuntimeId) {
87
- if (this[__private__dont__use]) {
88
- throw new Error('PHP runtime already initialized.');
89
- }
90
- const runtime = getLoadedRuntime(runtimeId);
91
- if (!runtime) {
92
- throw new Error('Invalid PHP runtime id.');
93
- }
94
- this[__private__dont__use] = runtime;
95
-
96
- this.#wasmErrorsTarget = improveWASMErrorReporting(runtime);
97
- }
98
-
99
- /** @inheritDoc */
100
- setPhpIniPath(path: string) {
101
- if (this.#webSapiInitialized) {
102
- throw new Error('Cannot set PHP ini path after calling run().');
103
- }
104
- this[__private__dont__use].ccall(
105
- 'wasm_set_phpini_path',
106
- null,
107
- ['string'],
108
- [path]
109
- );
110
- }
111
-
112
- /** @inheritDoc */
113
- setPhpIniEntry(key: string, value: string) {
114
- if (this.#webSapiInitialized) {
115
- throw new Error('Cannot set PHP ini entries after calling run().');
116
- }
117
- this.#phpIniOverrides.push([key, value]);
118
- }
119
-
120
- /** @inheritDoc */
121
- chdir(path: string) {
122
- this[__private__dont__use].FS.chdir(path);
123
- }
124
-
125
- /** @inheritDoc */
126
- async request(
127
- request: PHPRequest,
128
- maxRedirects?: number
129
- ): Promise<PHPResponse> {
130
- if (!this.requestHandler) {
131
- throw new Error('No request handler available.');
132
- }
133
- return this.requestHandler.request(request, maxRedirects);
134
- }
135
-
136
- /** @inheritDoc */
137
- async run(request: PHPRunOptions): Promise<PHPResponse> {
138
- if (!this.#webSapiInitialized) {
139
- this.#initWebRuntime();
140
- this.#webSapiInitialized = true;
141
- }
142
- this.#setScriptPath(request.scriptPath || '');
143
- this.#setRelativeRequestUri(request.relativeUri || '');
144
- this.#setRequestMethod(request.method || 'GET');
145
- const { host, ...headers } = {
146
- host: 'example.com:443',
147
- ...normalizeHeaders(request.headers || {}),
148
- };
149
- this.#setRequestHostAndProtocol(host, request.protocol || 'http');
150
- this.#setRequestHeaders(headers);
151
- if (request.body) {
152
- this.#setRequestBody(request.body);
153
- }
154
- if (request.fileInfos) {
155
- for (const file of request.fileInfos) {
156
- this.#addUploadedFile(file);
157
- }
158
- }
159
- if (request.code) {
160
- this.#setPHPCode(' ?>' + request.code);
161
- }
162
- return await this.#handleRequest();
163
- }
164
-
165
- #initWebRuntime() {
166
- if (this.#phpIniOverrides.length > 0) {
167
- const overridesAsIni =
168
- this.#phpIniOverrides
169
- .map(([key, value]) => `${key}=${value}`)
170
- .join('\n') + '\n\n';
171
- this[__private__dont__use].ccall(
172
- 'wasm_set_phpini_entries',
173
- null,
174
- [STRING],
175
- [overridesAsIni]
176
- );
177
- }
178
- this[__private__dont__use].ccall('php_wasm_init', null, [], []);
179
- }
180
-
181
- #getResponseHeaders(): {
182
- headers: PHPResponse['headers'];
183
- httpStatusCode: number;
184
- } {
185
- const headersFilePath = '/tmp/headers.json';
186
- if (!this.fileExists(headersFilePath)) {
187
- throw new Error(
188
- 'SAPI Error: Could not find response headers file.'
189
- );
190
- }
191
-
192
- const headersData = JSON.parse(this.readFileAsText(headersFilePath));
193
- const headers: PHPResponse['headers'] = {};
194
- for (const line of headersData.headers) {
195
- if (!line.includes(': ')) {
196
- continue;
197
- }
198
- const colonIndex = line.indexOf(': ');
199
- const headerName = line.substring(0, colonIndex).toLowerCase();
200
- const headerValue = line.substring(colonIndex + 2);
201
- if (!(headerName in headers)) {
202
- headers[headerName] = [] as string[];
203
- }
204
- headers[headerName].push(headerValue);
205
- }
206
- return {
207
- headers,
208
- httpStatusCode: headersData.status,
209
- };
210
- }
211
-
212
- #setRelativeRequestUri(uri: string) {
213
- this[__private__dont__use].ccall(
214
- 'wasm_set_request_uri',
215
- null,
216
- [STRING],
217
- [uri]
218
- );
219
- if (uri.includes('?')) {
220
- const queryString = uri.substring(uri.indexOf('?') + 1);
221
- this[__private__dont__use].ccall(
222
- 'wasm_set_query_string',
223
- null,
224
- [STRING],
225
- [queryString]
226
- );
227
- }
228
- }
229
-
230
- #setRequestHostAndProtocol(host: string, protocol: string) {
231
- this[__private__dont__use].ccall(
232
- 'wasm_set_request_host',
233
- null,
234
- [STRING],
235
- [host]
236
- );
237
-
238
- let port;
239
- try {
240
- port = parseInt(new URL(host).port, 10);
241
- } catch (e) {
242
- // ignore
243
- }
244
-
245
- if (!port || isNaN(port) || port === 80) {
246
- port = protocol === 'https' ? 443 : 80;
247
- }
248
- this[__private__dont__use].ccall(
249
- 'wasm_set_request_port',
250
- null,
251
- [NUMBER],
252
- [port]
253
- );
254
-
255
- if (protocol === 'https' || (!protocol && port === 443)) {
256
- this.addServerGlobalEntry('HTTPS', 'on');
257
- }
258
- }
259
-
260
- #setRequestMethod(method: string) {
261
- this[__private__dont__use].ccall(
262
- 'wasm_set_request_method',
263
- null,
264
- [STRING],
265
- [method]
266
- );
267
- }
268
-
269
- #setRequestHeaders(headers: PHPRequestHeaders) {
270
- if (headers['cookie']) {
271
- this[__private__dont__use].ccall(
272
- 'wasm_set_cookies',
273
- null,
274
- [STRING],
275
- [headers['cookie']]
276
- );
277
- }
278
- if (headers['content-type']) {
279
- this[__private__dont__use].ccall(
280
- 'wasm_set_content_type',
281
- null,
282
- [STRING],
283
- [headers['content-type']]
284
- );
285
- }
286
- if (headers['content-length']) {
287
- this[__private__dont__use].ccall(
288
- 'wasm_set_content_length',
289
- null,
290
- [NUMBER],
291
- [parseInt(headers['content-length'], 10)]
292
- );
293
- }
294
- for (const name in headers) {
295
- this.addServerGlobalEntry(
296
- `HTTP_${name.toUpperCase().replace(/-/g, '_')}`,
297
- headers[name]
298
- );
299
- }
300
- }
301
-
302
- #setRequestBody(body: string) {
303
- this[__private__dont__use].ccall(
304
- 'wasm_set_request_body',
305
- null,
306
- [STRING],
307
- [body]
308
- );
309
- this[__private__dont__use].ccall(
310
- 'wasm_set_content_length',
311
- null,
312
- [NUMBER],
313
- [new TextEncoder().encode(body).length]
314
- );
315
- }
316
-
317
- #setScriptPath(path: string) {
318
- this[__private__dont__use].ccall(
319
- 'wasm_set_path_translated',
320
- null,
321
- [STRING],
322
- [path]
323
- );
324
- }
325
-
326
- addServerGlobalEntry(key: string, value: string) {
327
- this[__private__dont__use].ccall(
328
- 'wasm_add_SERVER_entry',
329
- null,
330
- [STRING, STRING],
331
- [key, value]
332
- );
333
- }
334
-
335
- /**
336
- * Adds file information to $_FILES superglobal in PHP.
337
- *
338
- * In particular:
339
- * * Creates the file data in the filesystem
340
- * * Registers the file details in PHP
341
- *
342
- * @param fileInfo - File details
343
- */
344
- #addUploadedFile(fileInfo: FileInfo) {
345
- const { key, name, type, data } = fileInfo;
346
-
347
- const tmpPath = `/tmp/${Math.random().toFixed(20)}`;
348
- this.writeFile(tmpPath, data);
349
-
350
- const error = 0;
351
- this[__private__dont__use].ccall(
352
- 'wasm_add_uploaded_file',
353
- null,
354
- [STRING, STRING, STRING, STRING, NUMBER, NUMBER],
355
- [key, name, type, tmpPath, error, data.byteLength]
356
- );
357
- }
358
-
359
- #setPHPCode(code: string) {
360
- this[__private__dont__use].ccall(
361
- 'wasm_set_php_code',
362
- null,
363
- [STRING],
364
- [code]
365
- );
366
- }
367
-
368
- async #handleRequest(): Promise<PHPResponse> {
369
- let exitCode: number;
370
-
371
- /*
372
- * Emscripten throws WASM failures outside of the promise chain so we need
373
- * to listen for them here and rethrow in the correct context. Otherwise we
374
- * get crashes and unhandled promise rejections without any useful error messages
375
- * or stack traces.
376
- */
377
- let errorListener: any;
378
- try {
379
- // eslint-disable-next-line no-async-promise-executor
380
- exitCode = await new Promise<number>(async (resolve, reject) => {
381
- errorListener = (e: ErrorEvent) => {
382
- const rethrown = new Error('Rethrown');
383
- rethrown.cause = e.error;
384
- (rethrown as any).betterMessage = e.message;
385
- reject(rethrown);
386
- };
387
- this.#wasmErrorsTarget?.addEventListener(
388
- 'error',
389
- errorListener
390
- );
391
-
392
- try {
393
- resolve(
394
- /**
395
- * This is awkward, but Asyncify makes wasm_sapi_handle_request return
396
- * Promise<Promise<number>>.
397
- *
398
- * @TODO: Determine whether this is a bug in emscripten or in our code.
399
- */
400
- await await this[__private__dont__use].ccall(
401
- 'wasm_sapi_handle_request',
402
- NUMBER,
403
- [],
404
- []
405
- )
406
- );
407
- } catch (e) {
408
- reject(e);
409
- }
410
- });
411
- } catch (e) {
412
- /**
413
- * An exception here means an irrecoverable crash. Let's make
414
- * it very clear to the consumers of this API – every method
415
- * call on this PHP instance will throw an error from now on.
416
- */
417
- for (const name in this) {
418
- if (typeof this[name] === 'function') {
419
- (this as any)[name] = () => {
420
- throw new Error(
421
- `PHP runtime has crashed – see the earlier error for details.`
422
- );
423
- };
424
- }
425
- }
426
- (this as any).functionsMaybeMissingFromAsyncify =
427
- getFunctionsMaybeMissingFromAsyncify();
428
-
429
- const err = e as Error;
430
- const message = (
431
- 'betterMessage' in err ? err.betterMessage : err.message
432
- ) as string;
433
- const rethrown = new Error(message);
434
- rethrown.cause = err;
435
- throw rethrown;
436
- } finally {
437
- this.#wasmErrorsTarget?.removeEventListener('error', errorListener);
438
- }
439
-
440
- const { headers, httpStatusCode } = this.#getResponseHeaders();
441
- return new PHPResponse(
442
- httpStatusCode,
443
- headers,
444
- this.readFileAsBuffer('/tmp/stdout'),
445
- this.readFileAsText('/tmp/stderr'),
446
- exitCode
447
- );
448
- }
449
-
450
- /** @inheritDoc */
451
- @rethrowFileSystemError('Could not create directory "{path}"')
452
- mkdir(path: string) {
453
- this[__private__dont__use].FS.mkdirTree(path);
454
- }
455
-
456
- /** @inheritDoc */
457
- @rethrowFileSystemError('Could not create directory "{path}"')
458
- mkdirTree(path: string) {
459
- this.mkdir(path);
460
- }
461
-
462
- /** @inheritDoc */
463
- @rethrowFileSystemError('Could not read "{path}"')
464
- readFileAsText(path: string) {
465
- return new TextDecoder().decode(this.readFileAsBuffer(path));
466
- }
467
-
468
- /** @inheritDoc */
469
- @rethrowFileSystemError('Could not read "{path}"')
470
- readFileAsBuffer(path: string): Uint8Array {
471
- return this[__private__dont__use].FS.readFile(path);
472
- }
473
-
474
- /** @inheritDoc */
475
- @rethrowFileSystemError('Could not write to "{path}"')
476
- writeFile(path: string, data: string | Uint8Array) {
477
- this[__private__dont__use].FS.writeFile(path, data);
478
- }
479
-
480
- /** @inheritDoc */
481
- @rethrowFileSystemError('Could not unlink "{path}"')
482
- unlink(path: string) {
483
- this[__private__dont__use].FS.unlink(path);
484
- }
485
-
486
- /** @inheritDoc */
487
- @rethrowFileSystemError('Could not move "{path}"')
488
- mv(fromPath: string, toPath: string) {
489
- this[__private__dont__use].FS.mv(fromPath, toPath);
490
- }
491
-
492
- /** @inheritDoc */
493
- @rethrowFileSystemError('Could not remove directory "{path}"')
494
- rmdir(path: string, options: RmDirOptions = { recursive: true }) {
495
- if (options?.recursive) {
496
- this.listFiles(path).forEach((file) => {
497
- const filePath = `${path}/${file}`;
498
- if (this.isDir(filePath)) {
499
- this.rmdir(filePath, options);
500
- } else {
501
- this.unlink(filePath);
502
- }
503
- });
504
- }
505
- this[__private__dont__use].FS.rmdir(path);
506
- }
507
-
508
- /** @inheritDoc */
509
- @rethrowFileSystemError('Could not list files in "{path}"')
510
- listFiles(path: string): string[] {
511
- if (!this.fileExists(path)) {
512
- return [];
513
- }
514
- try {
515
- return this[__private__dont__use].FS.readdir(path).filter(
516
- (name: string) => name !== '.' && name !== '..'
517
- );
518
- } catch (e) {
519
- console.error(e, { path });
520
- return [];
521
- }
522
- }
523
-
524
- /** @inheritDoc */
525
- @rethrowFileSystemError('Could not stat "{path}"')
526
- isDir(path: string): boolean {
527
- if (!this.fileExists(path)) {
528
- return false;
529
- }
530
- return this[__private__dont__use].FS.isDir(
531
- this[__private__dont__use].FS.lookupPath(path).node.mode
532
- );
533
- }
534
-
535
- /** @inheritDoc */
536
- @rethrowFileSystemError('Could not stat "{path}"')
537
- fileExists(path: string): boolean {
538
- try {
539
- this[__private__dont__use].FS.lookupPath(path);
540
- return true;
541
- } catch (e) {
542
- return false;
543
- }
544
- }
545
- }
546
-
547
- export function normalizeHeaders(
548
- headers: PHPRunOptions['headers']
549
- ): PHPRunOptions['headers'] {
550
- const normalized: PHPRunOptions['headers'] = {};
551
- for (const key in headers) {
552
- normalized[key.toLowerCase()] = headers[key];
553
- }
554
- return normalized;
555
- }
@@ -1,50 +0,0 @@
1
- /*
2
- * Node.js Polyfill for ErrorEvent.
3
- */
4
-
5
- const kError = Symbol('error');
6
- const kMessage = Symbol('message');
7
-
8
- interface ErrorEventOptions {
9
- /* The error that generated this event */
10
- error?: Error;
11
- /* The error message */
12
- message?: string;
13
- }
14
- /**
15
- * Class representing an error event.
16
- *
17
- * @extends Event
18
- */
19
- class ErrorEvent2 extends Event {
20
- [kError]: any;
21
- [kMessage]: any;
22
- /**
23
- * Create a new `ErrorEvent`.
24
- *
25
- * @param type The name of the event
26
- * @param options A dictionary object that allows for setting
27
- * attributes via object members of the same name.
28
- */
29
- constructor(type: 'error', options: ErrorEventOptions = {}) {
30
- super(type);
31
-
32
- this[kError] = options.error === undefined ? null : options.error;
33
- this[kMessage] = options.message === undefined ? '' : options.message;
34
- }
35
-
36
- get error() {
37
- return this[kError];
38
- }
39
-
40
- get message() {
41
- return this[kMessage];
42
- }
43
- }
44
- Object.defineProperty(ErrorEvent2.prototype, 'error', { enumerable: true });
45
- Object.defineProperty(ErrorEvent2.prototype, 'message', { enumerable: true });
46
-
47
- export const ErrorEvent =
48
- typeof globalThis.ErrorEvent === 'function'
49
- ? globalThis.ErrorEvent
50
- : ErrorEvent2;
@@ -1,8 +0,0 @@
1
- import { BasePHP } from './base-php';
2
- import { IsomorphicLocalPHP, UniversalPHP } from './universal-php';
3
-
4
- export function isLocalPHP(
5
- playground: UniversalPHP
6
- ): playground is IsomorphicLocalPHP {
7
- return !(playground instanceof BasePHP);
8
- }
@@ -1,8 +0,0 @@
1
- import { isLocalPHP } from './is-local-php';
2
- import { IsomorphicRemotePHP, UniversalPHP } from './universal-php';
3
-
4
- export function isRemotePHP(
5
- playground: UniversalPHP
6
- ): playground is IsomorphicRemotePHP {
7
- return !isLocalPHP(playground);
8
- }