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