Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Admin_Bar_Notice
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 9
600
0.00% covered (danger)
0.00%
0 / 1
 instance
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 init_hooks
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 should_try_to_display_notice
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 enqueue_toolbar_script
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 add_inline_styles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 add_threats_to_toolbar
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
20
 get_icon
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 has_threats
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_threat_count
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * A class that adds the scan notice to the admin bar.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Scan;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Redirect;
12use WP_Admin_Bar;
13
14/**
15 * Class Main
16 *
17 * Responsible for loading the admin bar notice if threats are found.
18 *
19 * @package Automattic\Jetpack\Scan
20 */
21class Admin_Bar_Notice {
22    const SCRIPT_NAME    = 'jetpack-scan-show-notice';
23    const SCRIPT_VERSION = '1';
24
25    /**
26     * The singleton instance of this class.
27     *
28     * @var Admin_Bar_Notice
29     */
30    protected static $instance;
31
32    /**
33     * Get the singleton instance of the class.
34     *
35     * @return Admin_Bar_Notice
36     */
37    public static function instance() {
38        if ( ! isset( self::$instance ) ) {
39            self::$instance = new Admin_Bar_Notice();
40            self::$instance->init_hooks();
41        }
42
43        return self::$instance;
44    }
45    /**
46     * Initalize the hooks as needed.
47     */
48    private function init_hooks() {
49        if ( ! $this->should_try_to_display_notice() ) {
50            return;
51        }
52
53        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_toolbar_script' ) );
54        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_toolbar_script' ) );
55        add_action( 'admin_bar_menu', array( $this, 'add_threats_to_toolbar' ), 999 );
56
57        // Inject the data-ampdevmode attribute into the inline <script> output via wp_localize_script(). To revisit after https://github.com/ampproject/amp-wp/issues/4598.
58        add_filter(
59            'amp_dev_mode_element_xpaths',
60            static function ( $expressions ) {
61                $expressions[] = '//script[ contains( text(), "Jetpack_Scan" ) ]';
62                return $expressions;
63            }
64        );
65    }
66
67    /**
68     * Whether to even try to display the notice or now.
69     *
70     * @return bool
71     */
72    private function should_try_to_display_notice() {
73        // Jetpack Scan is currently not supported on multisite.
74        if ( is_multisite() ) {
75            return false;
76        }
77
78        // Check if VaultPress is active, the assumtion there is that VaultPress is working.
79        // It has its own notice in the admin bar.
80        if ( class_exists( 'VaultPress' ) ) {
81            return false;
82        }
83
84        // Check if Protect is active.
85        // It has its own notice in the admin bar.
86        if ( class_exists( 'Jetpack_Protect' ) ) {
87            return false;
88        }
89
90        // Only show the notice to admins.
91        if ( ! current_user_can( 'manage_options' ) ) {
92            return false;
93        }
94
95        return true;
96    }
97
98    /**
99     * Add the inline styles and scripts if they are needed.
100     */
101    public function enqueue_toolbar_script() {
102        $this->add_inline_styles();
103
104        if ( $this->has_threats() !== null ) {
105            return;
106        }
107
108        // We don't know about threats in the cache lets load the JS that fetches the info and updates the admin bar.
109        Assets::register_script(
110            self::SCRIPT_NAME,
111            '_inc/build/scan/admin-bar-notice.min.js',
112            JETPACK__PLUGIN_FILE,
113            array(
114                'in_footer'    => true,
115                'strategy'     => 'defer',
116                'nonmin_path'  => 'modules/scan/admin-bar-notice.js',
117                'dependencies' => array( 'admin-bar' ),
118                'version'      => self::SCRIPT_VERSION,
119                'enqueue'      => true,
120            )
121        );
122        $script_data = array(
123            'nonce'              => wp_create_nonce( 'wp_rest' ),
124            'scan_endpoint'      => get_rest_url( null, 'jetpack/v4/scan' ),
125            'scan_dashboard_url' => Redirect::get_url( 'calypso-scanner' ),
126            /* translators: %s is the alert icon */
127            'singular'           => sprintf( esc_html__( '%s Threat found', 'jetpack' ), $this->get_icon() ),
128            /* translators: %s is the alert icon */
129            'multiple'           => sprintf( esc_html__( '%s Threats found', 'jetpack' ), $this->get_icon() ),
130        );
131        wp_localize_script( self::SCRIPT_NAME, 'Jetpack_Scan', $script_data );
132    }
133
134    /**
135     * Adds the inline styles if they are needed.
136     */
137    public function add_inline_styles() {
138        // We know there are no threats so lets not include any css.
139        if ( false === $this->has_threats() ) {
140            return;
141        }
142
143        // We might be showing the threats in the admin bar lets make sure that they look great!
144        $hide_wording_on_mobile = '#wp-admin-bar-jetpack-scan-notice .is-hidden { display:none; } @media screen and (max-width: 959px ) { #wpadminbar #wp-admin-bar-jetpack-scan-notice { width:32px; } #wpadminbar #wp-admin-bar-jetpack-scan-notice a { color: transparent!important; } }';
145        $style                  = '#wp-admin-bar-jetpack-scan-notice svg { float:left; margin-top: 4px; margin-right: 6px; width: 18px; height: 22px; }' . $hide_wording_on_mobile;
146        if ( is_rtl() ) {
147            $style = '#wp-admin-bar-jetpack-scan-notice svg { float:right; margin-top: 4px; margin-left: 6px; width: 18px; height: 22px; }' . $hide_wording_on_mobile;
148        }
149        wp_add_inline_style( 'admin-bar', $style );
150    }
151
152    /**
153     * Add the link to the admin bar.
154     *
155     * @param WP_Admin_Bar $wp_admin_bar WP Admin Bar class object.
156     */
157    public function add_threats_to_toolbar( $wp_admin_bar ) {
158        if ( ! $this->should_try_to_display_notice() ) {
159            return;
160        }
161
162        $has_threats = $this->has_threats();
163        if ( false === $has_threats ) {
164            return;
165        }
166
167        $node = array(
168            'id'     => 'jetpack-scan-notice',
169            'title'  => '',
170            'parent' => 'top-secondary',
171            'meta'   => array(
172                'title' => esc_attr__( 'View security scan details', 'jetpack' ),
173                'class' => 'error is-hidden',
174            ),
175        );
176
177        if ( $has_threats ) {
178            $node['href']           = esc_url( Redirect::get_url( 'calypso-scanner' ) );
179            $node['meta']['target'] = '_blank';
180            $node['meta']['rel']    = 'noopener noreferrer';
181            $node['meta']['class']  = 'error';
182            $node['title']          = sprintf(
183                esc_html(
184                /* translators: %s is the alert icon */
185                    _n( '%s Threat found', '%s Threats found', $this->get_threat_count(), 'jetpack' )
186                ),
187                $this->get_icon()
188            );
189        }
190
191        $wp_admin_bar->add_node( $node );
192    }
193
194    /**
195     * Returns the shield icon.
196     *
197     * @return string
198     */
199    private function get_icon() {
200        return '<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 0L0 4V10C0 15.55 3.84 20.74 9 22C14.16 20.74 18 15.55 18 10V4L9 0Z" fill="#D63638"/><path d="M7.99121 6.00894H10.0085V11.9968H7.99121V6.00894Z" fill="#FFF"/><path d="M7.99121 14.014H10.0085V15.9911H7.99121V14.014Z" fill="#FFF"/></svg>';
201    }
202
203    /**
204     *
205     * Return Whether boolean cached threats exist or null if the state is unknown.
206     * * @return boolean or null
207     */
208    public function has_threats() {
209        $scan_state = get_transient( 'jetpack_scan_state' );
210        if ( empty( $scan_state ) ) {
211            return null;
212        }
213        // Return true if there is at least one threat found.
214        return isset( $scan_state->threats[0] );
215    }
216
217    /**
218     * Returns the number of threats found or 0.
219     *
220     * @return int
221     */
222    public function get_threat_count() {
223        if ( ! $this->has_threats() ) {
224            return 0;
225        }
226
227        $scan_state = get_transient( 'jetpack_scan_state' );
228        return is_array( $scan_state->threats ) ? count( $scan_state->threats ) : 0;
229    }
230}