is_rest_request() ) { return $user_id; } /* * The API's off switch covers this too. * * Only `RestServer` consulted `rest_enabled`, and it consulted it to decide * whether to register routes — so turning the plugin's REST API "off" left * this module resolving bearer tokens for core's routes and every other * plugin's, which is not what that setting says it does and is not what an * administrator switching it off is asking for. */ if ( ! Settings::flag( 'rest_enabled' ) ) { return $user_id; } $token = $this->bearer_token(); if ( '' === $token ) { return $user_id; } /* * With no handler installed the plugin has no opinion about this token. * Another plugin may own it, so it is neither resolved nor rejected. */ if ( ! has_filter( 'formpilot_rest_resolve_token' ) ) { return $user_id; } $this->attempted = true; /** * Filters the user a bearer token belongs to. * * Return a user ID to sign the request in, or 0 to reject the token. A * JWT implementation verifies the signature and expiry here. * * @since 1.0.0 * * @param int $resolved User ID, 0 when the token is not accepted. * @param string $token Raw bearer token. */ $resolved = (int) apply_filters( 'formpilot_rest_resolve_token', 0, $token ); $this->resolved = $resolved; return $resolved > 0 ? $resolved : $user_id; } /** * Rejects a request whose bearer token nobody accepted. * * Without this the request would quietly continue as a logged out visitor, * and the client would see a confusing 403 on a token it believes is valid. * * @param mixed $errors Result of the earlier authentication filters. * * @return mixed The unchanged result, or an error. */ public function authentication_errors( mixed $errors ): mixed { if ( null !== $errors ) { return $errors; } /* * Only speak up about a token this plugin actually tried to resolve. * `rest_authentication_errors` is global: rejecting on the mere presence * of an Authorization header would break every other endpoint on the * site, and would reject a cookie authenticated request that happens to * carry one. */ if ( ! $this->attempted ) { return $errors; } if ( null !== $this->resolved && $this->resolved > 0 ) { return $errors; } // A handler saw the token and refused it: expired, forged or revoked. return new WP_Error( 'formpilot_rest_invalid_token', __( 'The authentication token is missing, expired or invalid.', 'devplugins-custom-form-builder' ), array( 'status' => 401 ) ); } /** * Determines whether the current request targets the REST API. * * `REST_REQUEST` is only defined once the API has taken over, which is later * than the first call to `determine_current_user`, so the request itself is * inspected as well. */ private function is_rest_request(): bool { if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return true; } /* * Only what core itself would route to the API. The looser test signed a * token holder in on `admin-ajax.php?rest_route=/` (a `rest_route` in the * query or even the POST body) and on `admin-ajax.php/wp-json/` (a path * that merely contained the prefix), so every `wp_ajax_*` and * `admin_post_*` handler on the site ran as that user. */ if ( wp_doing_ajax() || is_admin() || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) ) { return false; } $route = Request::raw( 'rest_route', 'get' ); if ( is_scalar( $route ) && '' !== sanitize_text_field( (string) $route ) ) { return true; } if ( ! function_exists( 'rest_get_url_prefix' ) ) { return false; } $path = (string) wp_parse_url( Request::server( 'REQUEST_URI' ), PHP_URL_PATH ); $home = rtrim( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '/' ); return str_starts_with( $path, $home . '/' . trim( rest_get_url_prefix(), '/' ) . '/' ); } /** * Reads the bearer token out of the request. * * Some servers strip the Authorization header, which is why the redirected * copies are checked too. * * @return string The token, or an empty string. */ private function bearer_token(): string { $header = ''; foreach ( array( 'HTTP_AUTHORIZATION', 'REDIRECT_HTTP_AUTHORIZATION' ) as $key ) { $header = Request::server( $key ); if ( '' !== $header ) { break; } } if ( '' === $header && function_exists( 'getallheaders' ) ) { foreach ( (array) getallheaders() as $name => $value ) { if ( 'authorization' === strtolower( (string) $name ) ) { $header = sanitize_text_field( (string) $value ); break; } } } if ( 0 !== stripos( $header, 'bearer ' ) ) { return ''; } return trim( substr( $header, 7 ) ); } }