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