Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
63.53% covered (warning)
63.53%
54 / 85
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Webhooks
63.53% covered (warning)
63.53%
54 / 85
25.00% covered (danger)
25.00%
2 / 8
77.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 fallback_jetpack_controller
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 controller
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
8.12
 handle_authorize
77.42% covered (warning)
77.42%
24 / 31
0.00% covered (danger)
0.00%
0 / 1
8.74
 handle_authorize_redirect
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 do_exit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 handle_connect_url_redirect
40.00% covered (danger)
40.00%
12 / 30
0.00% covered (danger)
0.00%
0 / 1
31.60
1<?php
2/**
3 * Connection Webhooks class.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Automattic\Jetpack\CookieState;
11use Automattic\Jetpack\Roles;
12use Automattic\Jetpack\Status\Host;
13use Automattic\Jetpack\Tracking;
14use Jetpack_Options;
15
16/**
17 * Connection Webhooks class.
18 */
19class Webhooks {
20
21    /**
22     * The Connection Manager object.
23     *
24     * @var Manager
25     */
26    private $connection;
27
28    /**
29     * Webhooks constructor.
30     *
31     * @param Manager $connection The Connection Manager object.
32     */
33    public function __construct( $connection ) {
34        $this->connection = $connection;
35    }
36
37    /**
38     * Initialize the webhooks.
39     *
40     * @param Manager $connection The Connection Manager object.
41     */
42    public static function init( $connection ) {
43        $webhooks = new static( $connection );
44
45        add_action( 'init', array( $webhooks, 'controller' ) );
46        add_action( 'load-toplevel_page_jetpack', array( $webhooks, 'fallback_jetpack_controller' ) );
47    }
48
49    /**
50     * Jetpack plugin used to trigger this webhooks in Jetpack::admin_page_load()
51     *
52     * The Jetpack toplevel menu is still accessible for stand-alone plugins, and while there's no content for that page, there are still
53     * actions from Calypso and WPCOM that reach that route regardless of the site having the Jetpack plugin or not. That's why we are still handling it here.
54     */
55    public function fallback_jetpack_controller() {
56        $this->controller( true );
57    }
58
59    /**
60     * The "controller" decides which handler we need to run.
61     *
62     * @param bool $force Do not check if it's a webhook request and just run the controller.
63     */
64    public function controller( $force = false ) {
65        if ( ! $force ) {
66            // The nonce is verified in specific handlers.
67            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
68            if ( empty( $_GET['handler'] ) || 'jetpack-connection-webhooks' !== $_GET['handler'] ) {
69                return;
70            }
71        }
72
73        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
74        if ( isset( $_GET['connect_url_redirect'] ) ) {
75            $this->handle_connect_url_redirect();
76        }
77
78        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
79        if ( empty( $_GET['action'] ) ) {
80            return;
81        }
82
83        // The nonce is verified in specific handlers.
84        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
85        switch ( $_GET['action'] ) {
86            case 'authorize':
87                $this->handle_authorize();
88                $this->do_exit();
89                break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
90            case 'authorize_redirect':
91                $this->handle_authorize_redirect();
92                $this->do_exit();
93                break; // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though do_exit never returns.
94            // Class Jetpack::admin_page_load() still handles other cases.
95        }
96    }
97
98    /**
99     * Perform the authorization action.
100     */
101    public function handle_authorize() {
102        if ( $this->connection->is_connected() && $this->connection->is_user_connected() ) {
103            $redirect_url = apply_filters( 'jetpack_client_authorize_already_authorized_url', admin_url() );
104
105            if ( ! empty( $_GET['redirect'] ) ) {
106                $explicit = esc_url_raw( wp_unslash( $_GET['redirect'] ) );
107                if ( wp_validate_redirect( $explicit ) ) {
108                    $redirect_url = $explicit;
109                }
110            }
111
112            wp_safe_redirect( $redirect_url );
113
114            return;
115        }
116        do_action( 'jetpack_client_authorize_processing' );
117
118        $data              = stripslashes_deep( $_GET ); // We need all request data under the context of an authorization request.
119        $data['auth_type'] = 'client';
120        $roles             = new Roles();
121        $role              = $roles->translate_current_user_to_role();
122        $redirect          = isset( $data['redirect'] ) ? esc_url_raw( (string) $data['redirect'] ) : '';
123
124        check_admin_referer( "jetpack-authorize_{$role}_{$redirect}" );
125
126        $tracking = new Tracking();
127
128        $result = $this->connection->authorize( $data );
129
130        if ( is_wp_error( $result ) ) {
131            do_action( 'jetpack_client_authorize_error', $result );
132
133            $tracking->record_user_event(
134                'jpc_client_authorize_fail',
135                array(
136                    'error_code'    => $result->get_error_code(),
137                    'error_message' => $result->get_error_message(),
138                )
139            );
140        } else {
141            /**
142             * Fires after the Jetpack client is authorized to communicate with WordPress.com.
143             *
144             * @param int Jetpack Blog ID.
145             *
146             * @since 1.7.0
147             * @since-jetpack 4.2.0
148             */
149            do_action( 'jetpack_client_authorized', Jetpack_Options::get_option( 'id' ) );
150
151            $tracking->record_user_event( 'jpc_client_authorize_success' );
152        }
153
154        $fallback_redirect = apply_filters( 'jetpack_client_authorize_fallback_url', admin_url() );
155        $redirect          = wp_validate_redirect( $redirect ) ? $redirect : $fallback_redirect;
156
157        wp_safe_redirect( $redirect );
158    }
159
160    /**
161     * The authorhize_redirect webhook handler
162     */
163    public function handle_authorize_redirect() {
164        $authorize_redirect_handler = new Webhooks\Authorize_Redirect( $this->connection );
165        $authorize_redirect_handler->handle();
166    }
167
168    /**
169     * The `exit` is wrapped into a method so we could mock it.
170     *
171     * @return never
172     */
173    protected function do_exit() {
174        exit( 0 );
175    }
176
177    /**
178     * Handle the `connect_url_redirect` action,
179     * which is usually called to repeat an attempt for user to authorize the connection.
180     *
181     * @return void
182     */
183    public function handle_connect_url_redirect() {
184        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
185        $from = ! empty( $_GET['from'] ) ? sanitize_text_field( wp_unslash( $_GET['from'] ) ) : 'iframe';
186
187        $skip_pricing = filter_input( INPUT_GET, 'skip_pricing', FILTER_VALIDATE_BOOLEAN );
188
189        // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitization happens in get_authorization_url()
190        $redirect = ! empty( $_GET['redirect_after_auth'] ) ? wp_unslash( $_GET['redirect_after_auth'] ) : false;
191
192        add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_environments' ) );
193
194        if ( ! $this->connection->is_user_connected() ) {
195            if ( ! $this->connection->is_connected() ) {
196                $this->connection->register();
197            }
198
199            $connect_url = add_query_arg( 'from', $from, $this->connection->get_authorization_url( null, $redirect ) );
200
201            if ( $skip_pricing ) {
202                $connect_url = add_query_arg( 'skip_pricing', '1', $connect_url );
203            }
204
205            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
206            if ( isset( $_GET['notes_iframe'] ) ) {
207                $connect_url .= '&notes_iframe';
208            }
209            wp_safe_redirect( $connect_url );
210            $this->do_exit();
211        } elseif ( ! isset( $_GET['calypso_env'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
212            ( new CookieState() )->state( 'message', 'already_authorized' );
213            wp_safe_redirect( $redirect );
214            $this->do_exit();
215        } else {
216            if ( 'connect-after-checkout' === $from && $redirect ) {
217                wp_safe_redirect( $redirect );
218                $this->do_exit();
219            }
220            $connect_url = add_query_arg(
221                array(
222                    'from'               => $from,
223                    'already_authorized' => true,
224                ),
225                $this->connection->get_authorization_url()
226            );
227            wp_safe_redirect( $connect_url );
228            $this->do_exit();
229        }
230    }
231}