Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.30% covered (danger)
18.30%
43 / 235
10.00% covered (danger)
10.00%
2 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Blocked_Login_Page
18.30% covered (danger)
18.30%
43 / 235
10.00% covered (danger)
10.00%
2 / 20
4311.43
0.00% covered (danger)
0.00%
0 / 1
 instance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 get_help_url
n/a
0 / 0
n/a
0 / 0
0
 add_args_to_lostpassword_redirect_url
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 add_args_to_lostpassword_url
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 add_args_to_login_post_url
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 add_args_to_login_url
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 check_valid_blocked_user
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 is_blocked_user_valid
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 is_valid_protect_recovery_key
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 render_and_die
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
90
 render_blocked_login_message
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process_recovery_email
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 user_has_unfinished_registration
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 send_recovery_email
85.71% covered (warning)
85.71%
24 / 28
0.00% covered (danger)
0.00%
0 / 1
10.29
 protect_die
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 render_recovery_form
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 render_recovery_success
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_html_blocked_login_message
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 get_html_recovery_form
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 display_page
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
132
1<?php // phpcs:ignore - WordPress.Files.FileName.InvalidClassFileName
2
3namespace Automattic\Jetpack\Waf;
4
5use Automattic\Jetpack\Connection\Client;
6use Jetpack_Options;
7use WP_Error;
8use WP_User;
9
10/**
11 * Class Blocked_Login_Page
12 *
13 * Instantiated on the wp-login page when Jetpack modules are loaded and $pagenow
14 * is available, or during the login_head hook.
15 *
16 * Class will only be instantiated if a hard blocked IP address is detected.
17 */
18abstract class Blocked_Login_Page {
19
20    /**
21     * Instance of the class.
22     *
23     * @var Blocked_Login_Page
24     */
25    private static $instance = null;
26
27    /**
28     * Can send recovery emails. defaults to true.
29     *
30     * @var bool
31     */
32    public $can_send_recovery_emails;
33
34    /**
35     * The IP address.
36     *
37     * @var string
38     */
39    public $ip_address;
40
41    /**
42     * Valid blocked user ID.
43     *
44     * @var int
45     */
46    public $valid_blocked_user_id;
47
48    /**
49     * The email address.
50     *
51     * @var string
52     */
53    public $email_address;
54
55    /**
56     * Status code for too many requests.
57     *
58     * @var int
59     */
60    const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429;
61
62    /**
63     * How long after registration a reset key still counts as the registration email's key.
64     *
65     * @var int
66     */
67    const REGISTRATION_KEY_WINDOW_IN_SECONDS = 300;
68
69    /**
70     * Whether to point users who never set a password at their registration email instead of sending a recovery email.
71     *
72     * Only useful when the set-password link isn't blocked for this visitor.
73     *
74     * @var bool
75     */
76    protected $check_unfinished_registrations = true;
77
78    /**
79     * Singleton implementation
80     *
81     * @param string $ip_address - the IP address.
82     *
83     * @return object
84     */
85    public static function instance( $ip_address ) {
86        if ( ! self::$instance ) {
87            // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic -- This is only instantiated in non-abstract classes (i.e. Waf_Blocked_Login_Page).
88            self::$instance = new static( $ip_address );
89        }
90
91        return self::$instance;
92    }
93
94    /**
95     * Singleton implementation
96     *
97     * @param string $ip_address - the IP address.
98     */
99    public function __construct( $ip_address ) {
100        /**
101         * Filter controls if an email recovery form is shown to blocked IPs.
102         *
103         * A recovery form allows folks to re-gain access to the login form
104         * via an email link if their IP was mistakenly blocked.
105         *
106         * @module protect
107         *
108         * @since 5.6.0
109         *
110         * @param bool $can_send_recovery_emails Defaults to true.
111         */
112        $this->can_send_recovery_emails = apply_filters( 'jetpack_protect_can_send_recovery_emails', true );
113        $this->ip_address               = $ip_address;
114
115        add_filter( 'wp_authenticate_user', array( $this, 'check_valid_blocked_user' ), 10, 1 );
116        add_filter( 'site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
117        add_filter( 'network_site_url', array( $this, 'add_args_to_login_post_url' ), 10, 3 );
118        add_filter( 'lostpassword_url', array( $this, 'add_args_to_lostpassword_url' ), 10, 2 );
119        add_filter( 'login_url', array( $this, 'add_args_to_login_url' ), 10, 3 );
120        add_filter( 'lostpassword_redirect', array( $this, 'add_args_to_lostpassword_redirect_url' ), 10, 1 );
121    }
122
123    /**
124     * Gets the URL that redirects to the support page on unblocking
125     *
126     * @since 8.5.0
127     *
128     * @return string
129     */
130    abstract public function get_help_url();
131
132    /**
133     * Add arguments to lost password redirect url.
134     *
135     * @param string $url - the URL.
136     */
137    public function add_args_to_lostpassword_redirect_url( $url ) {
138        if ( $this->valid_blocked_user_id ) {
139            $url = empty( $url ) ? wp_login_url() : $url;
140            $url = add_query_arg(
141                array(
142                    'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
143                    'user_id'                           => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
144                    'checkemail'                        => 'confirm',
145                ),
146                $url
147            );
148        }
149
150        return $url;
151    }
152
153    /**
154     * Add arguments to lost password redirect url.
155     *
156     * @param string $url - the URL.
157     * @param string $redirect - where to redirect to.
158     */
159    public function add_args_to_lostpassword_url( $url, $redirect ) {
160        if ( $this->valid_blocked_user_id ) {
161            $args = array(
162                'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
163                'user_id'                           => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
164                'action'                            => 'lostpassword',
165            );
166            if ( ! empty( $redirect ) ) {
167                $args['redirect_to'] = $redirect;
168            }
169            $url = add_query_arg( $args, $url );
170        }
171
172        return $url;
173    }
174
175    /**
176     * Add arguments to login post url.
177     *
178     * @param string $url - the URL.
179     * @param string $path - the path.
180     * @param string $scheme -the scheme(?).
181     */
182    public function add_args_to_login_post_url( $url, $path, $scheme ) {
183        if ( $this->valid_blocked_user_id && ( 'login_post' === $scheme || 'login' === $scheme ) ) {
184            $url = add_query_arg(
185                array(
186                    'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
187                    'user_id'                           => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
188                ),
189                $url
190            );
191
192        }
193
194        return $url;
195    }
196
197    /**
198     * Add arguments to login post url.
199     *
200     * @param string $url - the URL.
201     * @param string $redirect - where we want to redirect to.
202     * @param string $force_reauth -if we're forcing reauthorization.
203     */
204    public function add_args_to_login_url( $url, $redirect, $force_reauth ) {
205        if ( $this->valid_blocked_user_id ) {
206            $args = array(
207                'validate_jetpack_protect_recovery' => isset( $_GET['validate_jetpack_protect_recovery'] ) ? filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ) : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
208                'user_id'                           => isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : null, // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
209            );
210
211            if ( ! empty( $redirect ) ) {
212                $args['redirect_to'] = $redirect;
213            }
214
215            if ( ! empty( $force_reauth ) ) {
216                $args['reauth'] = '1';
217            }
218            $url = add_query_arg( $args, $url );
219        }
220
221        return $url;
222    }
223
224    /**
225     * Check if user is blocked.
226     *
227     * @param WP_User|WP_Error $user - The user or error object if prior callback failed auth.
228     */
229    public function check_valid_blocked_user( $user ) {
230        if ( is_wp_error( $user ) ) {
231            return $user;
232        }
233
234        if ( $this->valid_blocked_user_id && $this->valid_blocked_user_id != $user->ID ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
235            return new WP_Error( 'invalid_recovery_token', __( 'The recovery token is not valid for this user.', 'jetpack-waf' ) );
236        }
237
238        return $user;
239    }
240
241    /**
242     * Check if user is valid.
243     */
244    public function is_blocked_user_valid() {
245        if ( ! $this->can_send_recovery_emails ) {
246            return false;
247        }
248
249        if ( $this->valid_blocked_user_id ) {
250            return true;
251        }
252
253        if ( ! isset( $_GET['validate_jetpack_protect_recovery'] ) || ! isset( $_GET['user_id'] ) ) { // phpcs:ignore: WordPress.Security.NonceVerification.Recommended -- no changes made if this isn't set.
254            return false;
255        }
256
257        if ( ! $this->is_valid_protect_recovery_key( filter_var( wp_unslash( $_GET['validate_jetpack_protect_recovery'] ) ), (int) $_GET['user_id'] ) ) { // phpcs:ignore: WordPress.Security.NonceVerification.Recommended -- no changes made if this isn't set.
258            return false;
259        }
260
261        $this->valid_blocked_user_id = (int) $_GET['user_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nothing on the site is changed in response to this request.
262
263        return true;
264    }
265
266    /**
267     * Checks if recovery key is valid.
268     *
269     * @param string $key - they recovery key.
270     * @param int    $user_id - the User ID.
271     */
272    public function is_valid_protect_recovery_key( $key, $user_id ) {
273
274        $path     = sprintf( '/sites/%d/protect/recovery/confirm', Jetpack_Options::get_option( 'id' ) );
275        $response = Client::wpcom_json_api_request_as_blog(
276            $path,
277            '1.1',
278            array(
279                'method' => 'post',
280            ),
281            array(
282                'token'   => $key,
283                'user_id' => $user_id,
284                'ip'      => $this->ip_address,
285            )
286        );
287
288        $result = json_decode( wp_remote_retrieve_body( $response ) );
289
290        if ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
291            return false;
292        }
293
294        set_transient( 'jetpack_protect_recovery_key_validated_' . $user_id, true, 600 );
295
296        return true;
297    }
298
299    /**
300     * Check if we should render the recovery form.
301     */
302    public function render_and_die() {
303        if ( ! $this->can_send_recovery_emails ) {
304            $this->render_blocked_login_message();
305
306            return;
307        }
308
309        if ( isset( $_GET['validate_jetpack_protect_recovery'] ) && ! empty( $_GET['user_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes, just throws invalid token error.
310            $error = new WP_Error( 'invalid_token', __( "Oops, we couldn't validate the recovery token.", 'jetpack-waf' ) );
311            $this->protect_die( $error );
312
313            return;
314        }
315
316        if (
317            isset( $_GET['jetpack-protect-recovery'] ) &&
318            isset( $_POST['_wpnonce'] ) &&
319            wp_verify_nonce( $_POST['_wpnonce'], 'bypass-protect' ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP Core doesn't unstrip or sanitize nonces either.
320        ) {
321            $this->process_recovery_email();
322
323            return;
324        }
325
326        if ( isset( $_GET['loggedout'] ) && 'true' === $_GET['loggedout'] ) {
327            $this->protect_die( __( 'You successfully logged out.', 'jetpack-waf' ) );
328        }
329
330        $this->render_recovery_form();
331    }
332
333    /**
334     * Render the blocked login message.
335     */
336    public function render_blocked_login_message() {
337        $this->protect_die( $this->get_html_blocked_login_message() );
338    }
339
340    /**
341     * Process sending a recovery email.
342     */
343    public function process_recovery_email() {
344        $sent               = $this->send_recovery_email();
345        $show_recovery_form = true;
346        if ( is_wp_error( $sent ) ) {
347            if ( in_array( $sent->get_error_code(), array( 'email_already_sent', 'password_not_set' ), true ) ) {
348                $show_recovery_form = false;
349            }
350            $this->protect_die( $sent, null, true, $show_recovery_form );
351        } else {
352            $this->render_recovery_success();
353        }
354    }
355
356    /**
357     * Whether the user registered but hasn't used their still-valid set-password link yet.
358     *
359     * The nag can outlive a working password (e.g. the install admin), so only a key issued at registration counts.
360     *
361     * @since 0.29.3
362     *
363     * @param WP_User $user The user.
364     *
365     * @return bool
366     */
367    public function user_has_unfinished_registration( $user ) {
368        if ( ! $this->check_unfinished_registrations || ! get_user_meta( $user->ID, 'default_password_nag', true ) ) {
369            return false;
370        }
371
372        $key_parts = explode( ':', (string) $user->user_activation_key, 2 );
373        if ( 2 !== count( $key_parts ) || ! ctype_digit( $key_parts[0] ) ) {
374            return false;
375        }
376
377        $key_issued_at = (int) $key_parts[0];
378        $registered_at = strtotime( $user->user_registered . ' UTC' );
379        if ( false === $registered_at || abs( $key_issued_at - $registered_at ) > self::REGISTRATION_KEY_WINDOW_IN_SECONDS ) {
380            return false;
381        }
382
383        /** This filter is documented in wp-includes/user.php */
384        $expiration = apply_filters( 'password_reset_expiration', DAY_IN_SECONDS );
385
386        return time() - $key_issued_at < $expiration;
387    }
388
389    /**
390     * Send the recovery form.
391     */
392    public function send_recovery_email() {
393        $email = isset( $_POST['email'] ) ? wp_unslash( $_POST['email'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only triggered after bypass-protect nonce check is done, and sanitization is checked on the next line.
394        if ( sanitize_email( $email ) !== $email || ! is_email( $email ) ) {
395            return new WP_Error( 'invalid_email', __( "Oops, looks like that's not the right email address. Please try again!", 'jetpack-waf' ) );
396        }
397        $user = get_user_by( 'email', trim( $email ) );
398
399        if ( ! $user ) {
400            return new WP_Error( 'invalid_user', __( "Oops, we couldn't find a user with that email. Please try again!", 'jetpack-waf' ) );
401        }
402
403        // A user who never set a password can't log in, so a recovery email wouldn't help them.
404        if ( $this->user_has_unfinished_registration( $user ) ) {
405            return new WP_Error( 'password_not_set', __( "This account doesn't have a password yet. Use the link in your registration email to set one, then try logging in again.", 'jetpack-waf' ) );
406        }
407
408        $this->email_address = $email;
409        $path                = sprintf( '/sites/%d/protect/recovery/request', Jetpack_Options::get_option( 'id' ) );
410
411        $response = Client::wpcom_json_api_request_as_blog(
412            $path,
413            '1.1',
414            array(
415                'method' => 'post',
416            ),
417            array(
418                'user_id' => $user->ID,
419                'ip'      => $this->ip_address,
420            )
421        );
422
423        $code   = wp_remote_retrieve_response_code( $response );
424        $result = json_decode( wp_remote_retrieve_body( $response ) );
425
426        if ( self::HTTP_STATUS_CODE_TOO_MANY_REQUESTS === $code ) {
427            // translators: email address the recovery instructions were sent to.
428            return new WP_Error( 'email_already_sent', sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack-waf' ), esc_html( $this->email_address ) ) );
429        } elseif ( is_wp_error( $result ) || empty( $result ) || isset( $result->error ) ) {
430            return new WP_Error( 'email_send_error', __( 'Oops, we were unable to send a recovery email. Try again.', 'jetpack-waf' ) );
431        }
432
433        return true;
434    }
435
436    /**
437     * Prevent login by locking the login page.
438     *
439     * @param string|WP_Error $content       - the content of the page.
440     * @param string|null     $title         - the page title.
441     * @param bool            $back_link     - the back link.
442     * @param bool            $recovery_form - the recovery form.
443     */
444    public function protect_die( $content, $title = null, $back_link = false, $recovery_form = false ) {
445        if ( empty( $title ) ) {
446            $title = __( 'Jetpack has locked your site\'s login page.', 'jetpack-waf' );
447        }
448        if ( is_wp_error( $content ) ) {
449            $svg     = '<svg class="gridicon gridicons-notice" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
450            $content = '<span class="error"> ' . $svg . $content->get_error_message() . '</span>';
451        }
452        $content = '<p>' . $content . '</p>';
453
454        // If for some reason the login pop up box show up in the wp-admin.
455        if ( isset( $_GET['interim-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes to the site itself, just rendering an error message.
456            $content = '<style>html{ background-color: #fff; } #error-message { margin:0 auto; padding: 1em; box-shadow: none; } </style>' . $content;
457        }
458        $this->display_page( $title, $content, $back_link, $recovery_form );
459    }
460
461    /**
462     * Render the recovery form.
463     */
464    public function render_recovery_form() {
465        $content = $this->get_html_blocked_login_message();
466        $this->protect_die( $content, null, false, true );
467    }
468
469    /**
470     * Render the recovery instructions.
471     */
472    public function render_recovery_success() {
473        // translators: the email address the recovery email was sent to.
474        $this->protect_die( sprintf( __( 'Recovery instructions were sent to %s. Check your inbox!', 'jetpack-waf' ), $this->email_address ) );
475    }
476
477    /**
478     * Get the HTML for the blocked login message.
479     */
480    public function get_html_blocked_login_message() {
481        $icon = '<svg class="gridicon gridicons-spam" style="fill:#d94f4f" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M17 2H7L2 7v10l5 5h10l5-5V7l-5-5zm-4 15h-2v-2h2v2zm0-4h-2l-.5-6h3l-.5 6z"/></g></svg>';
482        $ip   = str_replace( 'http://', '', esc_url( 'http://' . $this->ip_address ) );
483        return sprintf(
484            // translators: the IP address that was flagged.
485            __( '<p>Your IP address <code>%2$s</code> has been flagged for potential security violations. You can unlock your login by sending yourself a special link via email. <a href="%3$s">Learn More</a></p>', 'jetpack-waf' ), // phpcs:ignore WordPress.WP.I18n.NoHtmlWrappedStrings
486            $icon,
487            $ip,
488            esc_url( $this->get_help_url() )
489        );
490    }
491
492    /**
493     * Get the HTML recovery form.
494     */
495    public function get_html_recovery_form() {
496        // Build the markup as a string instead of using output buffering. Calling
497        // ob_start() while PHP is executing an output-buffer handler (e.g. a page
498        // cache or compression handler) raises a fatal: "Cannot use output buffering
499        // in output buffering display handlers".
500        return sprintf(
501            '<div>
502            <form method="post" action="?jetpack-protect-recovery=true">
503                %1$s
504                <p><label for="email">%2$s<br/></label>
505                    <input type="email" name="email" class="text-input"/>
506                    <input type="submit" class="button" value="%3$s"/>
507                </p>
508            </form>
509        </div>',
510            wp_nonce_field( 'bypass-protect', '_wpnonce', true, false ),
511            esc_html__( 'Your email', 'jetpack-waf' ),
512            esc_attr__( 'Send email', 'jetpack-waf' )
513        );
514    }
515
516    /**
517     * Display the page.
518     *
519     * @param string $title         - the page title.
520     * @param string $message       - the message we're sending.
521     * @param bool   $back_button   - the back button.
522     * @param bool   $recovery_form - the recovery form.
523     * @return never
524     */
525    public function display_page( $title, $message, $back_button = false, $recovery_form = false ) {
526
527        if ( ! headers_sent() ) {
528            nocache_headers();
529            header( 'Content-Type: text/html; charset=utf-8' );
530        }
531
532        $text_direction = 'ltr';
533        if ( is_rtl() ) {
534            $text_direction = 'rtl';
535        }
536        ?>
537        <!DOCTYPE html>
538        <html xmlns="http://www.w3.org/1999/xhtml"
539        <?php
540        if ( function_exists( 'language_attributes' ) && function_exists( 'is_rtl' ) ) {
541            language_attributes();
542        } else {
543            echo "dir='$text_direction'"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
544        }
545        ?>
546        >
547        <head>
548            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
549            <meta name="viewport" content="width=device-width">
550            <?php
551            if ( get_option( 'blog_public' ) ) {
552                echo "<meta name='robots' content='noindex,follow' />\n";
553            } else {
554                echo "<meta name='robots' content='noindex,nofollow' />\n";
555            }
556            ?>
557            <title><?php echo esc_html( $title ); ?></title>
558            <style type="text/css">
559                html {
560                    background: #f6f6f6;
561                }
562
563                body {
564                    color: #2e4453;
565                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
566                    margin: 2em auto;
567                    padding: 1em 2em;
568                    max-width: 460px;
569                    text-align: left;
570                }
571                body.is-rtl {
572                    text-align: right;
573                }
574                h1 {
575                    clear: both;
576                    color: #3d596d;
577                    font-size: 24px;
578                    margin:0 0 24px 0;
579                    padding: 0;
580                    font-weight: 400;
581                }
582
583                #error-message {
584                    box-sizing: border-box;
585                    background: white;
586                    box-shadow: 0 0 0 1px rgba(200, 215, 225, 0.5), 0 1px 2px #e9eff3;
587                    padding: 24px;
588                }
589
590                #error-message img {
591                    margin: 0 auto;
592                    display: block;
593                }
594
595                #error-page {
596                    margin-top: 50px;
597                }
598
599                #error-page p {
600                    font-size: 14px;
601                    line-height: 1.5;
602                    margin: 24px 0 0;
603                }
604
605                #error-page code {
606                    font-family: Consolas, Monaco, monospace;
607                }
608
609                ul li {
610                    margin-bottom: 10px;
611                    font-size: 14px;
612                }
613
614                a {
615                    color: #00aadc;
616                }
617
618                label {
619                    font-weight: bold;
620                    font-size:16px;
621                }
622
623                a:hover,
624                a:active {
625                    color: #0085be;
626                }
627
628                a:focus {
629                    color: #124964;
630                    -webkit-box-shadow: 0 0 0 1px #4f94d4,
631                    0 0 2px 1px rgba(30, 140, 190, .8);
632                    box-shadow: 0 0 0 1px #4f94d4,
633                    0 0 2px 1px rgba(30, 140, 190, .8);
634                    outline: none;
635                }
636
637                .button {
638                    background: #00aadc;
639                    color: white;
640                    border-color: #008ab3;
641                    border-style: solid;
642                    border-width: 1px 1px 2px;
643                    cursor: pointer;
644                    display: inline-block;
645                    margin: 0;
646                    margin-right: 0;
647                    outline: 0;
648                    overflow: hidden;
649                    font-weight: 500;
650                    text-overflow: ellipsis;
651                    text-decoration: none;
652                    vertical-align: top;
653                    box-sizing: border-box;
654                    font-size: 14px;
655                    line-height: 21px;
656                    border-radius: 4px;
657                    padding: 7px 14px 9px;
658                    -webkit-appearance: none;
659                    -moz-appearance: none;
660                    appearance: none;
661                    font-size: 14px;
662                    width: 100%;
663                }
664
665                .button:hover,
666                .button:focus {
667                    border-color: #005082;
668                    outline: none;
669                }
670
671                .button:focus {
672                    border-color: #005082;
673                    -webkit-box-shadow: 0 0 3px rgba(0, 115, 170, .8);
674                    box-shadow: 0 0 3px rgba(0, 115, 170, .8);
675                    outline: none;
676                }
677                .button::-moz-focus-inner {
678                    border: 0;
679                }
680
681                .button:active {
682                    border-width: 2px 1px 1px;
683                }
684                .gridicon {
685                    fill: currentColor;
686                    vertical-align: middle;
687                }
688                #error-footer {
689                    padding: 16px;
690                }
691                #error-footer a {
692                    text-decoration: none;
693                    line-height:20px;
694                    font-size: 14px;
695                    color: #4f748e;
696                }
697                #error-footer a:hover {
698                    color: #2e4453;
699                }
700                #error-footer .gridicon{
701                    width: 16px;
702                }
703                #error-footer .gridicons-help {
704                    width: 24px;
705                    margin-right:8px;
706                }
707
708                .is-rtl #error-footer .gridicons-help {
709                    margin-left:8px;
710                }
711
712                .error {
713                    background: #d94f4f;
714                    color:#FFF;
715                    display: block;
716                    border-radius: 3px;
717                    line-height: 1.5;
718                    padding: 16px;
719                    padding-left: 42px;
720                }
721                .is-rtl .error {
722                    padding-right: 42px;
723                }
724                .error .gridicon {
725                    float: left;
726                    margin-left: -32px;
727                }
728
729                .is-rtl .error .gridicon {
730                    float: right;
731                    margin-right: -32px;
732                }
733
734                .text-input {
735                    margin: 0;
736                    padding: 7px 14px;
737                    width: 100%;
738                    color: #2e4453;
739                    font-size: 16px;
740                    line-height: 1.5;
741                    border: 1px solid #c8d7e1;
742                    background-color: white;
743                    transition: all .15s ease-in-out;
744                    box-sizing: border-box;
745                    margin: 8px 0 16px;
746                }
747                #image {
748                    display: block;
749                    width: 200px;
750                    margin: 0 auto;
751                }
752                <?php
753                $rtl_class = '';
754                if ( 'rtl' === $text_direction ) {
755                    $rtl_class = 'class="is-rtl"';
756                    echo 'body { font-family: Tahoma, Arial; }';
757                }
758                ?>
759            </style>
760        </head>
761        <body id="error-page" <?php echo $rtl_class; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
762            <h1 id="error-title"><?php echo esc_html( $title ); ?></h1>
763            <div id="error-message">
764                <svg id="image" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 250 134">
765                    <path fill="#E9EFF4" d="M205.2,129.8c3.7-0.7,7.4-0.9,11.1-1.1l5.5-0.1l5.5,0c3.7,0,7.4,0.1,11.1,0.2c3.7,0.1,7.4,0.3,11.1,0.8 c0.3,0,0.5,0.3,0.5,0.6c0,0.2-0.2,0.4-0.5,0.5c-3.7,0.5-7.4,0.6-11.1,0.8c-3.7,0.1-7.4,0.2-11.1,0.2l-5.5,0l-5.5-0.1 c-3.7-0.1-7.4-0.4-11.1-1.1c-0.1,0-0.2-0.2-0.2-0.3C205,129.9,205.1,129.8,205.2,129.8"/>
766                    <path fill="#E9EFF4" d="M0.2,130.9c3-0.7,5.9-0.9,8.9-1.1l4.4-0.1l4.4,0c3,0,5.9,0.1,8.9,0.2c3,0.1,5.9,0.3,8.9,0.8 c0.3,0,0.5,0.3,0.4,0.6c0,0.2-0.2,0.4-0.4,0.4c-3,0.5-5.9,0.6-8.9,0.8c-3,0.1-5.9,0.2-8.9,0.2l-4.4,0l-4.4-0.1 c-3-0.1-5.9-0.4-8.9-1.1c-0.1,0-0.2-0.2-0.2-0.3C0,131,0.1,130.9,0.2,130.9"/>
767                    <path fill="#C8D7E2" d="M101.6,130.1H70.1V52.5c0-8.5,6.9-15.3,15.3-15.3h16.1V130.1z"/>
768                    <path fill="#0DA9DD" d="M191.5,130.1h-73.8v-5.4c0-8.9,7.2-16.1,16.1-16.1h57.7V130.1z"/>
769                    <path fill="#C7E9F5" d="M55.2,25.6l-0.1,9.8L55,57l-0.1,21.6c0,0.2,0.2,0.4,0.4,0.4c0.2,0,0.4-0.2,0.4-0.4L56.6,57l0.8-21.6 c0.1-3.3,0.2-6.5,0.3-9.8H55.2z"/>
770                    <path fill="#C7E9F5" d="M203.1,25.6l0.1,18.1c0.2,28.8,0.4,57.6,1.2,86.3c0,0.4,0.4,0.8,0.8,0.8c0.4,0,0.8-0.3,0.8-0.8 c0.8-28.8,1-57.6,1.2-86.3l0.1-18.1H203.1z"/>
771                    <path fill="#7FD3F2" d="M55.3,25.6v-8.2v-6.8c0-5.9,4-10.7,9-10.7h134c5,0,9,4.8,9,10.7v14.9H55.3z"/>
772                    <path fill="#005083" d="M210.7,25.6c-13.3,1.1-26.7,1-40,1l-40,0.2l-40-0.2c-13.3-0.1-26.7,0-40-1V25c13.3-1.1,26.7-1,40-1l40-0.2 l40,0.2c13.3,0.1,26.7,0,40,1V25.6z"/>
773                    <polygon fill="#C7E9F5" points="168.7,95.6 117.7,95.6 117.7,44.6     "/>
774                    <path fill="#C8D7E2" d="M191.5,56.5c0,11-8.9,19.9-19.9,19.9c-11,0-19.9-8.9-19.9-19.9c0-11,8.9-19.9,19.9-19.9 C182.6,36.6,191.5,45.5,191.5,56.5"/>
775                    <path fill="#FFFFFF" d="M213.2,95.5c-3.3-5.1-3.2-16.7-3.2-28.4h-32.3c0,0-5.2,25.5,4.6,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
776                    <path fill="#C8D7E2" d="M213.5,95.3l-0.1-0.1l-0.3-0.5c-0.2-0.4-0.3-0.7-0.5-1.1c-0.3-0.8-0.5-1.6-0.7-2.4c-0.1-0.5-0.2-1.1-0.3-1.6 c-0.4,0-0.8,0-1.2,0c0.5,2.1,1.1,4.3,2.4,6.1l0.2,0.2c0.2,0,0.4-0.1,0.5-0.3C213.6,95.5,213.6,95.4,213.5,95.3L213.5,95.3z"/>
777                    <path fill="#C8D7E2" d="M212.5,98.6c-0.1,0-0.2,0-0.3,0l-0.1,0H212l-0.3,0l-0.6,0l-1.3,0l-2.5,0l-5,0l-19.5,0.2 c-1.9-1.7-3.1-4.1-3.8-6.5c-0.8-2.6-1.1-5.4-1.2-8.2c-0.2-5.2,0.3-10.4,1.1-15.6l5.7-0.1c0-0.9,0-1.8,0-2.6l-4.4,0l-2.5,0 c-0.4,0-0.8,0.2-1,0.5c-0.1,0.2-0.2,0.3-0.3,0.5l-0.1,0.3l-0.2,1.2c-0.3,1.7-0.5,3.3-0.7,5c-0.3,3.3-0.5,6.7-0.4,10.1 c0.1,3.4,0.5,6.7,1.5,10c0.5,1.6,1.2,3.2,2.2,4.7c0.5,0.7,1,1.4,1.7,2c0.3,0.3,0.6,0.6,1,0.9l0.1,0.1c0.1,0,0.2,0.1,0.3,0.2 c0.2,0.1,0.5,0.1,0.6,0.1l0.6,0l20-0.6l5-0.2l2.5-0.1l1.2,0l0.3,0l0.2,0c0,0,0.3,0,0.4-0.1c0.3-0.2,0.5-0.5,0.5-0.9 C213.1,99.1,212.9,98.7,212.5,98.6z"/>
778                    <path fill="#FFFFFF" d="M223.1,84.8c-3.3-5.1-4.8-16.7-4.8-28.4h-32.3c0,0-3.5,25.5,6.3,33c7.5-0.1,29.9-0.6,29.9-0.6"/>
779                    <path fill="#C8D7E2" d="M222.9,84.9c-1.3-2.1-2.2-4.4-2.8-6.7c-0.6-2.4-1.1-4.8-1.5-7.2c-0.7-4.8-1-9.1-1-13.9l0,0l-31,0.1l0,0 c-0.4,2.8-0.5,5.1-0.5,7.9c-0.1,2.9,0,5.7,0.3,8.6c0.3,2.8,0.8,5.7,1.7,8.3c0.9,2.6,2.3,5.2,4.5,6.9l-0.4-0.1l14.9-0.2 c5-0.1,10-0.1,14.9-0.1c0.1,0,0.3,0.1,0.3,0.3c0,0.1-0.1,0.3-0.2,0.3c-5,0.2-10,0.4-14.9,0.5l-14.9,0.4c-0.1,0-0.3,0-0.4-0.1l0,0 c-2.5-1.9-3.9-4.7-5-7.4c-1-2.8-1.5-5.7-1.9-8.6c-0.3-2.9-0.4-5.8-0.4-8.8c0.1-2.9,0.2-5.8,0.6-8.8c0-0.4,0.4-0.6,0.7-0.6h0 l32.3,0.1h0c0.3,0,0.6,0.3,0.6,0.6v0c0,4.8,0.2,9.6,0.7,14.4c0.3,2.4,0.6,4.8,1.2,7.1c0.5,2.3,1.2,4.7,2.4,6.8c0,0.1,0,0.1,0,0.2 C223.1,85,223,85,222.9,84.9"/>
780                    <path fill="#C8D7E2" d="M192.1,67.1c1.6-0.9,3.4-1.2,5.1-1.3c1.7-0.2,3.5-0.2,5.2-0.2c3.5,0.1,6.9,0.2,10.3,1c0.1,0,0.2,0.2,0.2,0.3 c0,0.1-0.1,0.2-0.2,0.2c-3.4,0.2-6.9,0-10.3,0c-1.7,0-3.4,0-5.1,0c-1.7,0-3.4,0.1-5.1,0.3l0,0c-0.1,0-0.1,0-0.1-0.1 C192,67.2,192.1,67.1,192.1,67.1"/>
781                    <path fill="#C8D7E2" d="M194.1,74c1.4,0,2.7,0,4.1,0c1.4,0,2.7,0,4.1,0c2.7,0,5.4-0.1,8.2-0.2c0.1,0,0.3,0.1,0.3,0.3 c0,0.1-0.1,0.2-0.2,0.3c-1.3,0.5-2.7,0.7-4.1,0.9c-1.4,0.2-2.8,0.2-4.2,0.3c-1.4,0-2.8,0-4.2-0.2c-1.4-0.2-2.8-0.4-4.1-1.1 c-0.1,0-0.1-0.1,0-0.2C193.9,74.1,194,74,194.1,74L194.1,74z"/>
782                    <path fill="#86A6BD" d="M40.2,88.6c-0.5,0-0.8-0.4-0.9-0.9l-0.1-8.2c0-0.7,0-1.4,0-2.1c0.1-0.7,0.2-1.5,0.4-2.2c0.4-1.4,1-2.8,1.9-4 c1.7-2.5,4.3-4.3,7.1-5.1c0.7-0.2,1.5-0.3,2.2-0.5c0.7-0.1,1.5-0.1,2.2-0.1c1.3,0,2.9,0,4.4,0.4c2.9,0.7,5.6,2.5,7.4,4.9 c0.9,1.2,1.6,2.6,2.1,4c0.5,1.4,0.6,3,0.6,4.4l0,16.4c0,0.7-0.6,1.3-1.3,1.3l-6.7,0c-0.7,0-1.3-0.6-1.3-1.3v0l0-10.8l0-5.4 c0-1.4-0.7-2.8-1.8-3.5c-0.6-0.4-1.3-0.6-2-0.7c-0.7,0-1.9,0-2.5,0c-1.4,0.1-2.7,1-3.3,2.3c-0.3,0.7-0.4,1.3-0.4,2.1l0,2.7 l-0.1,5.4l0,0c0,0.5-0.4,0.9-1,0.9"/>
783                    <path fill="#FFFFFF" d="M41.1,86.9l0.1-7.3c-0.1-2.6,0.7-5,2.1-7.1c1.4-2,3.6-3.5,5.9-4.1c0.6-0.2,1.2-0.3,1.8-0.3 c0.6,0,1.2-0.1,1.9,0c1.4,0,2.5,0,3.7,0.4c2.4,0.6,4.5,2,5.9,4c0.7,1,1.3,2.1,1.6,3.2c0.4,1.2,0.5,2.3,0.5,3.7l0,15.1l0,0l-4.2,0 l0-9.5l0-5.4c0-2.2-1.2-4.4-3-5.5c-0.9-0.6-2-0.9-3.1-1c-1.1,0-1.7,0-2.9,0c-2.2,0.2-4.2,1.7-5.1,3.6c-0.5,0.9-0.7,2.1-0.6,3.1 l0,2.7l0.1,4.4l0,0L41.1,86.9L41.1,86.9"/>
784                    <path fill="#86A6BD" d="M36.3,133c-1.9,0-3.8-1.1-4.8-2.8c-0.5-0.8-0.7-1.8-0.7-2.8l0-2.4l0-9.6l-0.1-9.6l0-4.8c0-0.7,0-1.8,0.3-2.8 c0.3-1,0.9-1.8,1.7-2.5c0.8-0.6,1.7-1.1,2.7-1.3c1.1-0.2,1.8-0.1,2.6-0.1l4.8,0l9.6-0.1l19.2,0c2.1,0,4.1,1.2,5.1,3 c0.5,0.9,0.8,2,0.8,3l0,2.4l0,9.6l-0.1,9.6l0,4.8c0,0.7,0,1.8-0.4,2.8c-0.3,0.9-1,1.8-1.7,2.4c-0.8,0.6-1.7,1.1-2.7,1.2 c-1.1,0.1-1.8,0-2.6,0.1l-4.8,0l-9.6-0.1L36.3,133z"/>
785                    <path fill="#FFFFFF" d="M74.8,112.3l-0.1-9.6l0-2.4c0-0.6-0.1-1.1-0.4-1.6c-0.6-1-1.7-1.6-2.8-1.6l-19.2,0L42.7,97l-4.8,0 c-0.8,0-1.7,0-2.2,0c-0.6,0.1-1.1,0.3-1.6,0.7c-0.5,0.4-0.8,0.9-1,1.4c-0.2,0.6-0.2,1.1-0.2,2l0,4.8l-0.1,9.6l0,9.6l0,2.4 c0,0.6,0.2,1.3,0.5,1.8c0.6,1.1,1.9,1.8,3.1,1.8l19.2-0.1l9.6-0.1l4.8,0c0.8,0,1.7,0,2.2-0.1c0.6-0.1,1.2-0.4,1.6-0.8 c0.5-0.4,0.8-0.9,1-1.5c0.2-0.6,0.2-1.1,0.2-2l0-4.8L74.8,112.3z"/>
786                    <path fill="#86A6BD" d="M48.1,121.4l2.9-6.2c0.3-0.6,0.2-1.3-0.3-1.8c-1-1-1.5-2.5-1.2-4c0.3-1.7,1.7-3.1,3.4-3.4 c2.9-0.6,5.4,1.6,5.4,4.4c0,1.2-0.5,2.3-1.3,3.1c-0.5,0.5-0.6,1.2-0.3,1.8l2.9,6.2c0.1,0.2-0.1,0.5-0.3,0.5H48.4 C48.1,121.9,48,121.6,48.1,121.4"/>
787                </svg>
788
789                <?php echo $message; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- message includes HTML that's marked up ourselves. ?>
790                <?php
791                if ( $recovery_form ) {
792                    echo $this->get_html_recovery_form(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- content is escaped in the function.
793                }
794                ?>
795            </div>
796            <div id="error-footer">
797            <?php
798            if ( $back_button && ! $recovery_form ) {
799                if ( 'rtl' === $text_direction ) {
800                    $back_button_icon = '<svg class="gridicon gridicons-arrow-right" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></g></svg>';
801                } else {
802                    $back_button_icon = '<svg class="gridicon gridicons-arrow-left" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></g></svg>';
803                }
804                ?>
805                <a href='javascript:history.back()'
806                <?php
807                printf(
808                    /* translators: %s is HTML markup, for a back icon. */
809                    __( '%s Back', 'jetpack-waf' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
810                    $back_button_icon // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
811                );
812                ?>
813                </a>
814                <?php
815            } else {
816                $help_icon = '<svg class="gridicon gridicons-help" height="24" width="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 16h-2v-2h2v2zm0-4.14V15h-2v-2c0-.552.448-1 1-1 1.103 0 2-.897 2-2s-.897-2-2-2-2 .897-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.862-1.278 3.413-3 3.86z"/></g></svg>';
817                ?>
818                    <a href="<?php echo esc_url( $this->get_help_url() ); ?>" rel="noopener noreferrer" target="_blank">
819                        <?php
820                        printf(
821                            /* translators: %s is HTML markup, for a help icon. */
822                            __( '%s Get help unlocking your site', 'jetpack-waf' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
823                            $help_icon // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
824                        );
825                        ?>
826                    </a>
827            <?php } ?>
828            </div>
829        </body>
830        </html>
831        <?php
832        die( 0 );
833    }
834}