@@ -38,6 +38,13 @@ interface BaseConfiguration {
38
38
* Rewrite rules
39
39
*/
40
40
rewriteRules ?: RewriteRule [ ] ;
41
+
42
+ // TODO: Can we reduce coupling between the request handler and WP?
43
+ /**
44
+ * Answers whether the given path is a known remote asset path
45
+ * for a minified WP build.
46
+ */
47
+ isKnownRemoteAssetPath ?: ( wpRelativePath : string ) => boolean ;
41
48
}
42
49
43
50
export type PHPRequestHandlerFactoryArgs = PHPFactoryOptions & {
@@ -137,6 +144,8 @@ export class PHPRequestHandler {
137
144
#cookieStore: HttpCookieStore ;
138
145
rewriteRules : RewriteRule [ ] ;
139
146
processManager : PHPProcessManager ;
147
+ // TODO: Can we reduce coupling between the request handler and WP?
148
+ isKnownRemoteAssetPath : ( wpRelativePath : string ) => boolean = ( ) => false ;
140
149
141
150
/**
142
151
* The request handler needs to decide whether to serve a static asset or
@@ -194,6 +203,10 @@ export class PHPRequestHandler {
194
203
this . #PATHNAME,
195
204
] . join ( '' ) ;
196
205
this . rewriteRules = rewriteRules ;
206
+
207
+ if ( config . isKnownRemoteAssetPath ) {
208
+ this . isKnownRemoteAssetPath = config . isKnownRemoteAssetPath ;
209
+ }
197
210
}
198
211
199
212
async getPrimaryPhp ( ) {
@@ -306,14 +319,89 @@ export class PHPRequestHandler {
306
319
) ,
307
320
this . rewriteRules
308
321
) ;
309
- const fsPath = joinPaths ( this . #DOCROOT, normalizedRequestedPath ) ;
310
- if ( ! seemsLikeAPHPRequestHandlerPath ( fsPath ) ) {
311
- return this . #serveStaticFile(
312
- await this . processManager . getPrimaryPhp ( ) ,
313
- fsPath
314
- ) ;
322
+
323
+ const primaryPhp = await this . getPrimaryPhp ( ) ;
324
+
325
+ let fsPath = joinPaths ( this . #DOCROOT, normalizedRequestedPath ) ;
326
+
327
+ if ( primaryPhp . isDir ( fsPath ) ) {
328
+ // Ensure directory URIs have a trailing slash. Otherwise,
329
+ // relative URIs in index.php or index.html files are relative
330
+ // to the next directory up.
331
+ //
332
+ // Example:
333
+ // For a request to "/wp-admin", the relative link "edit.php"
334
+ // resolves to "/edit.php" rather than "/wp-admin/edit.php".
335
+ //
336
+ // This is correct behavior for the browser:
337
+ // https://www.rfc-editor.org/rfc/rfc3986#section-5.2.3
338
+ //
339
+ // But the intent for `/wp-admin/index.php` is that its relative
340
+ // URIs are relative to `/wp-admin/`.
341
+ //
342
+ // In fact, WordPress also redirects like this when given a chance.
343
+ // - https://github.com/WordPress/wordpress-develop/blob/b6a3b9c7d1ce33cbeca6f95871a26c48141e524b/src/wp-includes/canonical.php#L696
344
+ // - https://github.com/WordPress/wordpress-develop/blob/b6a3b9c7d1ce33cbeca6f95871a26c48141e524b/src/wp-includes/canonical.php#L1036-L1045
345
+ // - https://github.com/WordPress/wordpress-develop/blob/b6a3b9c7d1ce33cbeca6f95871a26c48141e524b/src/wp-includes/link-template.php#L3558
346
+ if ( ! fsPath . endsWith ( '/' ) ) {
347
+ return new PHPResponse (
348
+ 301 ,
349
+ { Location : [ `${ requestedUrl . pathname } /` ] } ,
350
+ new Uint8Array ( 0 )
351
+ ) ;
352
+ }
353
+
354
+ // We can only satisfy requests for directories with a default file
355
+ // so let's first resolve to a default path when available.
356
+ const defaultFilePath = [ 'index.php' , 'index.html' ]
357
+ . map ( ( defaultFilename ) => joinPaths ( fsPath , defaultFilename ) )
358
+ . find ( ( possibleDefaultPath ) =>
359
+ primaryPhp . isFile ( possibleDefaultPath )
360
+ ) ;
361
+
362
+ if ( defaultFilePath ) {
363
+ fsPath = defaultFilePath ;
364
+ }
365
+ }
366
+
367
+ if ( fsPath . endsWith ( '.php' ) ) {
368
+ if ( primaryPhp . isFile ( fsPath ) ) {
369
+ const effectiveRequest : PHPRequest = {
370
+ ...request ,
371
+ url : joinPaths ( this . #ABSOLUTE_URL, fsPath ) ,
372
+ } ;
373
+ return this . #spawnPHPAndDispatchRequest(
374
+ effectiveRequest ,
375
+ requestedUrl
376
+ ) ;
377
+ }
378
+ } else {
379
+ if ( primaryPhp . isFile ( fsPath ) ) {
380
+ return this . #serveStaticFile( primaryPhp , fsPath ) ;
381
+ } else if (
382
+ // Make sure fsPath doesn't describe any other entity on the filesystem
383
+ ! primaryPhp . fileExists ( fsPath ) &&
384
+ this . isKnownRemoteAssetPath ( normalizedRequestedPath )
385
+ ) {
386
+ // This path is listed as a remote asset. Mark it as a static file
387
+ // so the service worker knows it can issue a real fetch() to the server.
388
+ return new PHPResponse (
389
+ 404 ,
390
+ { 'x-file-type' : [ 'static' ] } ,
391
+ new TextEncoder ( ) . encode ( '404 File not found' )
392
+ ) ;
393
+ }
315
394
}
316
- return this . #spawnPHPAndDispatchRequest( request , requestedUrl ) ;
395
+
396
+ // TODO: Can we adjust this default to reduce coupling between the request handler and WP?
397
+ // Delegate unresolved requests to WordPress. This makes WP magic possible,
398
+ // like pretty permalinks and dynamically generated sitemaps.
399
+ const wpDefaultPath = joinPaths ( this . #DOCROOT, 'index.php' ) ;
400
+ const effectiveRequest : PHPRequest = {
401
+ ...request ,
402
+ url : joinPaths ( this . #ABSOLUTE_URL, wpDefaultPath ) ,
403
+ } ;
404
+ return this . #spawnPHPAndDispatchRequest( effectiveRequest , requestedUrl ) ;
317
405
}
318
406
319
407
/**
@@ -323,17 +411,6 @@ export class PHPRequestHandler {
323
411
* @returns The response.
324
412
*/
325
413
#serveStaticFile( php : PHP , fsPath : string ) : PHPResponse {
326
- if ( ! php . fileExists ( fsPath ) ) {
327
- return new PHPResponse (
328
- 404 ,
329
- // Let the service worker know that no static file was found
330
- // and that it's okay to issue a real fetch() to the server.
331
- {
332
- 'x-file-type' : [ 'static' ] ,
333
- } ,
334
- new TextEncoder ( ) . encode ( '404 File not found' )
335
- ) ;
336
- }
337
414
const arrayBuffer = php . readFileAsBuffer ( fsPath ) ;
338
415
return new PHPResponse (
339
416
200 ,
@@ -503,35 +580,6 @@ function inferMimeType(path: string): string {
503
580
return mimeTypes [ extension ] || mimeTypes [ '_default' ] ;
504
581
}
505
582
506
- /**
507
- * Guesses whether the given path looks like a PHP file.
508
- *
509
- * @example
510
- * ```js
511
- * seemsLikeAPHPRequestHandlerPath('/index.php') // true
512
- * seemsLikeAPHPRequestHandlerPath('/index.php') // true
513
- * seemsLikeAPHPRequestHandlerPath('/index.php/foo/bar') // true
514
- * seemsLikeAPHPRequestHandlerPath('/index.html') // false
515
- * seemsLikeAPHPRequestHandlerPath('/index.html/foo/bar') // false
516
- * seemsLikeAPHPRequestHandlerPath('/') // true
517
- * ```
518
- *
519
- * @param path The path to check.
520
- * @returns Whether the path seems like a PHP server path.
521
- */
522
- export function seemsLikeAPHPRequestHandlerPath ( path : string ) : boolean {
523
- return seemsLikeAPHPFile ( path ) || seemsLikeADirectoryRoot ( path ) ;
524
- }
525
-
526
- function seemsLikeAPHPFile ( path : string ) {
527
- return path . endsWith ( '.php' ) || path . includes ( '.php/' ) ;
528
- }
529
-
530
- function seemsLikeADirectoryRoot ( path : string ) {
531
- const lastSegment = path . split ( '/' ) . pop ( ) ;
532
- return ! lastSegment ! . includes ( '.' ) ;
533
- }
534
-
535
583
/**
536
584
* Applies the given rewrite rules to the given path.
537
585
*
0 commit comments