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