Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
REST_RTC_Notices
100.00% covered (success)
100.00%
84 / 84
100.00% covered (success)
100.00%
6 / 6
13
100.00% covered (success)
100.00%
1 / 1
 register_routes
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
1
 check_edit_post_permission
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 check_admin_edit_post_permission
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 record_join_request
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 get_join_requests
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 clear_join_requests
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * REST API controller for RTC notices.
4 *
5 * Handles:
6 * - Join requests: when a non-admin is blocked by the collaborator limit,
7 *   their browser records a join request so the admin can be notified.
8 *
9 * @package automattic/jetpack-rtc
10 */
11
12namespace Automattic\Jetpack\RTC;
13
14use WP_REST_Controller;
15use WP_REST_Request;
16use WP_REST_Response;
17use WP_REST_Server;
18
19/**
20 * Class REST_RTC_Notices
21 */
22class REST_RTC_Notices extends WP_REST_Controller {
23
24    const JOIN_REQUEST_OPTION = 'rtc_pending_join_requests';
25
26    /**
27     * Register the routes.
28     */
29    public function register_routes() {
30        register_rest_route(
31            'wpcom/v2',
32            '/rtc-notices/join-request',
33            array(
34                'methods'             => WP_REST_Server::CREATABLE,
35                'callback'            => array( $this, 'record_join_request' ),
36                'permission_callback' => array( $this, 'check_edit_post_permission' ),
37                'args'                => array(
38                    'post_id' => array(
39                        'required'          => true,
40                        'type'              => 'integer',
41                        'sanitize_callback' => 'absint',
42                    ),
43                ),
44            )
45        );
46
47        register_rest_route(
48            'wpcom/v2',
49            '/rtc-notices/join-requests',
50            array(
51                'methods'             => WP_REST_Server::READABLE,
52                'callback'            => array( $this, 'get_join_requests' ),
53                'permission_callback' => array( $this, 'check_admin_edit_post_permission' ),
54                'args'                => array(
55                    'post_id' => array(
56                        'required'          => true,
57                        'type'              => 'integer',
58                        'sanitize_callback' => 'absint',
59                    ),
60                ),
61            )
62        );
63
64        register_rest_route(
65            'wpcom/v2',
66            '/rtc-notices/join-requests/clear',
67            array(
68                'methods'             => WP_REST_Server::CREATABLE,
69                'callback'            => array( $this, 'clear_join_requests' ),
70                'permission_callback' => array( $this, 'check_admin_edit_post_permission' ),
71                'args'                => array(
72                    'post_id' => array(
73                        'required'          => true,
74                        'type'              => 'integer',
75                        'sanitize_callback' => 'absint',
76                    ),
77                ),
78            )
79        );
80    }
81
82    /**
83     * Check if the current user can edit the post specified in the request.
84     *
85     * @param WP_REST_Request $request The request.
86     * @return bool|\WP_Error
87     */
88    public function check_edit_post_permission( $request ) {
89        $post_id = $request->get_param( 'post_id' );
90
91        if ( ! get_post( $post_id ) ) {
92            return new \WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'jetpack-rtc' ), array( 'status' => 404 ) );
93        }
94
95        if ( ! current_user_can( 'edit_post', $post_id ) ) {
96            return new \WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to edit this post.', 'jetpack-rtc' ), array( 'status' => 403 ) );
97        }
98
99        return true;
100    }
101
102    /**
103     * Check if the current user is an admin who can edit the post.
104     *
105     * @param WP_REST_Request $request The request.
106     * @return bool|\WP_Error
107     */
108    public function check_admin_edit_post_permission( $request ) {
109        if ( ! current_user_can( 'manage_options' ) ) {
110            return false;
111        }
112
113        return $this->check_edit_post_permission( $request );
114    }
115
116    /**
117     * Record a join request from a blocked user.
118     * Stored as a transient per post so it auto-expires.
119     *
120     * @param WP_REST_Request $request The request.
121     * @return WP_REST_Response
122     */
123    public function record_join_request( $request ) {
124        $post_id = $request->get_param( 'post_id' );
125        $user    = wp_get_current_user();
126
127        $key      = self::JOIN_REQUEST_OPTION . '_' . $post_id;
128        $requests = get_transient( $key );
129        if ( ! is_array( $requests ) ) {
130            $requests = array();
131        }
132
133        $requests[ $user->ID ] = array(
134            'userName' => $user->display_name,
135            'userId'   => $user->ID,
136            'time'     => time(),
137        );
138
139        // Expire after 2 minutes.
140        set_transient( $key, $requests, 2 * MINUTE_IN_SECONDS );
141
142        return rest_ensure_response( array( 'success' => true ) );
143    }
144
145    /**
146     * Get pending join requests for a post. Admin only.
147     *
148     * @param WP_REST_Request $request The request.
149     * @return WP_REST_Response
150     */
151    public function get_join_requests( $request ) {
152        $post_id  = $request->get_param( 'post_id' );
153        $key      = self::JOIN_REQUEST_OPTION . '_' . $post_id;
154        $requests = get_transient( $key );
155
156        if ( ! is_array( $requests ) ) {
157            $requests = array();
158        }
159
160        // Filter out requests older than 60 seconds.
161        $now    = time();
162        $recent = array();
163        foreach ( $requests as $uid => $req ) {
164            if ( $now - $req['time'] < 60 ) {
165                $recent[ $uid ] = $req;
166            }
167        }
168
169        return rest_ensure_response( array( 'requests' => array_values( $recent ) ) );
170    }
171
172    /**
173     * Clear join requests for a post.
174     *
175     * @param WP_REST_Request $request The request.
176     * @return WP_REST_Response
177     */
178    public function clear_join_requests( $request ) {
179        $post_id = $request->get_param( 'post_id' );
180        delete_transient( self::JOIN_REQUEST_OPTION . '_' . $post_id );
181        return rest_ensure_response( array( 'success' => true ) );
182    }
183}