Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c088dd7
allow pretty permalinks to be handled for rest endpoints by default
jazzsequence Apr 22, 2025
710b50c
DEVREL-29: handle pretty permalinks for rest endpoints by default (#182)
jazzsequence Apr 22, 2025
05feec7
DEVREL-29: Update tests for handling plain permalinks (#182)
jazzsequence Apr 22, 2025
ef77ab2
remove todo
jazzsequence May 6, 2025
40df9d1
remove commented-out code
jazzsequence May 6, 2025
5c44fc5
Merge branch 'devrel-29-fix-subdir-wpms-url-issue-2' of github.com:pa…
jazzsequence May 6, 2025
7c6f344
Merge branch 'default' into devrel-29-fix-subdir-wpms-url-issue-2
jazzsequence May 6, 2025
38d26a5
fix bats test
jazzsequence May 6, 2025
9f64264
return early
jazzsequence May 6, 2025
5a9df6d
remove redundant else
jazzsequence May 6, 2025
3dafef0
fix spacing
jazzsequence May 6, 2025
25d993f
add helper functions
jazzsequence May 7, 2025
7a0418e
add teardown
jazzsequence May 7, 2025
1d159f2
use helper functions
jazzsequence May 7, 2025
9667586
add a test that checks the json body of the api endpoint for hello-wo…
jazzsequence May 7, 2025
34307e8
change delete site step to only run on success
jazzsequence May 7, 2025
7f71978
remove the hard failure for mixed PRs
jazzsequence May 7, 2025
6e6f5de
add a manual delete test sites workflow
jazzsequence May 7, 2025
b73befd
fix path to bats test
jazzsequence May 7, 2025
0f1a143
we can't actually delete the subdomain test site
jazzsequence May 7, 2025
8677a46
use bats-assert
jazzsequence May 7, 2025
d061302
log, but don't display errors
jazzsequence May 7, 2025
1f08945
fix path to bats-assert
jazzsequence May 7, 2025
2e8cb9d
load bats-support
jazzsequence May 7, 2025
21c356e
bats-assert and bats-support should be installed by the action
jazzsequence May 7, 2025
286ca44
use bats-action standard way of loading libraries
jazzsequence May 7, 2025
af5527c
remove export for BATS_LIB_PATH
jazzsequence May 7, 2025
79cec77
don't use bats_load_library
jazzsequence May 7, 2025
891ed5d
set the lib path in the action env
jazzsequence May 7, 2025
36a8463
move the BATS_LIB_PATH into the run command
jazzsequence May 7, 2025
593d190
try exporting locally again
jazzsequence May 7, 2025
bdfe05a
filter out terminus noise from wp commands
jazzsequence May 7, 2025
58780a7
use partial assertion for home url path
jazzsequence May 7, 2025
b2cd0a3
setup permalinks always
jazzsequence May 7, 2025
185dde3
run a full env:clear-cache and wait before moving on
jazzsequence May 7, 2025
8bb2f21
ensure permalinks are pretty for playwright tests
jazzsequence May 7, 2025
f6c7fa4
fix terminus site name
jazzsequence May 7, 2025
208d41c
fix workflow:wait
jazzsequence May 7, 2025
8c17c92
need site env and move wait
jazzsequence May 7, 2025
8f10153
fix tests
jazzsequence May 7, 2025
a3c7444
standardize vars
jazzsequence May 7, 2025
66deb3b
don't require wp-cli commands for post id and url
jazzsequence May 7, 2025
10f13ab
just remove clear-cache
jazzsequence May 7, 2025
f6925be
remove trap line to test
jazzsequence May 7, 2025
42d41b3
change assert_match to assert_success
jazzsequence May 7, 2025
0a3665e
click the continue button if the continue button exists
jazzsequence May 7, 2025
e78fe30
try a different means of clicking the button
jazzsequence May 7, 2025
f086bd2
adjust tests for more robust single/multisite handling
jazzsequence May 7, 2025
53bfdb7
update service-level to plan
jazzsequence May 7, 2025
0100597
set the plan to a real plan
jazzsequence May 7, 2025
57acb56
set the plan to free before delete
jazzsequence May 7, 2025
0b46728
update changelog
jazzsequence May 7, 2025
844687e
dont run tests when delete test sites workflow is updated
jazzsequence May 7, 2025
bb98ef9
downgrade sites before delete
jazzsequence May 7, 2025
5ee4a1c
bypass interstertial page via http headers
jazzsequence May 8, 2025
8da04ff
add || true to plan:set command
jazzsequence May 8, 2025
2904fc3
test php 8.4 too
jazzsequence May 8, 2025
d52081b
better handling of check commits step
jazzsequence May 8, 2025
e49c94c
style
jazzsequence May 8, 2025
01be302
ensure the comment only appears once
jazzsequence May 8, 2025
a86c250
remove backticks
jazzsequence May 8, 2025
4e672ee
but maybe it should be a blockquote
jazzsequence May 8, 2025
ae94198
combine multisite checks
jazzsequence May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/tests/2-rest-url-fix.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bats

# wp wrapper function
_wp() {
terminus wp -- ${SITE_ID}.dev "$@"
}

# Helper function to get REST URL via WP-CLI
get_rest_url() {
_wp eval 'echo get_rest_url();'
}

# Helper function to get home_url path via WP-CLI
get_home_url_path() {
_wp eval 'echo rtrim(parse_url(home_url(), PHP_URL_PATH) ?: "", "/");'
}

setup_suite() {
# Ensure WP is installed and we are in the right directory
_wp core is-installed || (echo "WordPress not installed. Run setup script first." && exit 1)
}

@test "Check REST URL with default (pretty) permalinks (after setup script flush)" {
run get_rest_url
assert_success
# Default setup script sets /%postname%/ and flushes.
# Expecting /wp/wp-json/ because home_url path should be /wp
assert_output --partial "/wp/wp-json/"
}

@test "Check REST URL with plain permalinks" {
# Set plain permalinks and flush
_wp option update permalink_structure '' --quiet
_wp rewrite flush --hard --quiet
run get_rest_url
assert_success
# With plain permalinks, expect ?rest_route= based on home_url
# Check if it contains the problematic /wp-json/wp/ segment (it shouldn't)
refute_output --partial "/wp-json/wp/"
# Check if it contains the expected ?rest_route=
assert_output --partial "?rest_route=/"

# Restore pretty permalinks for subsequent tests
_wp option update permalink_structure '/%postname%/' --quiet
_wp rewrite flush --hard --quiet
}

@test "Check REST URL with pretty permalinks *before* flush (Simulates new site)" {
# Set pretty permalinks *without* flushing
_wp option update permalink_structure '/%postname%/' --quiet
# DO NOT FLUSH HERE

# Check home_url path to confirm /wp setup
run get_home_url_path
assert_success
assert_output "/wp"

# Now check get_rest_url() - this is where the original issue might occur
run get_rest_url
assert_success
# Assert that the output *should* be the correct /wp/wp-json/ even before flush,
# assuming the fix (either integrated or separate filter) is in place.
# If the fix is NOT in place, this might output /wp-json/ and fail.
# If the plain permalink fix was active, it might output /wp/wp-json/wp/ and fail.
assert_output --partial "/wp/wp-json/"
refute_output --partial "/wp-json/wp/" # Ensure the bad structure isn't present

# Clean up: Flush permalinks
_wp rewrite flush --hard --quiet
}

@test "Access pretty REST API path directly with plain permalinks active" {
# Set plain permalinks and flush
_wp option update permalink_structure '' --quiet
_wp rewrite flush --hard --quiet

# Get the full home URL to construct the test URL
SITE_URL=$( _wp option get home )
# Construct the pretty-style REST API URL
# Note: home_url() includes /wp, so we append /wp-json/... directly
TEST_URL="${SITE_URL}/wp-json/wp/v2/posts"

# Make a curl request to the pretty URL
# -s: silent, -o /dev/null: discard body, -w '%{http_code}': output only HTTP code
# -L: follow redirects (we expect NO redirect, so this helps verify)
# We expect a 200 OK if the internal handling works, or maybe 404 if not found,
# but crucially NOT a 301/302 redirect.
run curl -s -o /dev/null -w '%{http_code}' -L "${TEST_URL}"
assert_success
# Assert that the final HTTP status code is 200 (OK)
# If it were redirecting, -L would follow, but the *initial* code wouldn't be 200.
# If the internal handling fails, it might be 404 or other error.
assert_output "200"

# Restore pretty permalinks for subsequent tests
_wp option update permalink_structure '/%postname%/' --quiet
_wp rewrite flush --hard --quiet
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
env:
CI: 1
run: |
bats -p -t .github/tests
bats -p -t .github/tests/1-test-update-php.bats

- name: Create failure status artifact
if: failure()
Expand Down
58 changes: 54 additions & 4 deletions .github/workflows/playwright.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: WordPress Composer Playwright Tests
name: WordPress Composer Tests
on:
pull_request:
paths-ignore:
Expand Down Expand Up @@ -29,13 +29,16 @@ permissions:

jobs:

playwright-single:
test-single:
name: Single site
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Bats
uses: bats-core/bats-action@2.0.0

- name: Wait for status artifacts
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -154,18 +157,31 @@ jobs:
SITE_URL: ${{ env.SITE_URL }}
run: npm run test .github/tests/wpcm.spec.ts

- name: Run Bats tests for URL fixes (Single Site)
env:
SITE_ID: wpcm-playwright-tests
TERMINUS_TOKEN: ${{ secrets.TERMINUS_TOKEN }}
run: |
echo "Running REST URL Bats tests..."
terminus auth:login --machine-token="${TERMINUS_TOKEN}" || echo "Terminus already logged in."
# Corrected path to the test file
bats -p -t .github/tests/rest-url-fix.bats

- name: Delete Site
if: success()
shell: bash
run: terminus site:delete wpcm-playwright-tests -y

playwright-subdir:
test-subdir:
name: Subdirectory multisite
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Bats
uses: bats-core/bats-action@2.0.0

- name: Wait for status artifacts
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -287,18 +303,35 @@ jobs:
echo "Running Playwright tests on WordPress subdirectory subsite"
npm run test .github/tests/wpcm.spec.ts

- name: Run Bats tests for URL fixes
# This step runs *after* the site setup, including the initial permalink flush.
# The Bats test itself handles permalink changes for specific test cases.
# We need to pass the SITE_ID to the Bats test environment so WP-CLI commands work via Terminus.
env:
SITE_ID: wpcm-subdir-playwright-tests
SUBSITE: foo
TERMINUS_TOKEN: ${{ secrets.TERMINUS_TOKEN }}
run: |
echo "Running REST URL Bats tests..."
# Ensure Terminus is logged in if needed by Bats WP-CLI calls
terminus auth:login --machine-token="${TERMINUS_TOKEN}" || echo "Terminus already logged in."
bats -p -t .github/tests/2-rest-url-fix.bats

- name: Delete Site
if: success()
shell: bash
run: terminus site:delete wpcm-subdir-playwright-tests -y

playwright-subdom:
test-subdom:
name: Subdomain multisite
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Bats
uses: bats-core/bats-action@2.0.0

- name: Wait for status artifacts
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -464,3 +497,20 @@ jobs:
SITE_URL: ${{ env.SUBDOMAIN_URL }}
GRAPHQL_ENDPOINT: ${{ env.SUBDOMAIN_URL }}/wp/graphql
run: npm run test .github/tests/wpcm.spec.ts

- name: Run Bats tests for URL fixes (Subdomain)
env:
SITE_ID: wpcm-subdom-playwright-tests
SUBSITE: foo
TERMINUS_TOKEN: ${{ secrets.TERMINUS_TOKEN }}
run: |
echo "Running REST URL Bats tests..."
terminus auth:login --machine-token="${TERMINUS_TOKEN}" || echo "Terminus already logged in."
# Corrected path to the test file
bats -p -t .github/tests/rest-url-fix.bats

- name: Delete Site
# Run always to ensure cleanup
if: always()
shell: bash
run: terminus site:delete wpcm-subdom-playwright-tests -y
148 changes: 147 additions & 1 deletion web/app/mu-plugins/filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: Pantheon WordPress Filters
* Plugin URI: https://github.com/pantheon-systems/wordpress-composer-managed
* Description: Filters for Composer-managed WordPress sites on Pantheon.
* Version: 1.2.2
* Version: 1.2.3
* Author: Pantheon Systems
* Author URI: https://pantheon.io/
* License: MIT License
Expand Down Expand Up @@ -235,3 +235,149 @@ function __rebuild_url_from_parts( array $parts ) : string {
( isset( $parts['fragment'] ) ? str_replace( '/', '', "#{$parts['fragment']}" ) : '' )
);
}

/**
* REST API Plain Permalink Fix
*
* Extracts the REST API endpoint from a potentially malformed path.
* Handles cases like /wp-json/v2/posts or /wp-json/wp/v2/posts.
*
* @since 1.2.3
* @param string $path The URL path component.
* @return string The extracted endpoint (e.g., /v2/posts) or '/'.
*/
function __extract_rest_endpoint( string $path ) : string {
$rest_route = '/'; // Default to base route
$wp_json_pos = strpos( $path, '/wp-json/' );

if ( $wp_json_pos !== false ) {
$extracted_route = substr( $path, $wp_json_pos + strlen( '/wp-json' ) ); // Get everything after /wp-json
// Special case: Handle the originally reported '/wp-json/wp/' malformation
if ( strpos( $extracted_route, 'wp/' ) === 0 ) {
$extracted_route = substr( $extracted_route, strlen( 'wp' ) ); // Remove the extra 'wp'
}
// Ensure the extracted route starts with a slash
if ( ! $extracted_route && $extracted_route[0] !== '/' ) {
$extracted_route = '/' . $extracted_route;
}
$rest_route = $extracted_route ?: '/'; // Use extracted route or default to base
}
return $rest_route;
}

/**
* Builds the correct plain permalink REST URL.
*
* @since 1.2.3
* @param string $endpoint The REST endpoint (e.g., /v2/posts).
* @param string|null $query_str The original query string (or null).
* @param string|null $fragment The original fragment (or null).
* @return string The fully constructed plain permalink REST URL.
*/
function __build_plain_rest_url( string $endpoint, ?string $query_str, ?string $fragment ) : string {
$home_url = home_url(); // Should be https://.../wp
// Ensure endpoint starts with /
$endpoint = '/' . ltrim( $endpoint, '/' );
// Construct the base plain permalink URL
$correct_url = rtrim( $home_url, '/' ) . '/?rest_route=' . $endpoint;

// Append original query parameters (if any, besides rest_route)
if ( !empty( $query_str ) ) {
parse_str( $query_str, $query_params );
unset( $query_params['rest_route'] ); // Ensure no leftover rest_route
if ( ! empty( $query_params ) ) {
// Check if $correct_url already has '?' (it should)
$correct_url .= '&' . http_build_query( $query_params );
}
}
// Append fragment if present
if ( !empty( $fragment ) ) {
$correct_url .= '#' . $fragment;
}

// Use normalization helper if available
if ( function_exists( __NAMESPACE__ . '\\__normalize_wp_url' ) ) {
return __normalize_wp_url( $correct_url );
} else {
return $correct_url; // Return without full normalization as fallback
}
}

/**
* Corrects generated REST API URL when plain permalinks are active but WordPress
* incorrectly generates a pretty-permalink-style path. Forces the URL
* back to the expected ?rest_route= format using helpers.
*
* @since 1.2.3 // TODO: Update version before release
* @param string $url The potentially incorrect REST URL generated by WP.
* @return string The corrected REST URL in plain permalink format.
*/
function filter_force_plain_rest_url_format( string $url ) : string {
$parsed_url = parse_url($url);

// Check if it looks like a pretty permalink URL (has /wp-json/ in path)
// AND lacks the ?rest_route= query parameter.
$has_wp_json_path = isset( $parsed_url['path'] ) && strpos( $parsed_url['path'], '/wp-json/' ) !== false;
$has_rest_route_query = isset( $parsed_url['query'] ) && strpos( $parsed_url['query'], 'rest_route=' ) !== false;

if ( $has_wp_json_path && ! $has_rest_route_query ) {
// It's using a pretty path format when it shouldn't be.
$endpoint = __extract_rest_endpoint( $parsed_url['path'] );
return __build_plain_rest_url( $endpoint, $parsed_url['query'] ?? null, $parsed_url['fragment'] ?? null );
}

// If the URL didn't match the problematic pattern, return it normalized.
return __normalize_wp_url($url);
}

/**
* Handles incoming requests using a pretty REST API path format when plain
* permalinks are active. It sets the correct 'rest_route' query variable
* internally instead of performing an external redirect.
*
* @since 1.2.3
* @param \WP $wp The WP object, passed by reference.
*/
function handle_pretty_rest_request_on_plain_permalinks( \WP &$wp ) {
// Only run if it's not an admin request. Permalink structure checked by the hook caller.
if ( is_admin() ) {
return;
}

// Use REQUEST_URI as it's more reliable for the raw request path before WP parsing.
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
// Get the path part before any query string.
$request_path = strtok($request_uri, '?');

// Define the pretty permalink base path we expect if pretty permalinks *were* active.
$home_url_path = rtrim( parse_url( home_url(), PHP_URL_PATH ) ?: '', '/' ); // e.g., /wp
$pretty_rest_path_base = $home_url_path . '/wp-json/'; // e.g., /wp/wp-json/

// Check if the actual request path starts with this pretty base.
if ( strpos( $request_path, $pretty_rest_path_base ) === 0 ) {
// Extract the endpoint part *after* the base.
$endpoint = substr( $request_path, strlen( $pretty_rest_path_base ) );
// Ensure endpoint starts with a slash, default to base if empty.
$endpoint = '/' . ltrim($endpoint, '/');
// If the result is just '/', set it back to empty string for root endpoint ?rest_route=/
$endpoint = ($endpoint === '/') ? '' : $endpoint;

// Check if rest_route is already set (e.g., from query string), if so, don't overwrite.
// This prevents conflicts if someone manually crafts a URL like /wp/wp-json/posts?rest_route=/users
if ( ! isset( $wp->query_vars['rest_route'] ) ) {
// Directly set the query variable for the REST API.
$wp->query_vars['rest_route'] = $endpoint;

// Optional: Unset other query vars WP might have incorrectly parsed.
// unset($wp->query_vars['pagename'], $wp->query_vars['error']);
}
// No redirect, no exit. Let WP continue processing with the modified query vars.
}
}

// Only add the REST URL *generation* fix and the request handler if plain permalinks are enabled.
if ( ! get_option('permalink_structure') ) {
add_filter('rest_url', __NAMESPACE__ . '\\filter_force_plain_rest_url_format', 10, 1);
// Hook the request handling logic to parse_request. Pass the $wp object by reference.
add_action('parse_request', __NAMESPACE__ . '\\handle_pretty_rest_request_on_plain_permalinks', 1, 1);
}
Loading