Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.55% covered (success)
94.55%
52 / 55
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Admin_Sidebar_Link
94.55% covered (success)
94.55%
52 / 55
70.00% covered (warning)
70.00%
7 / 10
32.17
0.00% covered (danger)
0.00%
0 / 1
 instance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 init_hooks
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 maybe_add_admin_link
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
5
 refresh_state_cache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 should_show_link
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 should_show_scan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 should_show_scan_history_only
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 has_scan
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 has_protect_page
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 maybe_refresh_transient_cache
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2/**
3 * A class that adds a scan link to the admin sidebar.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Scan;
9
10use Automattic\Jetpack\Admin_UI\Admin_Menu;
11use Automattic\Jetpack\My_Jetpack\Products\Protect;
12use Automattic\Jetpack\Redirect;
13use Automattic\Jetpack\Status;
14use Automattic\Jetpack\Status\Host;
15use Jetpack_Core_Json_Api_Endpoints;
16
17/**
18 * Class Main
19 *
20 * Responsible for showing the link if available.
21 *
22 * @package Automattic\Jetpack\Scan
23 */
24class Admin_Sidebar_Link {
25
26    const SCHEDULE_ACTION_HOOK = 'jetpack_scan_refresh_states_event';
27
28    /**
29     * The singleton instance of this class.
30     *
31     * @var Admin_Sidebar_Link
32     */
33    protected static $instance;
34
35    /**
36     * Used to check if we need to schedule the refresh or we need to do it.
37     *
38     * @var boolean | null
39     */
40    private $schedule_refresh_checked;
41
42    /**
43     * Get the singleton instance of the class.
44     *
45     * @return Admin_Sidebar_Link
46     */
47    public static function instance() {
48        if ( ! isset( self::$instance ) ) {
49            self::$instance = new Admin_Sidebar_Link();
50            self::$instance->init_hooks();
51        }
52
53        return self::$instance;
54    }
55
56    /**
57     * Adds action hooks.
58     */
59    public function init_hooks() {
60        add_action( 'jetpack_admin_menu', array( $this, 'maybe_add_admin_link' ), 99 );
61        add_action( self::SCHEDULE_ACTION_HOOK, array( $this, 'refresh_state_cache' ) );
62    }
63
64    /**
65     * Adds a link to the Scan page.
66     */
67    public function maybe_add_admin_link() {
68        if ( ! $this->should_show_link() ) {
69            return;
70        }
71
72        $position = defined( Admin_Menu::class . '::POSITION_EXTERNAL' ) ? Admin_Menu::POSITION_EXTERNAL : 100;
73
74        if ( $this->should_show_scan() ) {
75            Admin_Menu::add_menu(
76                /** "Scan" is a product name, do not translate. */
77                'Scan',
78                'Scan <span aria-hidden="true">↗</span>',
79                'manage_options',
80                esc_url( Redirect::get_url( 'cloud-scan-history-wp-menu' ) ),
81                null,
82                $position,
83                array( 'key' => 'jetpack-scan-cloud' )
84            );
85        }
86
87        // Add scan item which shows history page only. This is mutally exclusive from the scan item above and is only shown for Atomic sitse.
88        if ( $this->should_show_scan_history_only() ) {
89            Admin_Menu::add_menu(
90                /** "Scan" is a product name, do not translate. */
91                'Scan',
92                'Scan <span aria-hidden="true">↗</span>',
93                'manage_options',
94                esc_url( Redirect::get_url( 'cloud-scan-history-wp-menu' ) ),
95                null,
96                $position,
97                array( 'key' => 'jetpack-scan-cloud' )
98            );
99        }
100    }
101
102    /**
103     * Refreshes the state cache via API call. Called via cron.
104     *
105     * Still warms the rewind state, which this class no longer reads itself:
106     * Jetpack_Core_Json_Api_Endpoints::rewind_data() serves it from that cache.
107     */
108    public function refresh_state_cache() {
109        if ( ( new Status() )->is_offline_mode() ) {
110            return;
111        }
112
113        Jetpack_Core_Json_Api_Endpoints::get_scan_state();
114        Jetpack_Core_Json_Api_Endpoints::get_rewind_data();
115    }
116
117    /**
118     * Returns true if the link should appear.
119     *
120     * @return boolean
121     */
122    private function should_show_link() {
123        // Jetpack Scan is currently not supported on multisite.
124        if ( is_multisite() ) {
125            return false;
126        }
127
128        // Check if VaultPress is active, the assumption there is that VaultPress is working.
129        // It has its link the adminbar.
130        if ( class_exists( 'VaultPress' ) ) {
131            return false;
132        }
133
134        return $this->should_show_scan() || $this->should_show_scan_history_only();
135    }
136
137    /**
138     * Check if we should display the Scan menu item.
139     *
140     * It will only be displayed if site has Scan enabled, is not an Atomic site, and no Protect page is registered, because that has a menu item of its own.
141     *
142     * @return boolean
143     */
144    private function should_show_scan() {
145        return $this->has_scan() && ! $this->has_protect_page() && ! ( new Host() )->is_woa_site();
146    }
147
148    /**
149     * Check if we should display the Scan menu item history.
150     *
151     * It will only be displayed if site has Scan enabled, is an Atomic site.
152     *
153     * @return boolean
154     */
155    private function should_show_scan_history_only() {
156        return $this->has_scan() && ( new Host() )->is_woa_site() && get_option( 'wpcom_admin_interface' ) === 'wp-admin';
157    }
158
159    /**
160     * Detects if Scan is enabled.
161     *
162     * @return boolean
163     */
164    private function has_scan() {
165        $this->maybe_refresh_transient_cache();
166        $scan_state = get_transient( 'jetpack_scan_state' );
167        if ( ! $scan_state ) {
168            return false;
169        }
170
171        return isset( $scan_state->state ) && 'unavailable' !== $scan_state->state;
172    }
173
174    /**
175     * Detects if the Protect plugin or the Protect dashboard module provides the Protect page.
176     *
177     * @return boolean
178     */
179    private function has_protect_page() {
180        if ( method_exists( Protect::class, 'has_protect_page' ) ) {
181            return Protect::has_protect_page();
182        }
183        return class_exists( 'Jetpack_Protect' ) || did_action( 'jetpack_protect_dashboard_initialized' );
184    }
185
186    /**
187     * Triggers a cron job to refresh the Scan and Rewind state cache.
188     */
189    private function maybe_refresh_transient_cache() {
190        if ( ( new Status() )->is_offline_mode() || $this->schedule_refresh_checked ) {
191            return;
192        }
193
194        // Do we have a jetpack_scan and jetpack_rewind state set?
195        if ( get_transient( 'jetpack_scan_state' ) && get_transient( 'jetpack_rewind_state' ) ) {
196            return;
197        }
198
199        if ( false === wp_next_scheduled( self::SCHEDULE_ACTION_HOOK ) ) {
200            wp_schedule_single_event( time(), self::SCHEDULE_ACTION_HOOK );
201        }
202
203        $this->schedule_refresh_checked = true;
204    }
205}