Commit 3cd282a541 for wordpress.org

commit 3cd282a5410057e1157aef5e79715508211dd6e1
Author: westonruter <westonruter@git.wordpress.org>
Date:   Fri Sep 18 01:46:50 2026 +0000

    REST API: Split comma-separated methods in array form.

    `WP_REST_Server::register_route()` accepts `methods` as either a string or an array, but only the string form was split on commas. Any array element containing one became a single unmatchable method key, so `array( WP_REST_Server::READABLE, WP_REST_Server::EDITABLE )` registered the literal key `'POST, PUT, PATCH'` and `POST`, `PUT` and `PATCH` all returned 404, with no notice at registration time. The same route written as a string worked correctly.

    The route map is also documented for static analysis. The return value of `WP_REST_Server::get_routes()` was described as `array( $callback, $bitmask )`, which has never been accurate: there is no bitmask, the value is a list of handler arrays, and the route options are moved out to `WP_REST_Server::get_route_options()`. The corrected description is paired with `@phpstan-type` aliases for a handler and for an endpoint argument, so callers are no longer given `mixed` from the first offset onwards.

    Developed in https://github.com/WordPress/wordpress-develop/pull/13136.
    Follow-up to r34928.

    Props moonmeister, westonruter.
    See #43744, #65616, #65817.
    Fixes #65905.

    Built from https://develop.svn.wordpress.org/trunk@63756


    git-svn-id: http://core.svn.wordpress.org/trunk@62928 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/rest-api/class-wp-rest-server.php b/wp-includes/rest-api/class-wp-rest-server.php
