Skip to content

Commit 6f750fb

Browse files
committed
Fixes a Composer script error and an issue where WP REST API urls would be broken on newly created sites.
For more information see https://docs.pantheon.io/release-notes/2025/05/wordpress-composer-managed-1-33-0
1 parent 18352c7 commit 6f750fb

File tree

2 files changed

+152
-1
lines changed

2 files changed

+152
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### v1.33.0 (2025-05-08)
2+
* Fixes an issue where we were running `maybe-add-symlinks` but the script didn't exist. ([#183](https://github.com/pantheon-systems/wordpress-composer-managed/pull/183))
3+
* Fixes an issue where WP REST API urls would break on new sites before "pretty permalink" structure was set. ([#186](https://github.com/pantheon-systems/wordpress-composer-managed/pull/186))
4+
15
### v1.32.5 (2025-02-10)
26
* Adds the `maybe-install-symlinks` Composer script to `post-update-cmd` hook. This ensures that symlinks are created (and the `web/index.php` file is re-created) after a `composer update`. ([#175](https://github.com/pantheon-systems/wordpress-composer-managed/pull/175))
37

web/app/mu-plugins/filters.php

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

0 commit comments

Comments
 (0)