Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.28% covered (danger)
36.28%
78 / 215
32.14% covered (danger)
32.14%
9 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Protect
36.62% covered (danger)
36.62%
78 / 213
32.14% covered (danger)
32.14%
9 / 28
1175.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
33.33% covered (danger)
33.33%
18 / 54
0.00% covered (danger)
0.00%
0 / 1
8.74
 filter_opt_in_modules
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 drop_leftover_opt_in_module
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
7
 record_opt_in
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 forget_opt_in
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_opted_in_modules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 init
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 admin_page_init
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 enqueue_admin_styles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enqueue_admin_scripts
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 render_initial_state
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initial_state
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
2
 plugin_settings_page
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 plugin_activation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 do_plugin_activation_activities
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 activate_modules
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 plugin_deactivation
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 should_notify_threats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 admin_bar
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 is_protect_page
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 protect_filter_available_modules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jetpack_check_user_licenses
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 get_waf_upgrade_seen_status
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_waf_upgrade_seen_status
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_waf_upgrade_badge_timestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_waf_upgrade_badge_timestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_waf_upgrade_badge_display_status
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 get_waf_stats
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Primary class file for the Jetpack Protect plugin.
4 *
5 * @package automattic/jetpack-protect-plugin
6 */
7
8if ( ! defined( 'ABSPATH' ) ) {
9    exit( 0 );
10}
11
12use Automattic\Jetpack\Account_Protection\Settings as Account_Protection_Settings;
13use Automattic\Jetpack\Activity_Log\Jetpack_Activity_Log;
14use Automattic\Jetpack\Admin_UI\Admin_Menu;
15use Automattic\Jetpack\Assets;
16use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
17use Automattic\Jetpack\Connection\Manager as Connection_Manager;
18use Automattic\Jetpack\Connection\SSO;
19use Automattic\Jetpack\IP\Utils as IP_Utils;
20use Automattic\Jetpack\JITMS\JITM;
21use Automattic\Jetpack\Modules;
22use Automattic\Jetpack\Monitor\Monitor;
23use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
24use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
25use Automattic\Jetpack\Plugins_Installer;
26use Automattic\Jetpack\Protect\Credentials;
27use Automattic\Jetpack\Protect\Dashboard;
28use Automattic\Jetpack\Protect\Onboarding;
29use Automattic\Jetpack\Protect\REST_Controller;
30use Automattic\Jetpack\Protect\Scan_History;
31use Automattic\Jetpack\Protect\Site_Health;
32use Automattic\Jetpack\Protect\Threats;
33use Automattic\Jetpack\Protect_Status\Plan;
34use Automattic\Jetpack\Protect_Status\Protect_Status;
35use Automattic\Jetpack\Protect_Status\Scan_Status;
36use Automattic\Jetpack\Protect_Status\Status;
37use Automattic\Jetpack\Status as Jetpack_Status;
38use Automattic\Jetpack\Sync\Functions as Sync_Functions;
39use Automattic\Jetpack\Sync\Sender;
40use Automattic\Jetpack\Waf\Waf_Runner;
41use Automattic\Jetpack\Waf\Waf_Stats;
42
43/**
44 * Class Jetpack_Protect
45 *
46 * @phan-constructor-used-for-side-effects
47 */
48class Jetpack_Protect {
49
50    const JETPACK_SCAN_PRODUCT_IDS                   = array(
51        2010, // JETPACK_SECURITY_DAILY.
52        2011, // JETPACK_SECURITY_DAILY_MOTNHLY.
53        2012, // JETPACK_SECURITY_REALTIME.
54        2013, // JETPACK_SECURITY_REALTIME_MONTHLY.
55        2014, // JETPACK_COMPLETE.
56        2015, // JETPACK_COMPLETE_MONTHLY.
57        2025, // JETPACK_PRO_YEARLY.
58        2026, // JETPACK_PRO_BI_YEARLY.
59        2016, // JETPACK_SECURITY_TIER_1_YEARLY.
60        2017, // JETPACK_SECURITY_TIER_1_MONTHLY.
61        2019, // JETPACK_SECURITY_TIER_2_YEARLY.
62        2020, // JETPACK_SECURITY_TIER_2_MONTHLY.
63        2106, // JETPACK_SCAN.
64        2107, // JETPACK_SCAN_MONTHLY.
65        2108, // JETPACK_SCAN_REALTIME.
66        2109, // JETPACK_SCAN_REALTIME_MONTHLY.
67    );
68    const JETPACK_WAF_MODULE_SLUG                    = 'waf';
69    const JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG = 'protect';
70    const JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG     = 'account-protection';
71    const JETPACK_PROTECT_ACTIVATION_OPTION          = JETPACK_PROTECT_SLUG . '_activated';
72
73    /**
74     * Modules a site owner turns on from the Protect page, rather than keeping them from the Jetpack plugin.
75     *
76     * @var string[]
77     */
78    const OPT_IN_MODULES = array( 'sso', 'monitor' );
79
80    /**
81     * The opt-in modules turned on without the Jetpack plugin.
82     *
83     * @var string
84     */
85    const OPTED_IN_MODULES_OPTION = JETPACK_PROTECT_SLUG . '_opted_in_modules';
86
87    /**
88     * Constructor.
89     */
90    public function __construct() {
91        add_action( 'init', array( $this, 'init' ) );
92
93        Monitor::init();
94
95        /*
96         * The shared dashboard has no site connection flow, so unconnected sites keep the plugin's setup app.
97         * It also covers an older jetpack-protect package, loaded by another plugin, that lacks the dashboard.
98         */
99        if ( class_exists( Dashboard::class ) && ( new Connection_Manager() )->is_connected() ) {
100            Dashboard::init( array( 'product' => 'protect' ) );
101        } else {
102            add_action( '_admin_menu', array( $this, 'admin_page_init' ) );
103        }
104
105        // Activate the module as the plugin is activated
106        add_action( 'admin_init', array( $this, 'do_plugin_activation_activities' ) );
107
108        // Init Jetpack packages
109        add_action(
110            'plugins_loaded',
111            function () {
112                $config = new Automattic\Jetpack\Config();
113                // Connection package.
114                $config->ensure(
115                    'connection',
116                    array(
117                        'slug'     => JETPACK_PROTECT_SLUG,
118                        'name'     => JETPACK_PROTECT_NAME,
119                        'url_info' => JETPACK_PROTECT_URI,
120                    )
121                );
122                // Sync package.
123                $config->ensure(
124                    'sync',
125                    array(
126                        'jetpack_sync_modules'             => array(
127                            'Automattic\\Jetpack\\Sync\\Modules\\Options',
128                            'Automattic\\Jetpack\\Sync\\Modules\\Callables',
129                            'Automattic\\Jetpack\\Sync\\Modules\\Users',
130                        ),
131                        'jetpack_sync_callable_whitelist'  => array(
132                            'main_network_site' => array( 'Automattic\\Jetpack\\Connection\\Urls', 'main_network_site_url' ),
133                            'get_plugins'       => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
134                            'get_themes'        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
135                            'wp_version'        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
136                            // Tells WordPress.com which modules, such as Monitor, are on.
137                            'active_modules'    => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_active_modules' ),
138                        ),
139                        'jetpack_sync_options_contentless' => array(),
140                        'jetpack_sync_options_whitelist'   => array(
141                            'active_plugins',
142                            'stylesheet',
143                        ),
144                    )
145                );
146
147                // Identity crisis package.
148                $config->ensure( 'identity_crisis' );
149
150                // Web application firewall package.
151                $config->ensure( 'waf' );
152
153                // Account protection package.
154                $config->ensure( 'account_protection' );
155
156                // Fallback: an older connection package loaded by another plugin may predate init_standalone().
157                if ( method_exists( SSO::class, 'init_standalone' ) ) {
158                    SSO::init_standalone();
159                }
160            },
161            1
162        );
163
164        add_filter( 'jetpack_connection_user_has_license', array( $this, 'jetpack_check_user_licenses' ), 10, 3 );
165
166        add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'protect_filter_available_modules' ), 10, 1 );
167
168        add_filter( 'jetpack_active_modules', array( __CLASS__, 'filter_opt_in_modules' ) );
169        add_action( 'jetpack_pre_activate_module', array( __CLASS__, 'drop_leftover_opt_in_module' ) );
170        add_action( 'jetpack_activate_module', array( __CLASS__, 'record_opt_in' ) );
171        add_action( 'jetpack_deactivate_module', array( __CLASS__, 'forget_opt_in' ) );
172    }
173
174    /**
175     * Without the Jetpack plugin, count an opt-in module as on only if it was turned on without the Jetpack plugin.
176     *
177     * The saved list is left alone, so the Jetpack plugin gets its own state back if it returns.
178     *
179     * @param string[] $active Active module slugs.
180     * @return string[]
181     */
182    public static function filter_opt_in_modules( $active ) {
183        if ( class_exists( 'Jetpack', false ) ) {
184            return $active;
185        }
186        $stale = array_diff( self::OPT_IN_MODULES, self::get_opted_in_modules() );
187        return array_values( array_diff( (array) $active, $stale ) );
188    }
189
190    /**
191     * Without the Jetpack plugin, drop a leftover saved entry before an opt-in module is turned on, so activation runs its hooks.
192     *
193     * @param string $module Module slug.
194     * @return void
195     */
196    public static function drop_leftover_opt_in_module( $module ) {
197        if ( class_exists( 'Jetpack', false ) || ! in_array( $module, self::OPT_IN_MODULES, true ) || in_array( $module, self::get_opted_in_modules(), true ) || ! ( new Modules() )->is_module( $module ) ) {
198            return;
199        }
200        $saved = Jetpack_Options::get_option( 'active_modules', array() );
201        if ( is_array( $saved ) && in_array( $module, $saved, true ) ) {
202            Jetpack_Options::update_option( 'active_modules', array_values( array_diff( $saved, array( $module ) ) ) );
203        }
204    }
205
206    /**
207     * Remember that an opt-in module was turned on without the Jetpack plugin.
208     *
209     * @param string $module Module slug.
210     * @return void
211     */
212    public static function record_opt_in( $module ) {
213        if ( class_exists( 'Jetpack', false ) || ! in_array( $module, self::OPT_IN_MODULES, true ) || in_array( $module, self::get_opted_in_modules(), true ) ) {
214            return;
215        }
216        update_option( self::OPTED_IN_MODULES_OPTION, array_merge( self::get_opted_in_modules(), array( $module ) ) );
217    }
218
219    /**
220     * Forget an opt-in module once it is turned off; before that, the filter would hide it from the deactivation.
221     *
222     * @param string $module Module slug.
223     * @return void
224     */
225    public static function forget_opt_in( $module ) {
226        $opted_in = self::get_opted_in_modules();
227        if ( in_array( $module, $opted_in, true ) ) {
228            update_option( self::OPTED_IN_MODULES_OPTION, array_values( array_diff( $opted_in, array( $module ) ) ) );
229        }
230    }
231
232    /**
233     * The opt-in modules turned on without the Jetpack plugin.
234     *
235     * @return string[]
236     */
237    private static function get_opted_in_modules() {
238        $opted_in = get_option( self::OPTED_IN_MODULES_OPTION, array() );
239        return is_array( $opted_in ) ? $opted_in : array();
240    }
241
242    /**
243     * Initialize the plugin
244     *
245     * @return void
246     */
247    public function init() {
248        add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 65 );
249        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
250
251        REST_Controller::init();
252        My_Jetpack_Initializer::init();
253        // Activity Log. No-ops while the `activity-log` module is off.
254        Jetpack_Activity_Log::initialize();
255        Site_Health::init();
256
257        // Sets up JITMS.
258        JITM::configure();
259    }
260
261    /**
262     * Initialize the admin page resources.
263     */
264    public function admin_page_init() {
265        // Only report the threat count to users who can actually reach the Protect
266        // menu (added below with the 'manage_options' cap). Otherwise the central
267        // menu-badges total would include threats the current user can't see.
268        if ( current_user_can( 'manage_options' ) && self::should_notify_threats() ) {
269            \Automattic\Jetpack\Menu_Badges\Menu_Badges::init(); // idempotent; wires the renderer.
270            \Automattic\Jetpack\Menu_Badges\Notification_Counts::register(
271                'jetpack-protect',
272                array(
273                    'menu_slug' => 'jetpack-protect',
274                    'count'     => Status::get_total_threats(),
275                    'type'      => 'count',
276                )
277            );
278        }
279
280        $page_suffix = Admin_Menu::add_menu(
281            'Jetpack Protect', // "Jetpack Protect" is a product name, do not translate.
282            'Protect', // "Protect" is a product name, do not translate.
283            'manage_options',
284            'jetpack-protect',
285            array( $this, 'plugin_settings_page' ),
286            null,
287            array(
288                'product' => 'protect',
289                'key'     => 'jetpack-protect',
290            )
291        );
292
293        add_action( 'load-' . $page_suffix, array( $this, 'enqueue_admin_scripts' ) );
294    }
295
296    /**
297     * Enqueues the wp-admin styles (used outside the React app)
298     */
299    public function enqueue_admin_styles() {
300        wp_enqueue_style( 'jetpack-protect-wpadmin', JETPACK_PROTECT_BASE_PLUGIN_URL . '/assets/jetpack-protect.css', array(), JETPACK_PROTECT_VERSION );
301    }
302
303    /**
304     * Enqueue plugin admin scripts and styles.
305     */
306    public function enqueue_admin_scripts() {
307
308        Assets::register_script(
309            'jetpack-protect',
310            'build/index.js',
311            JETPACK_PROTECT_ROOT_FILE,
312            array(
313                'in_footer'  => true,
314                'textdomain' => 'jetpack-protect',
315            )
316        );
317        Assets::enqueue_script( 'jetpack-protect' );
318        // Required for Analytics.
319        wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
320        // Initial JS state including JP Connection data.
321        Connection_Initial_State::render_script( 'jetpack-protect' );
322        wp_add_inline_script( 'jetpack-protect', $this->render_initial_state(), 'before' );
323    }
324
325    /**
326     * Render the initial state into a JavaScript variable.
327     *
328     * @return string
329     */
330    public function render_initial_state() {
331        return 'var jetpackProtectInitialState=' . wp_json_encode( $this->initial_state(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
332    }
333
334    /**
335     * Get the initial state data for hydrating the React UI.
336     *
337     * @return array
338     */
339    public function initial_state() {
340        global $wp_version;
341
342        // Always fetch the latest plan status from WPCOM.
343        $has_plan = Plan::has_required_plan( true );
344
345        $status = Status::get_status();
346
347        $initial_state = array(
348            'apiRoot'            => esc_url_raw( rest_url() ),
349            'apiNonce'           => wp_create_nonce( 'wp_rest' ),
350            'registrationNonce'  => wp_create_nonce( 'jetpack-registration-nonce' ),
351            'credentials'        => Credentials::get_credential_array(),
352            'status'             => $status,
353            'fixerStatus'        => Threats::fix_threats_status( $status->fixable_threat_ids ),
354            'scanHistory'        => Scan_History::get_scan_history(),
355            'installedPlugins'   => Plugins_Installer::get_plugins(),
356            'installedThemes'    => Sync_Functions::get_themes(),
357            'wpVersion'          => $wp_version,
358            'adminUrl'           => 'admin.php?page=jetpack-protect',
359            'siteSuffix'         => ( new Jetpack_Status() )->get_site_suffix(),
360            'blogID'             => Connection_Manager::get_site_id( true ),
361            'jetpackScan'        => My_Jetpack_Products::get_product( 'scan' ),
362            'hasPlan'            => $has_plan,
363            'onboardingProgress' => Onboarding::get_current_user_progress(),
364            'accountProtection'  => ( new Account_Protection_Settings() )->get(),
365            'waf'                => array(
366                'wafSupported'        => Waf_Runner::is_supported_environment(),
367                'currentIp'           => IP_Utils::get_ip(),
368                'upgradeIsSeen'       => self::get_waf_upgrade_seen_status(),
369                'displayUpgradeBadge' => self::get_waf_upgrade_badge_display_status(),
370                'isEnabled'           => Waf_Runner::is_enabled(),
371                'config'              => Waf_Runner::get_config(),
372                'stats'               => self::get_waf_stats(),
373                'globalStats'         => Waf_Stats::get_global_stats(),
374            ),
375        );
376
377        $initial_state['jetpackScan']['pricingForUi'] = Plan::get_product( 'jetpack_scan' );
378
379        return $initial_state;
380    }
381    /**
382     * Main plugin settings page.
383     */
384    public function plugin_settings_page() {
385        ?>
386            <div id="jetpack-protect-root"></div>
387        <?php
388    }
389
390    /**
391     * Activate the WAF module on plugin activation.
392     *
393     * @static
394     */
395    public static function plugin_activation() {
396        add_option( self::JETPACK_PROTECT_ACTIVATION_OPTION, true );
397    }
398
399    /**
400     * Runs on admin_init, and does actions required on plugin activation, based on
401     * the activation option.
402     *
403     * This needs to be run after the activation hook, as that results in a redirect,
404     * and we need the sync module's actions and filters to be registered.
405     */
406    public static function do_plugin_activation_activities() {
407        if ( get_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ) && ( new Connection_Manager() )->is_connected() ) {
408            self::activate_modules();
409        }
410    }
411
412    /**
413     * Activates the waf and brute force protection modules and disables the activation option
414     */
415    public static function activate_modules() {
416        delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION );
417        ( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false );
418        ( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false );
419    }
420
421    /**
422     * Removes plugin from the connection manager
423     * If it's the last plugin using the connection, the site will be disconnected.
424     *
425     * @access public
426     * @static
427     */
428    public static function plugin_deactivation() {
429
430        // Clear Sync data.
431        Sender::get_instance()->uninstall();
432
433        $manager = new Connection_Manager( 'jetpack-protect' );
434        $manager->remove_connection();
435
436        Protect_Status::delete_option();
437        Scan_Status::delete_option();
438        Scan_History::delete_option();
439    }
440
441    /**
442     * Whether the host lets threats be announced in the menu and the admin bar.
443     *
444     * @return bool
445     */
446    private static function should_notify_threats() {
447        return ! method_exists( Status::class, 'should_notify_threats' ) || Status::should_notify_threats();
448    }
449
450    /**
451     * Create a shortcut on Admin Bar to show the total of threats found.
452     *
453     * @param object $wp_admin_bar The Admin Bar object.
454     * @return void
455     */
456    public function admin_bar( $wp_admin_bar ) {
457        if ( ! current_user_can( 'manage_options' ) || ! self::should_notify_threats() ) {
458            return;
459        }
460
461        $total = Status::get_total_threats();
462
463        // On the Protect page the app updates the count, so a hidden node is kept for a later scan to reveal.
464        if ( $total > 0 || self::is_protect_page() ) {
465            $args = array(
466                'id'    => 'jetpack-protect',
467                'title' => '<span class="ab-icon jp-protect-icon"></span><span class="ab-label">' . $total . '</span>',
468                'href'  => admin_url( 'admin.php?page=jetpack-protect' ),
469                'meta'  => array(
470                    'class' => $total > 0 ? '' : 'hidden',
471                    // translators: %d is the number of threats found.
472                    'title' => sprintf( _n( '%d threat found by Jetpack Protect', '%d threats found by Jetpack Protect', $total, 'jetpack-protect' ), $total ),
473                ),
474            );
475
476            $wp_admin_bar->add_node( $args );
477        }
478    }
479
480    /**
481     * Whether the request is for the Protect page in wp-admin.
482     *
483     * @return bool
484     */
485    private static function is_protect_page() {
486        if ( ! is_admin() ) {
487            return false;
488        }
489        if ( method_exists( Status::class, 'is_protect_page' ) ) {
490            return Status::is_protect_page();
491        }
492        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only compared against the page slug.
493        return isset( $_GET['page'] ) && 'jetpack-protect' === $_GET['page'];
494    }
495
496    /**
497     * Adds modules to the list of available modules
498     *
499     * @param array $modules The available modules.
500     * @return array
501     */
502    public function protect_filter_available_modules( $modules ) {
503        return array_merge( array( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG ), $modules );
504    }
505
506    /**
507     * Check if the user has an available license that includes Jetpack Scan.
508     *
509     * @param boolean  $has_license  Whether a license was already found.
510     * @param object[] $licenses     Unattached licenses belonging to the user.
511     * @param string   $plugin_slug  Slug of the plugin that initiated the flow.
512     *
513     * @return boolean
514     */
515    public static function jetpack_check_user_licenses( $has_license, $licenses, $plugin_slug ) {
516        if ( $plugin_slug !== JETPACK_PROTECT_SLUG || $has_license ) {
517            return $has_license;
518        }
519
520        $license_found = false;
521
522        foreach ( $licenses as $license ) {
523            if ( $license->attached_at || $license->revoked_at ) {
524                continue;
525            }
526
527            if ( in_array( $license->product_id, self::JETPACK_SCAN_PRODUCT_IDS, true ) ) {
528                $license_found = true;
529                break;
530            }
531        }
532
533        return $license_found;
534    }
535
536    /**
537     * Get WAF Upgrade "Seen" Status
538     *
539     * @return bool Whether the current user has dismissed the upgrade popover or enabled the automatic rules feature.
540     */
541    public static function get_waf_upgrade_seen_status() {
542        return (bool) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
543    }
544
545    /**
546     * Set WAF Upgrade "Seen" Status
547     *
548     * @return bool True if upgrade seen status updated to true, false on failure.
549     */
550    public static function set_waf_upgrade_seen_status() {
551        self::set_waf_upgrade_badge_timestamp();
552        return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
553    }
554
555    /**
556     * Get WAF Upgrade Badge Timestamp
557     *
558     * @return integer The timestamp for the when the upgrade seen status was first set to true.
559     */
560    public static function get_waf_upgrade_badge_timestamp() {
561        return (int) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', true );
562    }
563
564    /**
565     * Set WAF Upgrade Badge Timestamp
566     *
567     * @return bool True if upgrade badge timestamp to set to the current time, false on failure.
568     */
569    public static function set_waf_upgrade_badge_timestamp() {
570        return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', time() );
571    }
572
573    /**
574     * Get WAF Upgrade Badge Display Status
575     *
576     * @return bool True if upgrade badge timestamp is set and less than 7 days ago, otherwise false.
577     */
578    public static function get_waf_upgrade_badge_display_status() {
579        $badge_timestamp_exists = metadata_exists( 'user', get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp' );
580        if ( ! $badge_timestamp_exists ) {
581            return true;
582        }
583
584        $badge_timestamp = self::get_waf_upgrade_badge_timestamp();
585        $seven_days      = strtotime( '-7 days' );
586        if ( $badge_timestamp > $seven_days ) {
587            return true;
588        }
589
590        return false;
591    }
592
593    /**
594     * Get WAF stats
595     *
596     * @return bool|array False if WAF is not enabled, otherwise an array of stats.
597     */
598    public static function get_waf_stats() {
599        if ( ! Waf_Runner::is_enabled() ) {
600            return false;
601        }
602
603        return array(
604            'blockedRequests'           => Plan::has_required_plan() ? Waf_Stats::get_blocked_requests() : false,
605            'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(),
606        );
607    }
608}