Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.38% covered (warning)
65.38%
68 / 104
80.00% covered (warning)
80.00%
12 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Helpers
65.38% covered (warning)
65.38%
68 / 104
80.00% covered (warning)
80.00%
12 / 15
119.69
0.00% covered (danger)
0.00%
0 / 1
 should_hide_login_form
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 match_by_email
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 new_user_override
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 is_two_step_required
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 bypass_login_forward_wpcom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 show_sso_login
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_require_two_step_checkbox_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_match_by_email_checkbox_disabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 allowed_redirect_hosts
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
10
 extend_auth_cookie_expiration_for_sso
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 display_sso_form_for_action
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 get_json_api_auth_environment
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 get_custom_login_url
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 clear_wpcom_profile_cookies
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 delete_connection_for_user
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * A collection of helper functions used in the SSO module.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection\SSO;
9
10use Automattic\Jetpack\Connection\SSO;
11use Automattic\Jetpack\Constants;
12use Jetpack_IXR_Client;
13
14/**
15 * A collection of helper functions used in the SSO module.
16 *
17 * @since jetpack-4.1.0
18 */
19class Helpers {
20    /**
21     * Determine if the login form should be hidden or not
22     *
23     * @return bool
24     **/
25    public static function should_hide_login_form() {
26        /**
27         * Remove the default log in form, only leave the WordPress.com log in button.
28         *
29         * @module sso
30         *
31         * @since jetpack-3.1.0
32         *
33         * @param bool get_option( 'jetpack_sso_remove_login_form', false ) Should the default log in form be removed. Default to false.
34         */
35        return (bool) apply_filters( 'jetpack_remove_login_form', get_option( 'jetpack_sso_remove_login_form', false ) );
36    }
37
38    /**
39     * Returns a boolean value for whether logging in by matching the WordPress.com user email to a
40     * Jetpack site user's email is allowed.
41     *
42     * @return bool
43     */
44    public static function match_by_email() {
45        $match_by_email = defined( 'WPCC_MATCH_BY_EMAIL' ) ? \WPCC_MATCH_BY_EMAIL : (bool) get_option( 'jetpack_sso_match_by_email', true );
46
47        /**
48         * Link the local account to an account on WordPress.com using the same email address.
49         *
50         * @module sso
51         *
52         * @since jetpack-2.6.0
53         *
54         * @param bool $match_by_email Should we link the local account to an account on WordPress.com using the same email address. Default to false.
55         */
56        return (bool) apply_filters( 'jetpack_sso_match_by_email', $match_by_email );
57    }
58
59    /**
60     * Returns a boolean for whether users are allowed to register on the Jetpack site with SSO,
61     * even though the site disallows normal registrations.
62     *
63     * @param object|null $user_data WordPress.com user information.
64     * @return bool|string
65     */
66    public static function new_user_override( $user_data = null ) {
67        $new_user_override = defined( 'WPCC_NEW_USER_OVERRIDE' ) ? \WPCC_NEW_USER_OVERRIDE : false;
68
69        /**
70         * Allow users to register on your site with a WordPress.com account, even though you disallow normal registrations.
71         * If you return a string that corresponds to a user role, the user will be given that role.
72         *
73         * @module sso
74         *
75         * @since jetpack-2.6.0
76         * @since jetpack-4.6   $user_data object is now passed to the jetpack_sso_new_user_override filter
77         *
78         * @param bool|string $new_user_override Allow users to register on your site with a WordPress.com account. Default to false.
79         * @param object|null $user_data         An object containing the user data returned from WordPress.com.
80         */
81        $role = apply_filters( 'jetpack_sso_new_user_override', $new_user_override, $user_data );
82
83        if ( $role ) {
84            if ( is_string( $role ) && get_role( $role ) ) {
85                return $role;
86            } else {
87                return get_option( 'default_role' );
88            }
89        }
90
91        return false;
92    }
93
94    /**
95     * Returns a boolean value for whether two-step authentication is required for SSO.
96     *
97     * @since jetpack-4.1.0
98     *
99     * @return bool
100     */
101    public static function is_two_step_required() {
102        /**
103         * Is it required to have 2-step authentication enabled on WordPress.com to use SSO?
104         *
105         * @module sso
106         *
107         * @since jetpack-2.8.0
108         *
109         * @param bool get_option( 'jetpack_sso_require_two_step' ) Does SSO require 2-step authentication?
110         */
111        return (bool) apply_filters( 'jetpack_sso_require_two_step', get_option( 'jetpack_sso_require_two_step', false ) );
112    }
113
114    /**
115     * Returns a boolean for whether a user that is attempting to log in will be automatically
116     * redirected to WordPress.com to begin the SSO flow.
117     *
118     * @return bool
119     */
120    public static function bypass_login_forward_wpcom() {
121        /**
122         * Redirect the site's log in form to WordPress.com's log in form.
123         *
124         * @module sso
125         *
126         * @since jetpack-3.1.0
127         *
128         * @param bool false Should the site's log in form be automatically forwarded to WordPress.com's log in form.
129         */
130        return (bool) apply_filters( 'jetpack_sso_bypass_login_forward_wpcom', false );
131    }
132
133    /**
134     * Returns a boolean for whether the SSO login form should be displayed as the default
135     * when both the default and SSO login form allowed.
136     *
137     * @since jetpack-4.1.0
138     *
139     * @return bool
140     */
141    public static function show_sso_login() {
142        if ( self::should_hide_login_form() ) {
143            return true;
144        }
145
146        /**
147         * Display the SSO login form as the default when both the default and SSO login forms are enabled.
148         *
149         * @module sso
150         *
151         * @since jetpack-4.1.0
152         *
153         * @param bool true Should the SSO login form be displayed by default when the default login form is also enabled?
154         */
155        return (bool) apply_filters( 'jetpack_sso_default_to_sso_login', true );
156    }
157
158    /**
159     * Returns a boolean for whether the two step required checkbox, displayed on the Jetpack admin page, should be disabled.
160     *
161     * @since jetpack-4.1.0
162     *
163     * @return bool
164     */
165    public static function is_require_two_step_checkbox_disabled() {
166        return (bool) has_filter( 'jetpack_sso_require_two_step' );
167    }
168
169    /**
170     * Returns a boolean for whether the match by email checkbox, displayed on the Jetpack admin page, should be disabled.
171     *
172     * @since jetpack-4.1.0
173     *
174     * @return bool
175     */
176    public static function is_match_by_email_checkbox_disabled() {
177        return defined( 'WPCC_MATCH_BY_EMAIL' ) || has_filter( 'jetpack_sso_match_by_email' );
178    }
179
180    /**
181     * Returns an array of hosts that SSO will redirect to.
182     *
183     * Instead of accessing JETPACK__API_BASE within the method directly, we set it as the
184     * default for $api_base due to restrictions with testing constants in our tests.
185     *
186     * @since jetpack-4.3.0
187     * @since jetpack-4.6.0 Added public-api.wordpress.com as an allowed redirect
188     *
189     * @param array  $hosts Allowed redirect hosts.
190     * @param string $api_base Base API URL.
191     *
192     * @return array
193     */
194    public static function allowed_redirect_hosts( $hosts, $api_base = '' ) {
195        if ( empty( $api_base ) ) {
196            $api_base = Constants::get_constant( 'JETPACK__API_BASE' );
197        }
198
199        if ( empty( $hosts ) ) {
200            $hosts = array();
201        }
202
203        $hosts[] = 'wordpress.com';
204        $hosts[] = 'jetpack.wordpress.com';
205        $hosts[] = 'public-api.wordpress.com';
206        $hosts[] = 'jetpack.com';
207
208        if ( ! str_contains( $api_base, 'jetpack.wordpress.com/jetpack' ) ) {
209            $base_url_parts = wp_parse_url( esc_url_raw( $api_base ) );
210            if ( $base_url_parts && ! empty( $base_url_parts['host'] ) ) {
211                $hosts[] = $base_url_parts['host'];
212            }
213        }
214
215        foreach ( array( SSO::get_broker_url(), SSO::get_broker_auth_url() ) as $broker_url ) {
216            if ( $broker_url ) {
217                $broker_parts = wp_parse_url( $broker_url );
218                if ( $broker_parts && ! empty( $broker_parts['host'] ) ) {
219                    $hosts[] = $broker_parts['host'];
220                }
221            }
222        }
223
224        return array_unique( $hosts );
225    }
226
227    /**
228     * Determines how long the auth cookie is valid for when a user logs in with SSO.
229     *
230     * @return int result of the jetpack_sso_auth_cookie_expiration filter.
231     */
232    public static function extend_auth_cookie_expiration_for_sso() {
233        /**
234         * Determines how long the auth cookie is valid for when a user logs in with SSO.
235         *
236         * @module sso
237         *
238         * @since jetpack-4.4.0
239         * @since jetpack-6.1.0 Fixed a typo. Filter was previously jetpack_sso_auth_cookie_expirtation.
240         *
241         * @param int YEAR_IN_SECONDS
242         */
243        return (int) apply_filters( 'jetpack_sso_auth_cookie_expiration', YEAR_IN_SECONDS );
244    }
245
246    /**
247     * Determines if the SSO form should be displayed for the current action.
248     *
249     * @since jetpack-4.6.0
250     *
251     * @param string $action SSO action being performed.
252     *
253     * @return bool  Is SSO allowed for the current action?
254     */
255    public static function display_sso_form_for_action( $action ) {
256        /**
257         * Allows plugins the ability to overwrite actions where the SSO form is allowed to be used.
258         *
259         * @module sso
260         *
261         * @since jetpack-4.6.0
262         *
263         * @param array $allowed_actions_for_sso
264         */
265        $allowed_actions_for_sso = (array) apply_filters(
266            'jetpack_sso_allowed_actions',
267            array(
268                'login',
269                'jetpack-sso',
270                'jetpack_json_api_authorization',
271                'entered_recovery_mode',
272            )
273        );
274        return in_array( $action, $allowed_actions_for_sso, true );
275    }
276
277    /**
278     * This method returns an environment array that is meant to simulate `$_REQUEST` when the initial
279     * JSON API auth request was made.
280     *
281     * @since jetpack-4.6.0
282     *
283     * @return array|bool
284     */
285    public static function get_json_api_auth_environment() {
286        if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) {
287            return false;
288        }
289
290        $original_request = esc_url_raw( wp_unslash( $_COOKIE['jetpack_sso_original_request'] ) );
291
292        $parsed_url = wp_parse_url( $original_request );
293        if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) {
294            return false;
295        }
296
297        $args = array();
298        wp_parse_str( $parsed_url['query'], $args );
299
300        if ( empty( $args ) || empty( $args['action'] ) ) {
301            return false;
302        }
303
304        if ( 'jetpack_json_api_authorization' !== $args['action'] ) {
305            return false;
306        }
307
308        return array_merge(
309            $args,
310            array( 'jetpack_json_api_original_query' => $original_request )
311        );
312    }
313
314    /**
315     * Check if the site has a custom login page URL, and return it.
316     * If default login page URL is used (`wp-login.php`), `null` will be returned.
317     *
318     * @return string|null
319     */
320    public static function get_custom_login_url() {
321        $login_url = wp_login_url();
322
323        if ( str_ends_with( $login_url, 'wp-login.php' ) ) {
324            // No custom URL found.
325            return null;
326        }
327
328        $site_url = trailingslashit( site_url() );
329
330        if ( ! str_starts_with( $login_url, $site_url ) ) {
331            // Something went wrong, we can't properly extract the custom URL.
332            return null;
333        }
334
335        // Extracting the "path" part of the URL, because we don't need the `site_url` part.
336        return str_ireplace( $site_url, '', $login_url );
337    }
338
339    /**
340     * Clear the cookies that store the profile information for the last
341     * WPCOM user to connect.
342     */
343    public static function clear_wpcom_profile_cookies() {
344        if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
345            setcookie(
346                'jetpack_sso_wpcom_name_' . COOKIEHASH,
347                ' ',
348                time() - YEAR_IN_SECONDS,
349                COOKIEPATH,
350                COOKIE_DOMAIN,
351                is_ssl(),
352                true
353            );
354        }
355
356        if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
357            setcookie(
358                'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
359                ' ',
360                time() - YEAR_IN_SECONDS,
361                COOKIEPATH,
362                COOKIE_DOMAIN,
363                is_ssl(),
364                true
365            );
366        }
367    }
368
369    /**
370     * Remove an SSO connection for a user.
371     *
372     * @param int $user_id The local user id.
373     */
374    public static function delete_connection_for_user( $user_id ) {
375        $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true );
376        if ( ! $wpcom_user_id ) {
377            return;
378        }
379
380        $xml = new Jetpack_IXR_Client(
381            array(
382                'wpcom_user_id' => $user_id,
383            )
384        );
385        $xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
386
387        if ( $xml->isError() ) {
388            return false;
389        }
390
391        // Clean up local data stored for SSO.
392        delete_user_meta( $user_id, 'wpcom_user_id' );
393        delete_user_meta( $user_id, 'wpcom_user_data' );
394        self::clear_wpcom_profile_cookies();
395
396        return $xml->getResponse();
397    }
398}