register_routes(); ( new ConfigController() )->register_routes(); ( new SeoController() )->register_routes(); ( new MediaAltController() )->register_routes(); ( new SyncController() )->register_routes(); ( new SyncRunController() )->register_routes(); } /** * @param bool $is_rest WooCommerce's own verdict (wc/ and wc- routes). */ public static function include_cortex_namespace( $is_rest ): bool { if ( $is_rest ) { return true; } $route = self::requested_route(); return '/' . self::NAMESPACE === $route || 0 === strpos( $route, '/' . self::NAMESPACE . '/' ); } /** * The route WordPress will actually dispatch, anchored rather than substring-matched. * * A naive `strpos()` over the whole `REQUEST_URI` — query string included — let an attacker put * `wp-json/cortex/v1/` in a query parameter of an unrelated route (e.g. * `/wp-json/wp/v2/users?x=/wp-json/cortex/v1/`) and have WooCommerce's consumer-key auth apply to *that* * route instead, authenticating the backend's read_write key as its admin owner on every REST namespace. * This instead mirrors `WP::parse_request()`'s own precedence for picking the route: `$_POST['rest_route']` * wins over `$_GET['rest_route']`, which wins over the parsed path (that is what a plain-permalinks or * `?rest_route=`/POSTed-`rest_route` request actually dispatches on). Whichever of `$_POST`/`$_GET` is set * must be a string or this fails closed to `""` — WooCommerce's own `route_from_request_uri()` does the * same for e.g. `rest_route[]=`, which PHP turns into an array WordPress itself would refuse to route on. * Only once neither superglobal is set does the URL *path* get considered — never the query string — with * the site's own subdirectory and a front-controller `index.php` segment stripped before the `wp-json/` * prefix is required. * * @return string The route with a leading slash (e.g. "/cortex/v1/config"), or "" when none is found. * * Also used by KeyScope to limit the CortexCommerce key at login time. */ public static function requested_route(): string { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only route detection, not a state-changing action; WP::parse_request() gives POST precedence over GET and the parsed path, so both must be inspected here too. Validated in rest_route_override(), which is why the sanitization sniffs don't need to see this line. $route = self::rest_route_override( $_POST ); if ( null === $route ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only route detection, not a state-changing action. Validated in rest_route_override(). $route = self::rest_route_override( $_GET ); } if ( null !== $route ) { return $route; } if ( empty( $_SERVER['REQUEST_URI'] ) ) { return ''; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() would rewrite the raw path this needs to anchor against; validated below against the home path and the REST prefix instead of escaped. $path = (string) wp_parse_url( wp_unslash( $_SERVER['REQUEST_URI'] ), PHP_URL_PATH ); $home_path = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ); if ( '' !== $home_path && '/' !== $home_path && 0 === strpos( $path, $home_path ) ) { $path = substr( $path, strlen( $home_path ) - 1 ); } if ( 0 === strpos( $path, '/index.php/' ) ) { $path = substr( $path, strlen( '/index.php' ) ); } $prefix = '/' . rest_get_url_prefix() . '/'; if ( 0 !== strpos( $path, $prefix ) ) { return ''; } return '/' . substr( $path, strlen( $prefix ) ); } /** * @param array $superglobal $_POST or $_GET. * @return string|null Null when rest_route isn't set at all; "" when it's set but not a string (fail closed, * matching WooCommerce's own route_from_request_uri() for e.g. `rest_route[]=`). */ private static function rest_route_override( array $superglobal ): ?string { if ( ! isset( $superglobal['rest_route'] ) ) { return null; } if ( ! is_string( $superglobal['rest_route'] ) ) { return ''; } // The value is only ever compared against our own namespace below, never output or acted on; there is no sanitizer for "a route". $route = wp_unslash( $superglobal['rest_route'] ); return '/' === substr( $route, 0, 1 ) ? $route : '/' . $route; } /** * permission_callback for every authenticated route. * * @return true|\WP_Error */ public static function require_manage_woocommerce() { if ( current_user_can( 'manage_woocommerce' ) ) { return true; } return new \WP_Error( 'cortexcommerce_rest_forbidden', __( 'Sorry, you are not allowed to do that.', 'cortexcommerce' ), array( 'status' => rest_authorization_required_code() ) ); } }