Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.29% covered (warning)
60.29%
41 / 68
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Pinghub_Token
60.29% covered (warning)
60.29%
41 / 68
40.00% covered (danger)
40.00%
2 / 5
38.28
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_routes
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 create_item_permissions_check
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 create_item
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
4.03
 generate_token
53.33% covered (warning)
53.33%
16 / 30
0.00% covered (danger)
0.00%
0 / 1
14.50
1<?php
2/**
3 * REST API endpoint for PingHub token generation.
4 *
5 * @package automattic/jetpack-rtc
6 */
7
8namespace Automattic\Jetpack\RTC;
9
10use Automattic\Jetpack\RTC;
11use WP_Error;
12use WP_REST_Controller;
13use WP_REST_Request;
14use WP_REST_Response;
15use WP_REST_Server;
16
17/**
18 * REST controller that generates short-lived JWTs for PingHub WebSocket auth.
19 */
20class REST_Pinghub_Token extends WP_REST_Controller {
21
22    /**
23     * Constructor.
24     */
25    public function __construct() {
26        $this->namespace = 'wpcom/v2';
27        $this->rest_base = 'rtc/pinghub-token';
28    }
29
30    /**
31     * Register routes.
32     */
33    public function register_routes() {
34        register_rest_route(
35            $this->namespace,
36            '/' . $this->rest_base,
37            array(
38                array(
39                    'methods'             => WP_REST_Server::CREATABLE,
40                    'callback'            => array( $this, 'create_item' ),
41                    'permission_callback' => array( $this, 'create_item_permissions_check' ),
42                ),
43            )
44        );
45    }
46
47    /**
48     * Permission check: current user must be a member of the blog and able to edit posts.
49     *
50     * @param WP_REST_Request $request Full details about the request.
51     * @return true|WP_Error
52     */
53    public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
54        $providers = RTC::get_providers();
55        if ( ! in_array( 'pinghub', $providers, true ) || ! is_user_member_of_blog() || ! current_user_can( 'edit_posts' ) ) {
56            return new WP_Error(
57                'rest_forbidden',
58                __( 'You are not allowed to access this endpoint.', 'jetpack-rtc' ),
59                array( 'status' => 403 )
60            );
61        }
62
63        return true;
64    }
65
66    /**
67     * Generate a short-lived JWT for authenticating PingHub WebSocket connections.
68     *
69     * On simple WordPress.com sites sign_JWT() is called directly — the REST
70     * endpoint files are not loaded in the admin context so rest_do_request
71     * cannot be used. On Jetpack/Atomic sites it is fetched from the WPCOM API
72     * over the Jetpack connection.
73     *
74     * @param WP_REST_Request $request Full details about the request.
75     * @return WP_REST_Response|WP_Error
76     */
77    public function create_item( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
78        $blog_id = get_wpcom_blog_id();
79
80        if ( ! $blog_id ) {
81            return new WP_Error(
82                'rest_pinghub_token_error',
83                __( 'Could not determine blog ID.', 'jetpack-rtc' ),
84                array( 'status' => 500 )
85            );
86        }
87
88        $token = $this->generate_token( $blog_id );
89
90        if ( is_wp_error( $token ) ) {
91            return $token;
92        }
93
94        if ( $token === null ) {
95            return new WP_Error(
96                'rest_pinghub_token_error',
97                __( 'Could not generate PingHub token.', 'jetpack-rtc' ),
98                array( 'status' => 500 )
99            );
100        }
101
102        return rest_ensure_response( array( 'token' => $token ) );
103    }
104
105    /**
106     * Generate the JWT token for PingHub authentication.
107     *
108     * @param int $blog_id The blog ID.
109     * @return string|WP_Error|null Signed JWT, a WP_Error on a known failure, or null on an unexpected failure.
110     */
111    private function generate_token( $blog_id ) {
112        // Simple WordPress.com sites: use the internal REST endpoint.
113        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
114            $request  = new WP_REST_Request( 'POST', "/wpcom/v2/sites/$blog_id/jetpack-pinghub/jwt/sign" );
115            $response = rest_do_request( $request );
116
117            if ( $response->is_error() ) {
118                return null;
119            }
120
121            $data = $response->get_data();
122            return $data['jwt'] ?? null;
123        }
124
125        // Jetpack/Atomic: call the WPCOM endpoint over the Jetpack connection.
126        if ( ! class_exists( 'Automattic\Jetpack\Connection\Client' ) ) {
127            return null;
128        }
129
130        $is_user_connected = false;
131        if ( class_exists( 'Automattic\Jetpack\Connection\Manager' ) ) {
132            $manager           = new \Automattic\Jetpack\Connection\Manager();
133            $is_user_connected = $manager->is_user_connected( get_current_user_id() );
134        }
135
136        if ( $is_user_connected ) {
137            // Connected user: request with their WP.com user token.
138            $response = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_user(
139                "/sites/$blog_id/jetpack-pinghub/jwt/sign",
140                '2',
141                array( 'method' => 'POST' )
142            );
143        } else {
144            // Local (unconnected) user: request with the blog token, passing
145            // the local user's identity so the WPCOM endpoint can embed it in
146            // the JWT. The permission_callback already verified edit_posts.
147            $response = \Automattic\Jetpack\Connection\Client::wpcom_json_api_request_as_blog(
148                "/sites/$blog_id/jetpack-pinghub/jwt/sign",
149                '2',
150                array( 'method' => 'POST' ),
151                array( 'local_user_id' => get_current_user_id() ),
152                'wpcom'
153            );
154        }
155
156        if ( is_wp_error( $response ) ) {
157            return null;
158        }
159
160        $body = json_decode( wp_remote_retrieve_body( $response ), true );
161        return $body['jwt'] ?? null;
162    }
163}