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