Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (warning)
88.89%
144 / 162
94.12% covered (success)
94.12%
32 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Protect
89.38% covered (warning)
89.38%
143 / 160
94.12% covered (success)
94.12%
32 / 34
64.32
0.00% covered (danger)
0.00%
0 / 1
 get_module_name
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 should_use_standalone_plugin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_dashboard_module_available
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 do_product_specific_activation
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 register_endpoints
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 permissions_callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_long_description
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_features
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_tiers
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_features_by_tier
n/a
0 / 0
n/a
0 / 0
1
 get_pricing_for_ui
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 does_module_need_attention
30.43% covered (danger)
30.43%
7 / 23
0.00% covered (danger)
0.00%
0 / 1
13.42
 get_paid_plan_product_slugs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_paid_plan_for_product
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_status
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 is_upgradable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_checkout_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_post_checkout_urls_by_feature
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_protect_page_module_urls
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 get_firewall_settings_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has_protect_page
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_info
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 get_purchase_url
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 get_scan_dashboard_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_manage_url
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 get_manage_urls_by_feature
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 is_upgradable_by_bundle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_site_protect_data
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
 filter_waf_config_by_capability
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 filter_scan_data_by_capability
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 redact_threat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 redact_extension
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Protect product
4 *
5 * @package my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack\Products;
9
10use Automattic\Jetpack\Modules;
11use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
12use Automattic\Jetpack\My_Jetpack\Products;
13use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
14use Automattic\Jetpack\Protect_Status\Plan;
15use Automattic\Jetpack\Protect_Status\Status as Protect_Status;
16use Automattic\Jetpack\Redirect;
17use Automattic\Jetpack\Status\Host;
18use Automattic\Jetpack\Waf\Waf_Runner;
19use WP_Error;
20use WP_REST_Response;
21
22if ( ! defined( 'ABSPATH' ) ) {
23    exit( 0 );
24}
25
26/**
27 * Class responsible for handling the Protect product
28 */
29class Protect extends Hybrid_Product {
30
31    const FREE_TIER_SLUG             = 'free';
32    const UPGRADED_TIER_SLUG         = 'upgraded';
33    const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_scan';
34
35    const SCAN_FEATURE_SLUG     = 'scan';
36    const FIREWALL_FEATURE_SLUG = 'firewall';
37
38    /**
39     * Keys from Waf_Runner::get_config() that may be exposed to users without `manage_options`.
40     *
41     * My Jetpack is reachable with `edit_posts`, and the Protect card renders these two as
42     * on/off status indicators. Everything else the WAF config carries (IP allow/block lists,
43     * the bootstrap file path, data sharing settings) is administrator-only.
44     *
45     * @var string[]
46     */
47    private const NON_ADMIN_WAF_CONFIG_KEYS = array(
48        'jetpack_waf_automatic_rules',
49        'brute_force_protection',
50    );
51
52    /**
53     * Status_Model properties that may be exposed to users without `manage_options`.
54     *
55     * My Jetpack is reachable with `edit_posts`, and the Protect card renders scan counts, the
56     * last scan time and whether that scan failed from these. The rest of the status report --
57     * the scan state, the error details, the list of fixable threat IDs -- is administrator-only.
58     *
59     * @var string[]
60     */
61    private const NON_ADMIN_SCAN_DATA_KEYS = array(
62        'last_checked',
63        'last_scan_failed',
64        'num_threats',
65        'num_plugins_threats',
66        'num_themes_threats',
67        'threats',
68        'plugins',
69        'themes',
70        'core',
71        'files',
72        'database',
73    );
74
75    /**
76     * Threat_Model properties that may be exposed to users without `manage_options`.
77     *
78     * The card counts threats and critical (severity >= 5) threats; it never renders a threat.
79     * Everything else a threat carries -- the infected file path, the surrounding source, the
80     * signature, the database table and the vulnerable extension version -- is administrator-only.
81     *
82     * @var string[]
83     */
84    private const NON_ADMIN_THREAT_KEYS = array( 'severity' );
85
86    /**
87     * The product slug
88     *
89     * @var string
90     */
91    public static $slug = 'protect';
92
93    /**
94     * The Jetpack module name, used where the `protect-dashboard` module is not on offer.
95     *
96     * @var string
97     */
98    public static $module_name = 'protect';
99
100    /**
101     * The module that loads the Protect page.
102     *
103     * @var string
104     */
105    const DASHBOARD_MODULE_NAME = 'protect-dashboard';
106
107    /**
108     * The filename (id) of the plugin associated with this product.
109     *
110     * @var string
111     */
112    public static $plugin_filename = array(
113        'jetpack-protect/jetpack-protect.php',
114        'protect/jetpack-protect.php',
115        'jetpack-protect-dev/jetpack-protect.php',
116    );
117
118    /**
119     * The slug of the plugin associated with this product.
120     *
121     * @var string
122     */
123    public static $plugin_slug = 'jetpack-protect';
124
125    /**
126     * Get the module that switches Protect on: the Protect page's, wherever the Jetpack plugin offers it.
127     *
128     * Not with the standalone plugin active: the page module would take over that plugin's dashboard (see JETPACK-3062).
129     *
130     * @since 6.12.0
131     *
132     * @return string
133     */
134    public static function get_module_name() {
135        if ( ! static::is_standalone_plugin_active() && self::is_dashboard_module_available() ) {
136            return self::DASHBOARD_MODULE_NAME;
137        }
138        return static::$module_name;
139    }
140
141    /**
142     * Leave the standalone plugin alone where the Jetpack plugin already delivers the Protect page.
143     *
144     * @since 6.12.0
145     *
146     * @return bool
147     */
148    public static function should_use_standalone_plugin() {
149        return ! ( static::is_jetpack_plugin_active() && self::is_dashboard_module_available() );
150    }
151
152    /**
153     * Whether the Jetpack plugin offers the module that loads the Protect page, which only happens while its flag is on.
154     *
155     * Not Modules::is_module(): it passes any slug.
156     *
157     * @since 6.12.0
158     *
159     * @return bool
160     */
161    public static function is_dashboard_module_available() {
162        return in_array( self::DASHBOARD_MODULE_NAME, (array) ( new Modules() )->get_available(), true );
163    }
164
165    /**
166     * Activate local product steps without scheduling connected modules offline.
167     *
168     * @since 6.10.0
169     * @param bool|WP_Error $current_result The plugin activation result.
170     * @param bool          $local          Whether to keep activation local.
171     * @return bool|WP_Error
172     */
173    public static function do_product_specific_activation( $current_result, $local = false ) {
174        if ( $local ) {
175            // Protect's deferred admin hook would activate connected modules on the next request.
176            delete_option( static::$plugin_slug . '_activated' );
177        }
178        return parent::do_product_specific_activation( $current_result, $local );
179    }
180
181    /**
182     * The category of the product
183     *
184     * @var string
185     */
186    public static $category = 'security';
187
188    /**
189     * Whether this product requires a user connection
190     *
191     * @var string
192     */
193    public static $requires_user_connection = false;
194
195    /**
196     * Whether this product has a free offering
197     *
198     * @var bool
199     */
200    public static $has_free_offering = true;
201
202    /**
203     * Protect has a standalone plugin
204     *
205     * @var bool
206     */
207    public static $has_standalone_plugin = true;
208
209    /**
210     * The feature slug that identifies the paid plan
211     *
212     * @var string
213     */
214    public static $feature_identifying_paid_plan = 'scan';
215
216    /**
217     * Setup Protect REST API endpoints
218     *
219     * @return void
220     */
221    public static function register_endpoints(): void {
222        parent::register_endpoints();
223        // Get Jetpack Protect data.
224        register_rest_route(
225            'my-jetpack/v1',
226            '/site/protect/data',
227            array(
228                'methods'             => \WP_REST_Server::READABLE,
229                'callback'            => __CLASS__ . '::get_site_protect_data',
230                'permission_callback' => __CLASS__ . '::permissions_callback',
231            )
232        );
233    }
234
235    /**
236     * Checks if the user has the correct permissions
237     */
238    public static function permissions_callback() {
239        return current_user_can( 'edit_posts' );
240    }
241
242    /**
243     * Get the product name
244     *
245     * @return string
246     */
247    public static function get_name() {
248        return 'Protect';
249    }
250
251    /**
252     * Get the product title
253     *
254     * @return string
255     */
256    public static function get_title() {
257        return 'Jetpack Protect';
258    }
259
260    /**
261     * Get the internationalized product description
262     *
263     * @return string
264     */
265    public static function get_description() {
266        return __( 'Guard against malware and bad actors 24/7', 'jetpack-my-jetpack' );
267    }
268
269    /**
270     * Get the internationalized product long description
271     *
272     * @return string
273     */
274    public static function get_long_description() {
275        return __( 'Protect your site from bad actors and malware 24/7. Clean up security vulnerabilities with one click.', 'jetpack-my-jetpack' );
276    }
277
278    /**
279     * Get the internationalized features list
280     *
281     * @return array Protect features list
282     */
283    public static function get_features() {
284        return array(
285            __( 'Over 20,000 listed vulnerabilities', 'jetpack-my-jetpack' ),
286            __( 'Daily automatic scans', 'jetpack-my-jetpack' ),
287            __( 'Check plugin and theme version status', 'jetpack-my-jetpack' ),
288            __( 'Easy to navigate and use', 'jetpack-my-jetpack' ),
289        );
290    }
291
292    /**
293     * Get the product's available tiers
294     *
295     * @return string[] Slugs of the available tiers
296     */
297    public static function get_tiers() {
298        return array(
299            self::UPGRADED_TIER_SLUG,
300            self::FREE_TIER_SLUG,
301        );
302    }
303
304    /**
305     * Get the internationalized comparison of free vs upgraded features
306     *
307     * @deprecated 6.10.0
308     *
309     * @return array[] Protect features comparison
310     */
311    public static function get_features_by_tier() {
312        _deprecated_function( __METHOD__, 'jetpack-my-jetpack-6.10.0' );
313
314        return array(
315            array(
316                'name'  => __( 'Scan for threats and vulnerabilities', 'jetpack-my-jetpack' ),
317                'tiers' => array(
318                    self::FREE_TIER_SLUG     => array(
319                        'included'    => true,
320                        'description' => __( 'Check items against database', 'jetpack-my-jetpack' ),
321                    ),
322                    self::UPGRADED_TIER_SLUG => array(
323                        'included'    => true,
324                        'description' => __( 'Line by line malware scanning', 'jetpack-my-jetpack' ),
325                    ),
326                ),
327            ),
328            array(
329                'name'  => __( 'Daily automated scans', 'jetpack-my-jetpack' ),
330                'tiers' => array(
331                    self::FREE_TIER_SLUG     => array( 'included' => true ),
332                    self::UPGRADED_TIER_SLUG => array(
333                        'included'    => true,
334                        'description' => __( 'Plus on-demand manual scans', 'jetpack-my-jetpack' ),
335                    ),
336                ),
337            ),
338            array(
339                'name'  => __( 'Firewall', 'jetpack-my-jetpack' ),
340                'tiers' => array(
341                    self::FREE_TIER_SLUG     => array(
342                        'included'    => false,
343                        'description' => __( 'Manual rules only', 'jetpack-my-jetpack' ),
344                    ),
345                    self::UPGRADED_TIER_SLUG => array(
346                        'included'    => true,
347                        'description' => __( 'Automatic protection and rule updates', 'jetpack-my-jetpack' ),
348                    ),
349                ),
350            ),
351            array(
352                'name'  => __( 'Login protection', 'jetpack-my-jetpack' ),
353                'tiers' => array(
354                    self::FREE_TIER_SLUG     => array( 'included' => true ),
355                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
356                ),
357            ),
358            array(
359                'name'  => __( 'Account protection', 'jetpack-my-jetpack' ),
360                'tiers' => array(
361                    self::FREE_TIER_SLUG     => array( 'included' => true ),
362                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
363                ),
364            ),
365            array(
366                'name'  => __( 'Access to scan on Cloud', 'jetpack-my-jetpack' ),
367                'tiers' => array(
368                    self::FREE_TIER_SLUG     => array( 'included' => false ),
369                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
370                ),
371            ),
372            array(
373                'name'  => __( 'One-click auto fixes', 'jetpack-my-jetpack' ),
374                'tiers' => array(
375                    self::FREE_TIER_SLUG     => array( 'included' => false ),
376                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
377                ),
378            ),
379            array(
380                'name'  => __( 'Notifications', 'jetpack-my-jetpack' ),
381                'tiers' => array(
382                    self::FREE_TIER_SLUG     => array( 'included' => false ),
383                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
384                ),
385            ),
386            array(
387                'name'  => __( 'Severity labels', 'jetpack-my-jetpack' ),
388                'tiers' => array(
389                    self::FREE_TIER_SLUG     => array( 'included' => false ),
390                    self::UPGRADED_TIER_SLUG => array( 'included' => true ),
391                ),
392            ),
393        );
394    }
395
396    /**
397     * Get the product pricing details
398     *
399     * @return array Pricing details
400     */
401    public static function get_pricing_for_ui() {
402        return array(
403            'tiers' => array(
404                self::FREE_TIER_SLUG     => array(
405                    'available' => true,
406                    'is_free'   => true,
407                ),
408                self::UPGRADED_TIER_SLUG => array_merge(
409                    array(
410                        'available'          => true,
411                        'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
412                    ),
413                    Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
414                ),
415            ),
416        );
417    }
418
419    /**
420     * Determines whether the module/plugin/product needs the users attention.
421     * Typically due to some sort of error where user troubleshooting is needed.
422     *
423     * @return boolean|array
424     */
425    public static function does_module_need_attention() {
426        $protect_threat_status = false;
427        $scan_data             = Protect_Status::get_status();
428
429        // Check if there are scan threats.
430        $protect_data = $scan_data;
431        if ( is_wp_error( $protect_data ) ) {
432            return $protect_threat_status; // false
433        }
434        $critical_threat_count = false;
435        if ( ! empty( $protect_data->threats ) ) {
436            $critical_threat_count = array_reduce(
437                $protect_data->threats,
438                function ( $accum, $threat ) {
439                    return $threat->severity >= 5 ? ++$accum : $accum;
440                },
441                0
442            );
443
444            $protect_threat_status = array(
445                'type' => $critical_threat_count ? 'error' : 'warning',
446                'data' => array(
447                    'threat_count'          => count( $protect_data->threats ),
448                    'critical_threat_count' => $critical_threat_count,
449                    'fixable_threat_ids'    => $protect_data->fixable_threat_ids,
450                ),
451            );
452        }
453
454        return $protect_threat_status;
455    }
456
457    /**
458     * Get the product-slugs of the paid plans for this product.
459     * (Do not include bundle plans, unless it's a bundle plan itself).
460     *
461     * @return array
462     */
463    public static function get_paid_plan_product_slugs() {
464        return Scan::get_paid_plan_product_slugs();
465    }
466
467    /**
468     * Whether the site pays for Scan, by the same check as the Protect page.
469     *
470     * @return bool
471     */
472    public static function has_paid_plan_for_product() {
473        return Plan::has_required_plan();
474    }
475
476    /**
477     * Get the card's status, Expired for a lapsed purchase that Protect Status no longer counts.
478     *
479     * @return string
480     */
481    public static function get_status() {
482        $status = parent::get_status();
483
484        // Plan only sees active purchases; the purchases lookup still lists a lapsed one, as for other products.
485        if ( Products::STATUS_CAN_UPGRADE === $status && static::is_paid_plan_expired() ) {
486            return Products::STATUS_EXPIRED;
487        }
488
489        return $status;
490    }
491
492    /**
493     * Checks whether the product can be upgraded - i.e. this shows the /#add-protect interstitial
494     *
495     * @return boolean
496     */
497    public static function is_upgradable() {
498        return ! self::has_paid_plan_for_product();
499    }
500
501    /**
502     * Get the URL the user is taken after purchasing the product through the checkout
503     *
504     * @return ?string
505     */
506    public static function get_post_checkout_url() {
507        return self::get_manage_url();
508    }
509
510    /**
511     * Get the URL the user is taken after purchasing the product through the checkout for each product feature
512     *
513     * @return ?array
514     */
515    public static function get_post_checkout_urls_by_feature() {
516        return array(
517            self::SCAN_FEATURE_SLUG     => self::get_post_checkout_url(),
518            self::FIREWALL_FEATURE_SLUG => self::get_firewall_settings_url(),
519        );
520    }
521
522    /**
523     * Where each Jetpack security module's settings live while the Protect package's dashboard is the Protect page.
524     *
525     * Screens that would offer one of these settings link there instead.
526     *
527     * @since 6.12.0
528     *
529     * @return array<string, string> Settings URLs keyed by module slug; empty without the dashboard.
530     */
531    public static function get_protect_page_module_urls() {
532        // method_exists, not class_exists: an older bundled Protect package can ship the class without it.
533        $dashboard = 'Automattic\Jetpack\Protect\Dashboard';
534        if ( ! did_action( 'jetpack_protect_dashboard_initialized' ) || ! method_exists( $dashboard, 'get_module_settings_urls' ) ) {
535            return array();
536        }
537
538        return (array) $dashboard::get_module_settings_urls();
539    }
540
541    /**
542     * The firewall's settings: the Protect page's Firewall card, or the standalone plugin's Firewall screen.
543     *
544     * @return string
545     */
546    private static function get_firewall_settings_url() {
547        return self::get_protect_page_module_urls()['waf'] ?? admin_url( 'admin.php?page=jetpack-protect#/firewall' );
548    }
549
550    /**
551     * Whether wp-admin has a Protect page, from the standalone plugin or the Protect package's dashboard.
552     *
553     * @since 6.11.0
554     *
555     * @return bool
556     */
557    public static function has_protect_page() {
558        return static::is_standalone_plugin_active() || (bool) did_action( 'jetpack_protect_dashboard_initialized' );
559    }
560
561    /**
562     * Get the product info, with whether the Protect page exists and where Scan's results are listed.
563     *
564     * @return array
565     */
566    public static function get_info() {
567        return array_merge(
568            parent::get_info(),
569            array(
570                'has_protect_page' => static::has_protect_page(),
571                'scan_results_url' => static::get_manage_url(),
572            )
573        );
574    }
575
576    /**
577     * Where the card's upgrade goes: a host's plans when it sends them, else the Protect interstitial.
578     *
579     * @return ?string
580     */
581    public static function get_purchase_url() {
582        if ( static::has_paid_plan_for_product() ) {
583            return parent::get_purchase_url();
584        }
585
586        /** This filter is documented in projects/packages/my-jetpack/src/class-main-features.php */
587        $upgrade = apply_filters(
588            'jetpack_feature_upgrade',
589            array(
590                'path'     => '/add-protect',
591                'name'     => 'Jetpack Scan', // Product name, do not translate.
592                'back_url' => admin_url( 'admin.php?page=my-jetpack' ),
593            ),
594            'protect-dashboard',
595            false
596        );
597
598        $url = is_array( $upgrade ) ? esc_url_raw( (string) ( $upgrade['url'] ?? '' ) ) : '';
599
600        return $url ? $url : parent::get_purchase_url();
601    }
602
603    /**
604     * Where a site with Scan manages its threats without the Protect page: WordPress.com on WoA, Jetpack Cloud elsewhere.
605     *
606     * @return string
607     */
608    public static function get_scan_dashboard_url() {
609        return Redirect::get_url( ( new Host() )->is_woa_site() ? 'my-jetpack-manage-scan-wpcom' : 'my-jetpack-manage-scan' );
610    }
611
612    /**
613     * Get the URL where the user manages the product
614     *
615     * @return ?string
616     */
617    public static function get_manage_url() {
618        if ( static::has_protect_page() ) {
619            // Protect admin dashboard.
620            return admin_url( 'admin.php?page=jetpack-protect' );
621        }
622
623        if ( static::has_paid_plan_for_product() ) {
624            return static::get_scan_dashboard_url();
625        }
626
627        // Free users without standalone plugin go to the Protect details page.
628        return admin_url( 'admin.php?page=my-jetpack#/protect-details' );
629    }
630
631    /**
632     * Get the URL where the user manages the product for each product feature
633     *
634     * @return ?array
635     */
636    public static function get_manage_urls_by_feature() {
637        return array(
638            self::SCAN_FEATURE_SLUG     => self::get_manage_url(),
639            self::FIREWALL_FEATURE_SLUG => self::get_firewall_settings_url(),
640        );
641    }
642
643    /**
644     * Return product bundles list
645     * that supports the product.
646     *
647     * @return array Products bundle list.
648     */
649    public static function is_upgradable_by_bundle() {
650        return array( 'security', 'complete' );
651    }
652
653    /**
654     * Return site Jetpack Protect data for the REST API.
655     *
656     * @return WP_Rest_Response|WP_Error
657     */
658    public static function get_site_protect_data() {
659        $scan_data = Protect_Status::get_status();
660
661        $waf_config          = array();
662        $waf_supported       = false;
663        $is_waf_enabled      = false;
664        $is_provided_by_host = false;
665
666        if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
667            $waf_config          = Waf_Runner::get_config();
668            $is_waf_enabled      = Waf_Runner::is_enabled();
669            $waf_supported       = Waf_Runner::is_supported_environment();
670            $is_provided_by_host = method_exists( Waf_Runner::class, 'is_provided_by_host' ) && Waf_Runner::is_provided_by_host();
671        }
672
673        return rest_ensure_response(
674            array(
675                'scanData'  => self::filter_scan_data_by_capability( $scan_data ),
676                'wafConfig' => array_merge(
677                    self::filter_waf_config_by_capability( $waf_config ),
678                    array(
679                        'waf_supported'        => $waf_supported,
680                        'waf_enabled'          => $is_waf_enabled,
681                        'waf_provided_by_host' => $is_provided_by_host,
682                    ),
683                    array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
684                ),
685            )
686        );
687    }
688
689    /**
690     * Reduce the WAF configuration to the keys the current user is allowed to read.
691     *
692     * @param array $waf_config The WAF configuration as returned by Waf_Runner::get_config().
693     * @return array
694     */
695    private static function filter_waf_config_by_capability( array $waf_config ) {
696        if ( current_user_can( 'manage_options' ) ) {
697            return $waf_config;
698        }
699
700        return array_intersect_key( $waf_config, array_flip( self::NON_ADMIN_WAF_CONFIG_KEYS ) );
701    }
702
703    /**
704     * Reduce the scan status to the parts the current user is allowed to read.
705     *
706     * @param \Automattic\Jetpack\Protect_Models\Status_Model $scan_data The scan status as returned by Protect_Status::get_status().
707     * @return \Automattic\Jetpack\Protect_Models\Status_Model|array
708     */
709    private static function filter_scan_data_by_capability( $scan_data ) {
710        if ( current_user_can( 'manage_options' ) ) {
711            return $scan_data;
712        }
713
714        $filtered = array_intersect_key( (array) $scan_data, array_flip( self::NON_ADMIN_SCAN_DATA_KEYS ) );
715
716        // `files` and `database` hold Threat_Model instances, not extensions.
717        foreach ( array( 'threats', 'files', 'database' ) as $key ) {
718            $filtered[ $key ] = array_map( array( __CLASS__, 'redact_threat' ), (array) ( $filtered[ $key ] ?? array() ) );
719        }
720
721        foreach ( array( 'plugins', 'themes' ) as $key ) {
722            $filtered[ $key ] = array_map( array( __CLASS__, 'redact_extension' ), (array) ( $filtered[ $key ] ?? array() ) );
723        }
724
725        $filtered['core'] = self::redact_extension( $filtered['core'] ?? array() );
726
727        return $filtered;
728    }
729
730    /**
731     * Reduce a threat to the properties a user without `manage_options` may read.
732     *
733     * @param object|array $threat A Threat_Model instance.
734     * @return array
735     */
736    private static function redact_threat( $threat ) {
737        return array_intersect_key( (array) $threat, array_flip( self::NON_ADMIN_THREAT_KEYS ) );
738    }
739
740    /**
741     * Reduce an extension to the properties a user without `manage_options` may read.
742     *
743     * Only the nested threats survive: the extension's name, slug and installed version identify
744     * which vulnerable software the site is running.
745     *
746     * @param object|array $extension An Extension_Model instance.
747     * @return array
748     */
749    private static function redact_extension( $extension ) {
750        $threats = ( (array) $extension )['threats'] ?? array();
751
752        return array( 'threats' => array_map( array( __CLASS__, 'redact_threat' ), (array) $threats ) );
753    }
754}