Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.92% covered (danger)
26.92%
49 / 182
18.18% covered (danger)
18.18%
4 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Protect
27.22% covered (danger)
27.22%
49 / 180
18.18% covered (danger)
18.18%
4 / 22
656.76
0.00% covered (danger)
0.00%
0 / 1
 __construct
25.00% covered (danger)
25.00%
11 / 44
0.00% covered (danger)
0.00%
0 / 1
1.42
 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%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 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\IP\Utils as IP_Utils;
19use Automattic\Jetpack\JITMS\JITM;
20use Automattic\Jetpack\Modules;
21use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
22use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products;
23use Automattic\Jetpack\Plugins_Installer;
24use Automattic\Jetpack\Protect\Credentials;
25use Automattic\Jetpack\Protect\Onboarding;
26use Automattic\Jetpack\Protect\REST_Controller;
27use Automattic\Jetpack\Protect\Scan_History;
28use Automattic\Jetpack\Protect\Site_Health;
29use Automattic\Jetpack\Protect\Threats;
30use Automattic\Jetpack\Protect_Status\Plan;
31use Automattic\Jetpack\Protect_Status\Protect_Status;
32use Automattic\Jetpack\Protect_Status\Scan_Status;
33use Automattic\Jetpack\Protect_Status\Status;
34use Automattic\Jetpack\Status as Jetpack_Status;
35use Automattic\Jetpack\Sync\Functions as Sync_Functions;
36use Automattic\Jetpack\Sync\Sender;
37use Automattic\Jetpack\Waf\Waf_Runner;
38use Automattic\Jetpack\Waf\Waf_Stats;
39
40/**
41 * Class Jetpack_Protect
42 *
43 * @phan-constructor-used-for-side-effects
44 */
45class Jetpack_Protect {
46
47    const JETPACK_SCAN_PRODUCT_IDS                   = array(
48        2010, // JETPACK_SECURITY_DAILY.
49        2011, // JETPACK_SECURITY_DAILY_MOTNHLY.
50        2012, // JETPACK_SECURITY_REALTIME.
51        2013, // JETPACK_SECURITY_REALTIME_MONTHLY.
52        2014, // JETPACK_COMPLETE.
53        2015, // JETPACK_COMPLETE_MONTHLY.
54        2025, // JETPACK_PRO_YEARLY.
55        2026, // JETPACK_PRO_BI_YEARLY.
56        2016, // JETPACK_SECURITY_TIER_1_YEARLY.
57        2017, // JETPACK_SECURITY_TIER_1_MONTHLY.
58        2019, // JETPACK_SECURITY_TIER_2_YEARLY.
59        2020, // JETPACK_SECURITY_TIER_2_MONTHLY.
60        2106, // JETPACK_SCAN.
61        2107, // JETPACK_SCAN_MONTHLY.
62        2108, // JETPACK_SCAN_REALTIME.
63        2109, // JETPACK_SCAN_REALTIME_MONTHLY.
64    );
65    const JETPACK_WAF_MODULE_SLUG                    = 'waf';
66    const JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG = 'protect';
67    const JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG     = 'account-protection';
68    const JETPACK_PROTECT_ACTIVATION_OPTION          = JETPACK_PROTECT_SLUG . '_activated';
69
70    /**
71     * Constructor.
72     */
73    public function __construct() {
74        add_action( 'init', array( $this, 'init' ) );
75        add_action( '_admin_menu', array( $this, 'admin_page_init' ) );
76
77        // Activate the module as the plugin is activated
78        add_action( 'admin_init', array( $this, 'do_plugin_activation_activities' ) );
79
80        // Init Jetpack packages
81        add_action(
82            'plugins_loaded',
83            function () {
84                $config = new Automattic\Jetpack\Config();
85                // Connection package.
86                $config->ensure(
87                    'connection',
88                    array(
89                        'slug'     => JETPACK_PROTECT_SLUG,
90                        'name'     => JETPACK_PROTECT_NAME,
91                        'url_info' => JETPACK_PROTECT_URI,
92                    )
93                );
94                // Sync package.
95                $config->ensure(
96                    'sync',
97                    array(
98                        'jetpack_sync_modules'             => array(
99                            'Automattic\\Jetpack\\Sync\\Modules\\Options',
100                            'Automattic\\Jetpack\\Sync\\Modules\\Callables',
101                            'Automattic\\Jetpack\\Sync\\Modules\\Users',
102                        ),
103                        'jetpack_sync_callable_whitelist'  => array(
104                            'main_network_site' => array( 'Automattic\\Jetpack\\Connection\\Urls', 'main_network_site_url' ),
105                            'get_plugins'       => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_plugins' ),
106                            'get_themes'        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'get_themes' ),
107                            'wp_version'        => array( 'Automattic\\Jetpack\\Sync\\Functions', 'wp_version' ),
108                        ),
109                        'jetpack_sync_options_contentless' => array(),
110                        'jetpack_sync_options_whitelist'   => array(
111                            'active_plugins',
112                            'stylesheet',
113                        ),
114                    )
115                );
116
117                // Identity crisis package.
118                $config->ensure( 'identity_crisis' );
119
120                // Web application firewall package.
121                $config->ensure( 'waf' );
122
123                // Account protection package.
124                $config->ensure( 'account_protection' );
125            },
126            1
127        );
128
129        add_filter( 'jetpack_connection_user_has_license', array( $this, 'jetpack_check_user_licenses' ), 10, 3 );
130
131        add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'protect_filter_available_modules' ), 10, 1 );
132    }
133
134    /**
135     * Initialize the plugin
136     *
137     * @return void
138     */
139    public function init() {
140        add_action( 'admin_bar_menu', array( $this, 'admin_bar' ), 65 );
141        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_styles' ) );
142
143        REST_Controller::init();
144        My_Jetpack_Initializer::init();
145        // Activity Log. No-ops while the `activity-log` module is off.
146        Jetpack_Activity_Log::initialize();
147        Site_Health::init();
148
149        // Sets up JITMS.
150        JITM::configure();
151    }
152
153    /**
154     * Initialize the admin page resources.
155     */
156    public function admin_page_init() {
157        // Only report the threat count to users who can actually reach the Protect
158        // menu (added below with the 'manage_options' cap). Otherwise the central
159        // menu-badges total would include threats the current user can't see.
160        if ( current_user_can( 'manage_options' ) && self::should_notify_threats() ) {
161            \Automattic\Jetpack\Menu_Badges\Menu_Badges::init(); // idempotent; wires the renderer.
162            \Automattic\Jetpack\Menu_Badges\Notification_Counts::register(
163                'jetpack-protect',
164                array(
165                    'menu_slug' => 'jetpack-protect',
166                    'count'     => Status::get_total_threats(),
167                    'type'      => 'count',
168                )
169            );
170        }
171
172        $page_suffix = Admin_Menu::add_menu(
173            'Jetpack Protect', // "Jetpack Protect" is a product name, do not translate.
174            'Protect', // "Protect" is a product name, do not translate.
175            'manage_options',
176            'jetpack-protect',
177            array( $this, 'plugin_settings_page' ),
178            null,
179            array(
180                'product' => 'protect',
181                'key'     => 'jetpack-protect',
182            )
183        );
184
185        add_action( 'load-' . $page_suffix, array( $this, 'enqueue_admin_scripts' ) );
186    }
187
188    /**
189     * Enqueues the wp-admin styles (used outside the React app)
190     */
191    public function enqueue_admin_styles() {
192        wp_enqueue_style( 'jetpack-protect-wpadmin', JETPACK_PROTECT_BASE_PLUGIN_URL . '/assets/jetpack-protect.css', array(), JETPACK_PROTECT_VERSION );
193    }
194
195    /**
196     * Enqueue plugin admin scripts and styles.
197     */
198    public function enqueue_admin_scripts() {
199
200        Assets::register_script(
201            'jetpack-protect',
202            'build/index.js',
203            JETPACK_PROTECT_ROOT_FILE,
204            array(
205                'in_footer'  => true,
206                'textdomain' => 'jetpack-protect',
207            )
208        );
209        Assets::enqueue_script( 'jetpack-protect' );
210        // Required for Analytics.
211        wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
212        // Initial JS state including JP Connection data.
213        Connection_Initial_State::render_script( 'jetpack-protect' );
214        wp_add_inline_script( 'jetpack-protect', $this->render_initial_state(), 'before' );
215    }
216
217    /**
218     * Render the initial state into a JavaScript variable.
219     *
220     * @return string
221     */
222    public function render_initial_state() {
223        return 'var jetpackProtectInitialState=' . wp_json_encode( $this->initial_state(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';';
224    }
225
226    /**
227     * Get the initial state data for hydrating the React UI.
228     *
229     * @return array
230     */
231    public function initial_state() {
232        global $wp_version;
233
234        // Always fetch the latest plan status from WPCOM.
235        $has_plan = Plan::has_required_plan( true );
236
237        $status = Status::get_status();
238
239        $initial_state = array(
240            'apiRoot'            => esc_url_raw( rest_url() ),
241            'apiNonce'           => wp_create_nonce( 'wp_rest' ),
242            'registrationNonce'  => wp_create_nonce( 'jetpack-registration-nonce' ),
243            'credentials'        => Credentials::get_credential_array(),
244            'status'             => $status,
245            'fixerStatus'        => Threats::fix_threats_status( $status->fixable_threat_ids ),
246            'scanHistory'        => Scan_History::get_scan_history(),
247            'installedPlugins'   => Plugins_Installer::get_plugins(),
248            'installedThemes'    => Sync_Functions::get_themes(),
249            'wpVersion'          => $wp_version,
250            'adminUrl'           => 'admin.php?page=jetpack-protect',
251            'siteSuffix'         => ( new Jetpack_Status() )->get_site_suffix(),
252            'blogID'             => Connection_Manager::get_site_id( true ),
253            'jetpackScan'        => My_Jetpack_Products::get_product( 'scan' ),
254            'hasPlan'            => $has_plan,
255            'onboardingProgress' => Onboarding::get_current_user_progress(),
256            'accountProtection'  => ( new Account_Protection_Settings() )->get(),
257            'waf'                => array(
258                'wafSupported'        => Waf_Runner::is_supported_environment(),
259                'currentIp'           => IP_Utils::get_ip(),
260                'upgradeIsSeen'       => self::get_waf_upgrade_seen_status(),
261                'displayUpgradeBadge' => self::get_waf_upgrade_badge_display_status(),
262                'isEnabled'           => Waf_Runner::is_enabled(),
263                'config'              => Waf_Runner::get_config(),
264                'stats'               => self::get_waf_stats(),
265                'globalStats'         => Waf_Stats::get_global_stats(),
266            ),
267        );
268
269        $initial_state['jetpackScan']['pricingForUi'] = Plan::get_product( 'jetpack_scan' );
270
271        return $initial_state;
272    }
273    /**
274     * Main plugin settings page.
275     */
276    public function plugin_settings_page() {
277        ?>
278            <div id="jetpack-protect-root"></div>
279        <?php
280    }
281
282    /**
283     * Activate the WAF module on plugin activation.
284     *
285     * @static
286     */
287    public static function plugin_activation() {
288        add_option( self::JETPACK_PROTECT_ACTIVATION_OPTION, true );
289    }
290
291    /**
292     * Runs on admin_init, and does actions required on plugin activation, based on
293     * the activation option.
294     *
295     * This needs to be run after the activation hook, as that results in a redirect,
296     * and we need the sync module's actions and filters to be registered.
297     */
298    public static function do_plugin_activation_activities() {
299        if ( get_option( self::JETPACK_PROTECT_ACTIVATION_OPTION ) && ( new Connection_Manager() )->is_connected() ) {
300            self::activate_modules();
301        }
302    }
303
304    /**
305     * Activates the waf and brute force protection modules and disables the activation option
306     */
307    public static function activate_modules() {
308        delete_option( self::JETPACK_PROTECT_ACTIVATION_OPTION );
309        ( new Modules() )->activate( self::JETPACK_WAF_MODULE_SLUG, false, false );
310        ( new Modules() )->activate( self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG, false, false );
311    }
312
313    /**
314     * Removes plugin from the connection manager
315     * If it's the last plugin using the connection, the site will be disconnected.
316     *
317     * @access public
318     * @static
319     */
320    public static function plugin_deactivation() {
321
322        // Clear Sync data.
323        Sender::get_instance()->uninstall();
324
325        $manager = new Connection_Manager( 'jetpack-protect' );
326        $manager->remove_connection();
327
328        Protect_Status::delete_option();
329        Scan_Status::delete_option();
330        Scan_History::delete_option();
331    }
332
333    /**
334     * Whether the host lets threats be announced in the menu and the admin bar.
335     *
336     * @return bool
337     */
338    private static function should_notify_threats() {
339        return ! method_exists( Status::class, 'should_notify_threats' ) || Status::should_notify_threats();
340    }
341
342    /**
343     * Create a shortcut on Admin Bar to show the total of threats found.
344     *
345     * @param object $wp_admin_bar The Admin Bar object.
346     * @return void
347     */
348    public function admin_bar( $wp_admin_bar ) {
349        if ( ! current_user_can( 'manage_options' ) || ! self::should_notify_threats() ) {
350            return;
351        }
352
353        $total = Status::get_total_threats();
354
355        if ( $total > 0 ) {
356            $args = array(
357                'id'    => 'jetpack-protect',
358                'title' => '<span class="ab-icon jp-protect-icon"></span><span class="ab-label">' . $total . '</span>',
359                'href'  => admin_url( 'admin.php?page=jetpack-protect' ),
360                'meta'  => array(
361                    // translators: %d is the number of threats found.
362                    'title' => sprintf( _n( '%d threat found by Jetpack Protect', '%d threats found by Jetpack Protect', $total, 'jetpack-protect' ), $total ),
363                ),
364            );
365
366            $wp_admin_bar->add_node( $args );
367        }
368    }
369
370    /**
371     * Adds modules to the list of available modules
372     *
373     * @param array $modules The available modules.
374     * @return array
375     */
376    public function protect_filter_available_modules( $modules ) {
377        return array_merge( array( self::JETPACK_ACCOUNT_PROTECTION_MODULE_SLUG, self::JETPACK_WAF_MODULE_SLUG, self::JETPACK_BRUTE_FORCE_PROTECTION_MODULE_SLUG ), $modules );
378    }
379
380    /**
381     * Check if the user has an available license that includes Jetpack Scan.
382     *
383     * @param boolean  $has_license  Whether a license was already found.
384     * @param object[] $licenses     Unattached licenses belonging to the user.
385     * @param string   $plugin_slug  Slug of the plugin that initiated the flow.
386     *
387     * @return boolean
388     */
389    public static function jetpack_check_user_licenses( $has_license, $licenses, $plugin_slug ) {
390        if ( $plugin_slug !== JETPACK_PROTECT_SLUG || $has_license ) {
391            return $has_license;
392        }
393
394        $license_found = false;
395
396        foreach ( $licenses as $license ) {
397            if ( $license->attached_at || $license->revoked_at ) {
398                continue;
399            }
400
401            if ( in_array( $license->product_id, self::JETPACK_SCAN_PRODUCT_IDS, true ) ) {
402                $license_found = true;
403                break;
404            }
405        }
406
407        return $license_found;
408    }
409
410    /**
411     * Get WAF Upgrade "Seen" Status
412     *
413     * @return bool Whether the current user has dismissed the upgrade popover or enabled the automatic rules feature.
414     */
415    public static function get_waf_upgrade_seen_status() {
416        return (bool) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
417    }
418
419    /**
420     * Set WAF Upgrade "Seen" Status
421     *
422     * @return bool True if upgrade seen status updated to true, false on failure.
423     */
424    public static function set_waf_upgrade_seen_status() {
425        self::set_waf_upgrade_badge_timestamp();
426        return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_seen', true );
427    }
428
429    /**
430     * Get WAF Upgrade Badge Timestamp
431     *
432     * @return integer The timestamp for the when the upgrade seen status was first set to true.
433     */
434    public static function get_waf_upgrade_badge_timestamp() {
435        return (int) get_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', true );
436    }
437
438    /**
439     * Set WAF Upgrade Badge Timestamp
440     *
441     * @return bool True if upgrade badge timestamp to set to the current time, false on failure.
442     */
443    public static function set_waf_upgrade_badge_timestamp() {
444        return (bool) update_user_meta( get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp', time() );
445    }
446
447    /**
448     * Get WAF Upgrade Badge Display Status
449     *
450     * @return bool True if upgrade badge timestamp is set and less than 7 days ago, otherwise false.
451     */
452    public static function get_waf_upgrade_badge_display_status() {
453        $badge_timestamp_exists = metadata_exists( 'user', get_current_user_id(), 'jetpack_protect_waf_upgrade_badge_timestamp' );
454        if ( ! $badge_timestamp_exists ) {
455            return true;
456        }
457
458        $badge_timestamp = self::get_waf_upgrade_badge_timestamp();
459        $seven_days      = strtotime( '-7 days' );
460        if ( $badge_timestamp > $seven_days ) {
461            return true;
462        }
463
464        return false;
465    }
466
467    /**
468     * Get WAF stats
469     *
470     * @return bool|array False if WAF is not enabled, otherwise an array of stats.
471     */
472    public static function get_waf_stats() {
473        if ( ! Waf_Runner::is_enabled() ) {
474            return false;
475        }
476
477        return array(
478            'blockedRequests'           => Plan::has_required_plan() ? Waf_Stats::get_blocked_requests() : false,
479            'automaticRulesLastUpdated' => Waf_Stats::get_automatic_rules_last_updated(),
480        );
481    }
482}