index 7a40bd9b20..426f20d41d 100644
--- a/wp-includes/rest-api/class-wp-rest-server.php
+++ b/wp-includes/rest-api/class-wp-rest-server.php
@@ -10,7 +10,35 @@
 /**
  * Core class used to implement the WordPress REST API server.
  *
+ * The aliases below describe a route handler as {@see WP_REST_Server::get_routes()}
+ * returns it, once the defaults have been filled in and the methods normalized.
+ *
+ * An endpoint argument is a JSON Schema fragment, so the keys named here are only the
+ * ones WordPress reads itself. The rest are schema keywords, left to the open end of
+ * the shape and validated by {@see rest_validate_value_from_schema()}. See
+ * {@see rest_get_allowed_schema_keywords()} for the vocabulary the REST API exposes.
+ *
  * @since 4.4.0
+ *
+ * @phpstan-type Endpoint_Arg array{
+ *     required?: bool,
+ *     default?: mixed,
+ *     type?: string|list<string>,
+ *     validate_callback?: callable|false|null,
+ *     sanitize_callback?: callable|false|null,
+ *     ...
+ * }
+ * @phpstan-type Route_Handler array{
+ *     methods: array<uppercase-string, true>,
+ *     callback?: callable|null,
+ *     permission_callback?: callable|null,
+ *     args: array<non-empty-string, Endpoint_Arg>,
+ *     accept_json: bool,
+ *     accept_raw: bool,
+ *     show_in_index: bool,
+ *     allow_batch?: array{v1?: bool}|false,
+ *     ...
+ * }
  */
 #[AllowDynamicProperties]
 class WP_REST_Server {
@@ -58,16 +86,28 @@ class WP_REST_Server {
 	/**
 	 * Namespaces registered to the server.
 	 *
+	 * Keyed by namespace, each value being the set of path regexes registered in it,
+	 * held as keys mapped to true so that a route cannot be registered twice.
+	 *
 	 * @since 4.4.0
 	 * @var array
+	 *
+	 * @phpstan-var array<string, array<non-empty-string, true>>
 	 */
 	protected $namespaces = array();

 	/**
 	 * Endpoints registered to the server.
 	 *
+	 * Keyed by path regex. Each value is a route exactly as it was registered, before
+	 * {@see WP_REST_Server::get_routes()} normalizes it: either a single endpoint's
+	 * arguments, or an array of them under numeric keys alongside the route's options
+	 * under their own non-numeric keys.
+	 *
 	 * @since 4.4.0
 	 * @var array
+	 *
+	 * @phpstan-var array<non-empty-string, array<array-key, mixed>>
 	 */
 	protected $endpoints = array();

@@ -76,6 +116,8 @@ class WP_REST_Server {
 	 *
 	 * @since 4.4.0
 	 * @var array
+	 *
+	 * @phpstan-var array<non-empty-string, array<string, mixed>>
 	 */
 	protected $route_options = array();

@@ -894,6 +936,8 @@ class WP_REST_Server {
 	 * @param array  $route_args      Route arguments.
 	 * @param bool   $override        Optional. Whether the route should be overridden if it already exists.
 	 *                                Default false.
+	 *
+	 * @phpstan-param non-empty-string $route
 	 */
 	public function register_route( $route_namespace, $route, $route_args, $override = false ) {
 		if ( ! isset( $this->namespaces[ $route_namespace ] ) ) {
@@ -934,14 +978,18 @@ class WP_REST_Server {
 	/**
 	 * Retrieves the route map.
 	 *
-	 * The route map is an associative array with path regexes as the keys. The
-	 * value is an indexed array with the callback function/method as the first
-	 * item, and a bitmask of HTTP methods as the second item (see the class
-	 * constants).
+	 * The route map is an associative array with path regexes as the keys. The value
+	 * is an indexed array of the handlers registered for that route, each of them an
+	 * associative array of endpoint arguments: the callback, its permission callback,
+	 * the arguments it accepts, and the HTTP methods it responds to as a map of method
+	 * name to true.
 	 *
-	 * Each route can be mapped to more than one callback by using an array of
-	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
-	 * and POST requests to another.
+	 * Each route can be mapped to more than one handler. This allows mapping e.g. GET
+	 * requests to one callback and POST requests to another.
+	 *
+	 * Route options, the non-numeric keys a route is registered with such as `schema`
+	 * and `namespace`, are not returned here. They are moved to
+	 * {@see WP_REST_Server::get_route_options()}.
 	 *
 	 * Note that the path regexes (array keys) must have @ escaped, as this is
 	 * used as the delimiter with preg_match()
@@ -950,8 +998,10 @@ class WP_REST_Server {
 	 * @since 5.4.0 Added `$route_namespace` parameter.
 	 *
 	 * @param string $route_namespace Optionally, only return routes in the given namespace.
-	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
-	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
+	 * @return array Route map as `'/path/regex' => array( $handler, ... )`, where each
+	 *               `$handler` is an array of endpoint arguments.
+	 *
+	 * @phpstan-return array<non-empty-string, array<int, Route_Handler>>
 	 */
 	public function get_routes( $route_namespace = '' ) {
 		$endpoints = $this->endpoints;
@@ -965,10 +1015,12 @@ class WP_REST_Server {
 		 *
 		 * @since 4.4.0
 		 *
-		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
-		 *                         to an array of callbacks for the endpoint. These take the format
-		 *                         `'/path/regex' => array( $callback, $bitmask )` or
-		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
+		 * @param array $endpoints The available endpoints, as registered and before normalization.
+		 *                         An array of matching regex patterns, each mapped either to a single
+		 *                         endpoint's arguments or to an array of them, alongside any route
+		 *                         options under their own non-numeric keys.
+		 *
+		 * @phpstan-param array<non-empty-string, array<array-key, mixed>> $endpoints
 		 */
 		$endpoints = apply_filters( 'rest_endpoints', $endpoints );

@@ -1007,7 +1059,15 @@ class WP_REST_Server {
 				if ( is_string( $handler['methods'] ) ) {
 					$methods = explode( ',', $handler['methods'] );
 				} elseif ( is_array( $handler['methods'] ) ) {
-					$methods = $handler['methods'];
+					$methods = array();
+
+					/*
+					 * Array values may themselves be comma-separated, either written that way or
+					 * because they are a multi-method constant such as WP_REST_Server::EDITABLE.
+					 */
+					foreach ( array_filter( $handler['methods'], 'is_string' ) as $method ) {
+						$methods = array_merge( $methods, explode( ',', $method ) );
+					}
 				} else {
 					$methods = array();
 				}
@@ -1021,6 +1081,12 @@ class WP_REST_Server {
 			}
 		}

+		/*
+		 * The loop above is what turns each registered handler into the documented shape,
+		 * but it does so by reference, which static analysis cannot follow.
+		 */
+		/** @phpstan-var array<non-empty-string, array<int, Route_Handler>> $endpoints */
+
 		return $endpoints;
 	}

diff --git a/wp-includes/version.php b/wp-includes/version.php
index e18090b339..c212ac43f5 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-63755';
+$wp_version = '7.2-alpha-63756';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.