Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/utils/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { getGBKit } from './bridge';
import { warn, debug } from './logger';
import { stripTrailingSlash } from './url';

/**
* Configure AJAX for use without authentication cookies.
Expand All @@ -21,7 +22,7 @@ export function configureAjax() {
window.wp.ajax.settings = window.wp.ajax.settings || {};

const { siteURL: rawSiteURL, authHeader } = getGBKit();
const siteURL = rawSiteURL?.replace( /\/+$/, '' );
const siteURL = stripTrailingSlash( rawSiteURL );
configureAjaxUrl( siteURL );
configureAjaxAuth( siteURL, authHeader );
configureMediaAjax();
Expand Down
5 changes: 3 additions & 2 deletions src/utils/api-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n';
*/
import { getGBKit, POST_FALLBACKS } from './bridge';
import { info, error as logError } from './logger';
import { ensureTrailingSlash, stripTrailingSlash } from './url';

/**
* @typedef {import('@wordpress/api-fetch').APIFetchMiddleware} APIFetchMiddleware
Expand Down Expand Up @@ -85,7 +86,7 @@ function apiPathModifierMiddleware( options, next ) {
// single trailing slash.
options.path = options.path.replace(
/^(?<apiPath>\/?(?:[\w.-]+\/){2})/,
`$<apiPath>${ siteApiNamespace[ 0 ].replace( /\/+$/, '' ) }/`
`$<apiPath>${ ensureTrailingSlash( siteApiNamespace[ 0 ] ) }`
);
}

Expand Down Expand Up @@ -528,7 +529,7 @@ function siteIndexMiddleware( options, next ) {
return next( options );
}

const home = siteURL?.replace( /\/+$/, '' );
const home = stripTrailingSlash( siteURL );
return Promise.resolve( home ? { home } : {} );
}

Expand Down
31 changes: 31 additions & 0 deletions src/utils/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Removes any trailing slashes from a URL or path.
*
* An absent value normalizes to an empty string, so an unset config field can
* be handed over directly. Any other non-string still throws, so a wrong type
* is not quietly passed through.
*
* @param {string} [value] The URL or path to normalize.
* @return {string} The value without a trailing slash.
*/
export function stripTrailingSlash( value ) {
return ( value ?? '' ).replace( /\/+$/, '' );
}

/**
* Normalizes a URL or path to exactly one trailing slash.
*
* An absent value stays absent rather than becoming a lone slash, which would
* resolve requests against the page root. A value that is already just `/` is
* kept, since that is a meaningful root.
*
* @param {string} [value] The URL or path to normalize.
* @return {string} The value with a single trailing slash, or an empty string.
*/
export function ensureTrailingSlash( value ) {
if ( value === undefined || value === null || value === '' ) {
return '';
}

return `${ stripTrailingSlash( value ) }/`;
}
49 changes: 49 additions & 0 deletions src/utils/url.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { describe, it, expect } from 'vitest';

/**
* Internal dependencies
*/
import { ensureTrailingSlash, stripTrailingSlash } from './url';

describe( 'stripTrailingSlash', () => {
it.each( [
[ 'https://example.com/wp-json/', 'https://example.com/wp-json' ],
[ 'https://example.com/wp-json//', 'https://example.com/wp-json' ],
[ 'https://example.com/wp-json', 'https://example.com/wp-json' ],
[ 'sites/123/', 'sites/123' ],
[ '/', '' ],
[ '', '' ],
[ undefined, '' ],
[ null, '' ],
] )( 'normalizes %s to %s', ( value, expected ) => {
expect( stripTrailingSlash( value ) ).toBe( expected );
} );

// An absent value is expected; a wrong type is a caller bug worth surfacing.
it( 'throws on a non-string', () => {
expect( () => stripTrailingSlash( 42 ) ).toThrow( TypeError );
} );
} );

describe( 'ensureTrailingSlash', () => {
it.each( [
[ 'https://example.com/wp-json', 'https://example.com/wp-json/' ],
[ 'https://example.com/wp-json/', 'https://example.com/wp-json/' ],
[ 'https://example.com/wp-json//', 'https://example.com/wp-json/' ],
[ 'sites/123', 'sites/123/' ],
// A lone slash is a meaningful root, unlike an absent value.
[ '/', '/' ],
[ '', '' ],
[ undefined, '' ],
[ null, '' ],
] )( 'normalizes %s to %s', ( value, expected ) => {
expect( ensureTrailingSlash( value ) ).toBe( expected );
} );

it( 'throws on a non-string', () => {
expect( () => ensureTrailingSlash( 42 ) ).toThrow( TypeError );
} );
} );
Loading