Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/js/entries/admin-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ const buildRequestUrl = (status: PaginationStatus, page: number): string => {
url.searchParams.set('network', '1')
}

const safeMode = new URLSearchParams(window.location.search).get('snippets-safe-mode')

if (safeMode) {
url.searchParams.set('snippets-safe-mode', safeMode)
}

for (const excluded of config.excludeTypes) {
url.searchParams.append('exclude_types[]', excluded)
}
Expand Down
3 changes: 2 additions & 1 deletion src/js/hooks/useRestAPI.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'
import axios from 'axios'
import { createContextHook } from '../utils/bootstrap'
import { REST_API_AXIOS_CONFIG, applyMethodOverride, applyRestNonce, listenForNonceRefresh } from '../utils/restAPI'
import { REST_API_AXIOS_CONFIG, applyMethodOverride, applyRestNonce, applySafeMode, listenForNonceRefresh } from '../utils/restAPI'
import type { PropsWithChildren } from 'react'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

Expand Down Expand Up @@ -62,6 +62,7 @@ export const WithRestAPIContext: React.FC<PropsWithChildren> = ({ children }) =>
const instance = axios.create(REST_API_AXIOS_CONFIG)
instance.interceptors.request.use(applyRestNonce)
instance.interceptors.request.use(applyMethodOverride)
instance.interceptors.request.use(applySafeMode)
listenForNonceRefresh()
return instance
}, [])
Expand Down
18 changes: 18 additions & 0 deletions src/js/utils/restAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ export const addQueryArg = ({ url, name, value }: QueryArg): string => {
return parsed.toString()
}

/**
* Carry the per-page safe mode flag on to REST requests.
*
* The server cannot add the flag to the localized REST URLs, as with pretty
* permalinks it would bury the route in the query string. Without it, the
* request that deactivates or deletes a broken snippet would still run that
* snippet, and fail with it.
*/
export const applySafeMode = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
const safeMode = new URLSearchParams(window.location.search).get('snippets-safe-mode')

if (safeMode && config.url) {
config.url = addQueryArg({ url: config.url, name: 'snippets-safe-mode', value: safeMode })
}

return config
}

/**
* A WordPress core REST route (`wp/v2/…`) with its query arguments, built so it
* works whether or not the site has pretty permalinks: without them the REST
Expand Down
24 changes: 23 additions & 1 deletion src/php/Integration/Evaluate_Functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function __construct( DB $db ) {
// The capability is checked in the callback instead, which never runs
// before URLs are being generated.
if ( $this->is_safe_mode_query_var_set() ) {
add_filter( 'home_url', [ $this, 'add_safe_mode_query_var' ] );
add_filter( 'home_url', [ $this, 'add_safe_mode_query_var_to_home_url' ], 10, 3 );
add_filter( 'admin_url', [ $this, 'add_safe_mode_query_var' ] );
}
}
Expand Down Expand Up @@ -86,6 +86,28 @@ public function add_safe_mode_query_var( $url ): string {
$url;
}

/**
* Inject the safe mode query var into home URLs, leaving REST API URLs alone.
*
* REST URLs are built on the home URL, so a query string added here would end
* up in front of the route, or of index.php with plain permalinks, and REST
* requests would miss their endpoint. The admin scripts carry safe mode on to
* their REST requests themselves.
*
* @param mixed $url Original URL, from an unknown earlier callback.
* @param mixed $path Path relative to the home URL.
* @param mixed $orig_scheme Scheme requested for the URL, 'rest' when built by rest_url().
*
* @return string Modified URL.
*/
public function add_safe_mode_query_var_to_home_url( $url, $path = '', $orig_scheme = null ): string {
if ( 'rest' === $orig_scheme ) {
return is_string( $url ) ? $url : '';
}

return $this->add_safe_mode_query_var( $url );
}

/**
* Retrieve details about the currently edited snippet, if any.
*
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/Integration/Evaluate_Functions_Safe_Mode_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,59 @@ public function test_constructing_with_the_query_var_set_is_not_fatal(): void {
$this->assertSame( 10, has_filter( 'admin_url', [ $evaluate, 'add_safe_mode_query_var' ] ) );
}

/**
* REST API URLs are left unchanged when safe mode is requested.
*
* The route, or index.php with plain permalinks, is appended to the home URL,
* so a query var added there would end up in front of it and break every request.
*
* @dataProvider data_permalink_structures
*
* @param string $structure Permalink structure.
*
* @return void
*/
public function test_rest_urls_are_unchanged_in_safe_mode( string $structure ): void {
$_REQUEST['snippets-safe-mode'] = '1';
wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );
$this->set_permalink_structure( $structure );
$expected = rest_url( 'code-snippets/v1/snippets/5' );

new Evaluate_Functions( code_snippets()->db );

$this->assertSame( $expected, rest_url( 'code-snippets/v1/snippets/5' ) );
}

/**
* Permalink structures, covering each way rest_url() builds a URL.
*
* @return array<string, array{string}>
*/
public function data_permalink_structures(): array {
return [
'plain permalinks' => [ '' ],
'pretty permalinks' => [ '/%postname%/' ],
'index permalinks' => [ '/index.php/%postname%/' ],
];
}

/**
* Other site and admin URLs still carry the safe mode query var.
*
* @return void
*/
public function test_site_and_admin_urls_carry_the_safe_mode_query_var(): void {
$_REQUEST['snippets-safe-mode'] = '1';
wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );
$this->set_permalink_structure( '/%postname%/' );

new Evaluate_Functions( code_snippets()->db );

$this->assertStringContainsString( 'snippets-safe-mode=1', home_url( '/sample-page/' ) );
$this->assertStringContainsString( 'snippets-safe-mode=1', home_url( '/guides/' . rest_get_url_prefix() . '/help/' ) );
$this->assertStringContainsString( 'snippets-safe-mode=1', admin_url( 'admin.php?page=snippets' ) );
}

/**
* The URL callback tolerates a non-string from an earlier callback.
*
Expand All @@ -75,6 +128,7 @@ public function test_url_callback_tolerates_a_null_from_an_earlier_callback(): v
$evaluate = new Evaluate_Functions( code_snippets()->db );

$this->assertIsString( $evaluate->add_safe_mode_query_var( null ) );
$this->assertIsString( $evaluate->add_safe_mode_query_var_to_home_url( null, null, 'rest' ) );
}

/**
Expand Down