Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.44% covered (warning)
86.44%
51 / 59
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Zendesk_Chat
86.44% covered (warning)
86.44%
51 / 59
80.00% covered (warning)
80.00%
4 / 5
13.42
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
1
 chat_authentication_permissions_callback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_auth_token_transient_key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_chat_authentication
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
6
 get_chat_availability
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Sets up the Zendesk Chat REST API endpoints.
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use Automattic\Jetpack\Connection\Client;
11use WP_Error;
12use WP_REST_Response;
13
14/**
15 * Registers the REST routes for Zendesk Chat.
16 *
17 * @phan-constructor-used-for-side-effects
18 */
19class REST_Zendesk_Chat {
20    const TRANSIENT_EXPIRY   = 1 * MINUTE_IN_SECONDS * 60 * 24 * 7; // 1 week (JWT is actually 2 weeks, but lets be on the safe side)
21    const ZENDESK_AUTH_TOKEN = 'zendesk_auth_token';
22    /**
23     * Constructor.
24     */
25    public function __construct() {
26        register_rest_route(
27            'my-jetpack/v1',
28            'chat/availability',
29            array(
30                'methods'             => \WP_REST_Server::READABLE,
31                'callback'            => __CLASS__ . '::get_chat_availability',
32                'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
33            )
34        );
35
36        register_rest_route(
37            'my-jetpack/v1',
38            'chat/authentication',
39            array(
40                'methods'             => \WP_REST_Server::READABLE,
41                'callback'            => __CLASS__ . '::get_chat_authentication',
42                'args'                => array(
43                    'type'      => array(
44                        'required' => false,
45                        'type'     => 'string',
46                    ),
47                    'test_mode' => array(
48                        'required' => false,
49                        'type'     => 'boolean',
50                    ),
51                ),
52                'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
53            )
54        );
55    }
56
57    /**
58     * Ensure user is logged in if making an authentication request
59     *
60     * @access public
61     * @static
62     *
63     * @return WP_Error|true
64     */
65    public static function chat_authentication_permissions_callback() {
66        if ( ! get_current_user_id() ) {
67            return new WP_Error( 'unauthorized', 'You must be logged in to access this resource.', array( 'status' => 401 ) );
68        }
69
70        return true;
71    }
72
73    /**
74     * Gets the transient key holding the current user's chat authentication token.
75     *
76     * The token is minted for the calling user's WordPress.com identity, so it must never
77     * be cached under a key shared by every user of the site.
78     *
79     * @access private
80     * @static
81     *
82     * @return string
83     */
84    private static function get_auth_token_transient_key() {
85        return self::ZENDESK_AUTH_TOKEN . '_' . get_current_user_id();
86    }
87
88    /**
89     * Gets the chat authentication token.
90     *
91     * @return WP_Error|WP_REST_Response { token: string }
92     */
93    public static function get_chat_authentication() {
94        $transient_key = self::get_auth_token_transient_key();
95
96        $authentication = get_transient( $transient_key );
97        if ( $authentication ) {
98            return rest_ensure_response( $authentication );
99        }
100
101        // Discard any token left behind under the previous site-wide key.
102        delete_transient( self::ZENDESK_AUTH_TOKEN );
103
104        $proxied           = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
105        $wpcom_endpoint    = 'help/authenticate/chat';
106        $wpcom_api_version = '2';
107
108        $body = array(
109            'type'      => 'zendesk',
110            'test_mode' => $proxied ? true : false,
111        );
112
113        $response      = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version, array( 'method' => 'POST' ), $body );
114        $response_code = wp_remote_retrieve_response_code( $response );
115        $body          = json_decode( wp_remote_retrieve_body( $response ) );
116
117        if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
118            return new WP_Error( 'chat_authentication_failed', 'Chat authentication failed', array( 'status' => $response_code ) );
119        }
120
121        set_transient( $transient_key, $body, self::TRANSIENT_EXPIRY );
122        return rest_ensure_response( $body );
123    }
124
125    /**
126     * Calls `wpcom/v2/presales/chat?group=jp_presales` endpoint.
127     * This endpoint returns whether or not the Jetpack presales chat group is available
128     *
129     * @return WP_Error|WP_REST_Response { is_available: bool }
130     */
131    public static function get_chat_availability() {
132        $wpcom_endpoint    = '/presales/chat?group=jp_presales';
133        $wpcom_api_version = '2';
134        $response          = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version );
135        $response_code     = wp_remote_retrieve_response_code( $response );
136        $body              = json_decode( wp_remote_retrieve_body( $response ) );
137
138        if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
139            return new WP_Error( 'chat_config_data_fetch_failed', 'Chat config data fetch failed', array( 'status' => $response_code ) );
140        }
141
142        return rest_ensure_response( $body );
143    }
144}