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