Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.11% covered (warning)
86.11%
31 / 36
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_Proxy
91.18% covered (success)
91.18%
31 / 34
75.00% covered (warning)
75.00%
3 / 4
6.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 register_route
85.71% covered (warning)
85.71%
18 / 21
0.00% covered (danger)
0.00%
0 / 1
2.01
 forward
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 with_declared_args_only
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Local REST routes that forward the dashboard's WordPress.com API calls as the current user.
4 *
5 * @package automattic/jetpack-monetize
6 */
7
8namespace Automattic\Jetpack\Monetize;
9
10use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
11use WP_REST_Request;
12
13if ( ! defined( 'ABSPATH' ) ) {
14    exit( 0 );
15}
16
17/**
18 * Registers `jetpack/v4/monetize/<api>/<path>` routes that each forward to the same `<api>/sites/<id>/<path>` on WordPress.com.
19 *
20 * The local path mirrors the upstream one, so the page's client only swaps the prefix: Simple sites call WordPress.com directly.
21 *
22 * @since $$next-version$$
23 */
24class WPCOM_Proxy {
25    use WPCOM_REST_API_Proxy_Request;
26
27    const REST_NAMESPACE = 'jetpack/v4';
28
29    const REST_BASE = 'monetize';
30
31    /**
32     * The WordPress.com APIs a route can forward to, with each one's base path and version.
33     *
34     * @var array<string, string[]>
35     */
36    const APIS = array(
37        'wpcom/v2'  => array( 'wpcom', 'v2' ),
38        'rest/v1.1' => array( 'rest', '1.1' ),
39    );
40
41    /**
42     * Set up a proxy for one upstream path.
43     *
44     * @param string $api           A key of APIS.
45     * @param string $upstream_path Path under `/sites/<id>/`.
46     */
47    private function __construct( $api, $upstream_path ) {
48        list( $this->base_api_path, $this->version ) = self::APIS[ $api ];
49        $this->rest_base                             = $upstream_path;
50    }
51
52    /**
53     * Register a route that forwards to the same path on WordPress.com.
54     *
55     * @param string       $api     A key of APIS.
56     * @param string       $path    Route pattern under `/sites/<id>/`, e.g. `memberships/product/(?P<id>\d+)`.
57     * @param string|array $methods HTTP methods the route accepts.
58     * @param array        $args    Route args: only these and `_locale` are forwarded, once validated and sanitized.
59     * @return bool Whether the route was registered.
60     */
61    public static function register_route( $api, $path, $methods, array $args = array() ) {
62        if ( ! isset( self::APIS[ $api ] ) ) {
63            /* translators: %s is a WordPress.com API, such as wpcom/v2. */
64            $message = sprintf( __( 'Unknown WordPress.com API "%s".', 'jetpack-monetize' ), $api );
65            _doing_it_wrong( __METHOD__, esc_html( $message ), '$$next-version$$' );
66            return false;
67        }
68
69        return register_rest_route(
70            self::REST_NAMESPACE,
71            '/' . self::REST_BASE . '/' . $api . '/' . $path,
72            array(
73                'methods'             => $methods,
74                'callback'            => static function ( WP_REST_Request $request ) use ( $api ) {
75                    return self::forward( $api, $request );
76                },
77                'permission_callback' => array( Dashboard::class, 'can_manage' ),
78                // apiFetch adds `_locale=user` to every request, so WordPress.com answers in the user's language.
79                'args'                => $args + array(
80                    '_locale' => array(
81                        'type' => 'string',
82                        'enum' => array( 'user' ),
83                    ),
84                ),
85            )
86        );
87    }
88
89    /**
90     * Forward a request to its upstream path as the current user.
91     *
92     * @param string          $api     A key of APIS.
93     * @param WP_REST_Request $request The request to one of this class's routes.
94     * @return \WP_REST_Response|\WP_Error
95     */
96    public static function forward( $api, WP_REST_Request $request ) {
97        $prefix   = '/' . self::REST_NAMESPACE . '/' . self::REST_BASE . '/' . $api . '/';
98        $upstream = substr( $request->get_route(), strlen( $prefix ) );
99
100        return rest_ensure_response( ( new self( $api, $upstream ) )->proxy_request_to_wpcom_as_user( self::with_declared_args_only( $request ) ) );
101    }
102
103    /**
104     * A copy of the request that keeps only its route's declared args, with the values WordPress sanitized.
105     *
106     * The body goes out as JSON whatever its incoming encoding, since that is the only body the proxy sends.
107     *
108     * @param WP_REST_Request $request The request to one of this class's routes.
109     * @return WP_REST_Request
110     */
111    private static function with_declared_args_only( WP_REST_Request $request ) {
112        $declared  = $request->get_attributes()['args'] ?? array();
113        $forwarded = new WP_REST_Request( $request->get_method(), $request->get_route() );
114        $forwarded->set_query_params( array_intersect_key( $request->get_query_params(), $declared ) );
115
116        $body = array_intersect_key( array_merge( $request->get_body_params(), (array) $request->get_json_params() ), $declared );
117        if ( $body ) {
118            $forwarded->set_header( 'Content-Type', 'application/json' );
119            $forwarded->set_body( (string) wp_json_encode( $body, JSON_UNESCAPED_SLASHES ) );
120        }
121
122        return $forwarded;
123    }
124}