Skip to content

Commit c088dd7

Browse files
committed
allow pretty permalinks to be handled for rest endpoints by default
bypass the issue with bad string replacement by just parsing pretty permalinks natively for rest endpoints even when pretty permalinks aren't active
1 parent b8fabe9 commit c088dd7

File tree

1 file changed

+147
-1
lines changed

1 file changed

+147
-1
lines changed

web/app/mu-plugins/filters.php

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: Pantheon WordPress Filters
44
* Plugin URI: https://github.com/pantheon-systems/wordpress-composer-managed
55
* Description: Filters for Composer-managed WordPress sites on Pantheon.
6-
* Version: 1.2.2
6+
* Version: 1.2.3
77
* Author: Pantheon Systems
88
* Author URI: https://pantheon.io/
99
* License: MIT License
@@ -235,3 +235,149 @@ function __rebuild_url_from_parts( array $parts ) : string {
235235
( isset( $parts['fragment'] ) ? str_replace( '/', '', "#{$parts['fragment']}" ) : '' )
236236
);
237237
}
238+
239+
/**
240+
* REST API Plain Permalink Fix
241+
*
242+
* Extracts the REST API endpoint from a potentially malformed path.
243+
* Handles cases like /wp-json/v2/posts or /wp-json/wp/v2/posts.
244+
*
245+
* @since 1.2.3
246+
* @param string $path The URL path component.
247+
* @return string The extracted endpoint (e.g., /v2/posts) or '/'.
248+
*/
249+
function __extract_rest_endpoint( string $path ) : string {
250+
$rest_route = '/'; // Default to base route
251+
$wp_json_pos = strpos( $path, '/wp-json/' );
252+
253+
if ( $wp_json_pos !== false ) {
254+
$extracted_route = substr( $path, $wp_json_pos + strlen( '/wp-json' ) ); // Get everything after /wp-json
255+
// Special case: Handle the originally reported '/wp-json/wp/' malformation
256+
if ( strpos( $extracted_route, 'wp/' ) === 0 ) {
257+
$extracted_route = substr( $extracted_route, strlen( 'wp' ) ); // Remove the extra 'wp'
258+
}
259+
// Ensure the extracted route starts with a slash
260+
if ( ! $extracted_route && $extracted_route[0] !== '/' ) {
261+
$extracted_route = '/' . $extracted_route;
262+
}
263+
$rest_route = $extracted_route ?: '/'; // Use extracted route or default to base
264+
}
265+
return $rest_route;
266+
}
267+
268+
/**
269+
* Builds the correct plain permalink REST URL.
270+
*
271+
* @since 1.2.3
272+
* @param string $endpoint The REST endpoint (e.g., /v2/posts).
273+
* @param string|null $query_str The original query string (or null).
274+
* @param string|null $fragment The original fragment (or null).
275+
* @return string The fully constructed plain permalink REST URL.
276+
*/
277+
function __build_plain_rest_url( string $endpoint, ?string $query_str, ?string $fragment ) : string {
278+
$home_url = home_url(); // Should be https://.../wp
279+
// Ensure endpoint starts with /
280+
$endpoint = '/' . ltrim( $endpoint, '/' );
281+
// Construct the base plain permalink URL
282+
$correct_url = rtrim( $home_url, '/' ) . '/?rest_route=' . $endpoint;
283+
284+
// Append original query parameters (if any, besides rest_route)
285+
if ( !empty( $query_str ) ) {
286+
parse_str( $query_str, $query_params );
287+
unset( $query_params['rest_route'] ); // Ensure no leftover rest_route
288+
if ( ! empty( $query_params ) ) {
289+
// Check if $correct_url already has '?' (it should)
290+
$correct_url .= '&' . http_build_query( $query_params );
291+
}
292+
}
293+
// Append fragment if present
294+
if ( !empty( $fragment ) ) {
295+
$correct_url .= '#' . $fragment;
296+
}
297+
298+
// Use normalization helper if available
299+
if ( function_exists( __NAMESPACE__ . '\\__normalize_wp_url' ) ) {
300+
return __normalize_wp_url( $correct_url );
301+
} else {
302+
return $correct_url; // Return without full normalization as fallback
303+
}
304+
}
305+
306+
/**
307+
* Corrects generated REST API URL when plain permalinks are active but WordPress
308+
* incorrectly generates a pretty-permalink-style path. Forces the URL
309+
* back to the expected ?rest_route= format using helpers.
310+
*
311+
* @since 1.2.3 // TODO: Update version before release
312+
* @param string $url The potentially incorrect REST URL generated by WP.
313+
* @return string The corrected REST URL in plain permalink format.
314+
*/
315+
function filter_force_plain_rest_url_format( string $url ) : string {
316+
$parsed_url = parse_url($url);
317+
318+
// Check if it looks like a pretty permalink URL (has /wp-json/ in path)
319+
// AND lacks the ?rest_route= query parameter.
320+
$has_wp_json_path = isset( $parsed_url['path'] ) && strpos( $parsed_url['path'], '/wp-json/' ) !== false;
321+
$has_rest_route_query = isset( $parsed_url['query'] ) && strpos( $parsed_url['query'], 'rest_route=' ) !== false;
322+
323+
if ( $has_wp_json_path && ! $has_rest_route_query ) {
324+
// It's using a pretty path format when it shouldn't be.
325+
$endpoint = __extract_rest_endpoint( $parsed_url['path'] );
326+
return __build_plain_rest_url( $endpoint, $parsed_url['query'] ?? null, $parsed_url['fragment'] ?? null );
327+
}
328+
329+
// If the URL didn't match the problematic pattern, return it normalized.
330+
return __normalize_wp_url($url);
331+
}
332+
333+
/**
334+
* Handles incoming requests using a pretty REST API path format when plain
335+
* permalinks are active. It sets the correct 'rest_route' query variable
336+
* internally instead of performing an external redirect.
337+
*
338+
* @since 1.2.3
339+
* @param \WP $wp The WP object, passed by reference.
340+
*/
341+
function handle_pretty_rest_request_on_plain_permalinks( \WP &$wp ) {
342+
// Only run if it's not an admin request. Permalink structure checked by the hook caller.
343+
if ( is_admin() ) {
344+
return;
345+
}
346+
347+
// Use REQUEST_URI as it's more reliable for the raw request path before WP parsing.
348+
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
349+
// Get the path part before any query string.
350+
$request_path = strtok($request_uri, '?');
351+
352+
// Define the pretty permalink base path we expect if pretty permalinks *were* active.
353+
$home_url_path = rtrim( parse_url( home_url(), PHP_URL_PATH ) ?: '', '/' ); // e.g., /wp
354+
$pretty_rest_path_base = $home_url_path . '/wp-json/'; // e.g., /wp/wp-json/
355+
356+
// Check if the actual request path starts with this pretty base.
357+
if ( strpos( $request_path, $pretty_rest_path_base ) === 0 ) {
358+
// Extract the endpoint part *after* the base.
359+
$endpoint = substr( $request_path, strlen( $pretty_rest_path_base ) );
360+
// Ensure endpoint starts with a slash, default to base if empty.
361+
$endpoint = '/' . ltrim($endpoint, '/');
362+
// If the result is just '/', set it back to empty string for root endpoint ?rest_route=/
363+
$endpoint = ($endpoint === '/') ? '' : $endpoint;
364+
365+
// Check if rest_route is already set (e.g., from query string), if so, don't overwrite.
366+
// This prevents conflicts if someone manually crafts a URL like /wp/wp-json/posts?rest_route=/users
367+
if ( ! isset( $wp->query_vars['rest_route'] ) ) {
368+
// Directly set the query variable for the REST API.
369+
$wp->query_vars['rest_route'] = $endpoint;
370+
371+
// Optional: Unset other query vars WP might have incorrectly parsed.
372+
// unset($wp->query_vars['pagename'], $wp->query_vars['error']);
373+
}
374+
// No redirect, no exit. Let WP continue processing with the modified query vars.
375+
}
376+
}
377+
378+
// Only add the REST URL *generation* fix and the request handler if plain permalinks are enabled.
379+
if ( ! get_option('permalink_structure') ) {
380+
add_filter('rest_url', __NAMESPACE__ . '\\filter_force_plain_rest_url_format', 10, 1);
381+
// Hook the request handling logic to parse_request. Pass the $wp object by reference.
382+
add_action('parse_request', __NAMESPACE__ . '\\handle_pretty_rest_request_on_plain_permalinks', 1, 1);
383+
}

0 commit comments

Comments
 (0)