Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.67% covered (danger)
46.67%
28 / 60
66.67% covered (warning)
66.67%
10 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Status
46.67% covered (danger)
46.67%
28 / 60
66.67% covered (warning)
66.67%
10 / 15
257.06
0.00% covered (danger)
0.00%
0 / 1
 get_status
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_cached_status
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_cached_status_expired
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_status_class
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
9.66
 is_cache_expired
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 should_use_cache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_from_options
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 update_status_option
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 get_cache_end_date_by_status
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 delete_option
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 should_notify_threats
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 has_threats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_total_threats
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_all_threats
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_wordpress_threats
n/a
0 / 0
n/a
0 / 0
1
 get_themes_threats
n/a
0 / 0
n/a
0 / 0
1
 get_plugins_threats
n/a
0 / 0
n/a
0 / 0
1
 get_files_threats
n/a
0 / 0
n/a
0 / 0
1
 get_database_threats
n/a
0 / 0
n/a
0 / 0
1
 get_threats
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 sort_threats
n/a
0 / 0
n/a
0 / 0
2
1<?php
2/**
3 * Class to handle the Status of Jetpack Protect
4 *
5 * @package automattic/jetpack-protect-status
6 */
7
8namespace Automattic\Jetpack\Protect_Status;
9
10use Automattic\Jetpack\Protect_Models\Status_Model;
11
12/**
13 * Class that handles fetching and caching the Status of vulnerabilities check from the WPCOM servers
14 */
15class Status {
16
17    const PACKAGE_VERSION = '0.10.2';
18    /**
19     * Name of the option where status is stored
20     *
21     * @var string
22     */
23    const OPTION_NAME = '';
24
25    /**
26     * Name of the option where the timestamp of the status is stored
27     *
28     * @var string
29     */
30    const OPTION_TIMESTAMP_NAME = '';
31
32    /**
33     * Time in seconds that the cache should last
34     *
35     * @var int
36     */
37    const OPTION_EXPIRES_AFTER = 3600; // 1 hour.
38
39    /**
40     * Time in seconds that the cache for the initial empty response should last
41     *
42     * @var int
43     */
44    const INITIAL_OPTION_EXPIRES_AFTER = 1 * MINUTE_IN_SECONDS;
45
46    /**
47     * Memoization for the current status
48     *
49     * @var null|Status_Model
50     */
51    public static $status = null;
52
53    /**
54     * Gets the current status of the Jetpack Protect checks
55     *
56     * @param bool $refresh_from_wpcom Refresh the local plan and status cache from wpcom.
57     * @return Status_Model
58     */
59    public static function get_status( $refresh_from_wpcom = false ) {
60        $status_class = self::get_status_class( $refresh_from_wpcom );
61        self::$status = $status_class::get_status( $refresh_from_wpcom );
62        return self::$status;
63    }
64
65    /**
66     * Gets the last status stored on the site, even if it has expired, without asking WordPress.com.
67     *
68     * @since 0.9.0
69     *
70     * @return Status_Model|null Null when no status has been stored yet.
71     */
72    public static function get_cached_status() {
73        $status_class = self::get_status_class();
74        return $status_class::get_cached_status();
75    }
76
77    /**
78     * Whether the stored status is missing or old enough that get_status() would fetch a new one.
79     *
80     * @since 0.9.0
81     *
82     * @return bool
83     */
84    public static function is_cached_status_expired() {
85        $status_class = self::get_status_class();
86        return ! $status_class::should_use_cache() || $status_class::is_cache_expired();
87    }
88
89    /**
90     * The class that provides this site's status: Scan's when the plan includes it, the Protect report otherwise.
91     *
92     * @param bool $refresh_from_wpcom Refresh the local plan cache from wpcom.
93     * @return class-string<Scan_Status|Protect_Status>
94     */
95    private static function get_status_class( $refresh_from_wpcom = false ) {
96        $use_scan_status = Plan::has_required_plan( $refresh_from_wpcom );
97
98        if ( defined( 'JETPACK_PROTECT_DEV__DATA_SOURCE' ) ) {
99            if ( 'scan_api' === JETPACK_PROTECT_DEV__DATA_SOURCE ) {
100                $use_scan_status = true;
101            }
102
103            if ( 'protect_report' === JETPACK_PROTECT_DEV__DATA_SOURCE ) {
104                $use_scan_status = false;
105            }
106        }
107
108        return $use_scan_status ? Scan_Status::class : Protect_Status::class;
109    }
110
111    /**
112     * Checks if the current cached status is expired and should be renewed
113     *
114     * @return boolean
115     */
116    public static function is_cache_expired() {
117        $option_timestamp = get_option( static::OPTION_TIMESTAMP_NAME );
118
119        if ( ! $option_timestamp ) {
120            return true;
121        }
122
123        return time() > (int) $option_timestamp;
124    }
125
126    /**
127     * Checks if we should consider the stored cache or bypass it
128     *
129     * @return boolean
130     */
131    public static function should_use_cache() {
132        return ! ( defined( 'JETPACK_PROTECT_DEV__BYPASS_CACHE' ) && JETPACK_PROTECT_DEV__BYPASS_CACHE );
133    }
134
135    /**
136     * Gets the current cached status
137     *
138     * @return bool|array False if value is not found. Array with values if cache is found.
139     */
140    public static function get_from_options() {
141        return maybe_unserialize( get_option( static::OPTION_NAME ) );
142    }
143
144    /**
145     * Updated the cached status and its timestamp
146     *
147     * @param object $status The new status to be cached, as decoded from the API response.
148     * @return void
149     */
150    public static function update_status_option( $status ) {
151        // TODO: Sanitize $status.
152        update_option( static::OPTION_NAME, maybe_serialize( $status ) );
153        $end_date = self::get_cache_end_date_by_status( $status );
154        update_option( static::OPTION_TIMESTAMP_NAME, $end_date );
155    }
156
157    /**
158     * Returns the timestamp the cache should expire depending on the current status
159     *
160     * Initial empty status, which are returned before the first check was performed, should be cache for less time
161     *
162     * @param object $status The response from the server being cached.
163     * @return int The timestamp when the cache should expire.
164     */
165    public static function get_cache_end_date_by_status( $status ) {
166        if ( ! is_object( $status ) || empty( $status->last_checked ) ) {
167            return time() + static::INITIAL_OPTION_EXPIRES_AFTER;
168        }
169        return time() + static::OPTION_EXPIRES_AFTER;
170    }
171
172    /**
173     * Delete the cached status and its timestamp
174     *
175     * @return bool Whether all related status options were successfully deleted.
176     */
177    public static function delete_option() {
178        $option_deleted           = delete_option( static::OPTION_NAME );
179        $option_timestamp_deleted = delete_option( static::OPTION_TIMESTAMP_NAME );
180
181        return $option_deleted && $option_timestamp_deleted;
182    }
183
184    /**
185     * Whether threats may be announced outside the Protect page; never while the site owner has the free check off.
186     *
187     * @since 0.10.0
188     *
189     * @return bool
190     */
191    public static function should_notify_threats() {
192        if ( ! Protect_Status::are_checks_enabled() && Protect_Status::class === self::get_status_class() ) {
193            return false;
194        }
195
196        /**
197         * Filters whether threats are announced outside the Protect page. Hosts that handle threats themselves can turn this off.
198         *
199         * @since 0.10.0
200         *
201         * @param bool $enabled Whether to announce threats. Default true.
202         */
203        return (bool) apply_filters( 'jetpack_protect_threat_notifications_enabled', true );
204    }
205
206    /**
207     * Checks the current status to see if there are any threats found
208     *
209     * @return boolean
210     */
211    public static function has_threats() {
212        return 0 < self::get_total_threats();
213    }
214
215    /**
216     * Gets the total number of threats found
217     *
218     * @return integer
219     */
220    public static function get_total_threats() {
221        $status = static::get_status();
222        return count( $status->threats );
223    }
224
225    /**
226     * Get all threats combined
227     *
228     * @return array
229     */
230    public static function get_all_threats() {
231        $status = static::get_status();
232        return $status->threats;
233    }
234
235    /**
236     * Get threats found for WordPress core
237     *
238     * @deprecated 0.3.0
239     *
240     * @return array
241     */
242    public static function get_wordpress_threats() {
243        return self::get_threats( 'core' );
244    }
245
246    /**
247     * Get threats found for themes
248     *
249     * @deprecated 0.3.0
250     *
251     * @return array
252     */
253    public static function get_themes_threats() {
254        return self::get_threats( 'themes' );
255    }
256
257    /**
258     * Get threats found for plugins
259     *
260     * @deprecated 0.3.0
261     *
262     * @return array
263     */
264    public static function get_plugins_threats() {
265        return self::get_threats( 'plugins' );
266    }
267
268    /**
269     * Get threats found for files
270     *
271     * @deprecated 0.3.0
272     *
273     * @return array
274     */
275    public static function get_files_threats() {
276        return self::get_threats( 'files' );
277    }
278
279    /**
280     * Get threats found for plugins
281     *
282     * @deprecated 0.3.0
283     *
284     * @return array
285     */
286    public static function get_database_threats() {
287        return self::get_threats( 'database' );
288    }
289
290    /**
291     * Get the threats for one type of extension or core
292     *
293     * @param string $type What threats you want to get. Possible values are 'core', 'themes' and 'plugins'.
294     *
295     * @return array
296     */
297    public static function get_threats( $type ) {
298        $status = static::get_status();
299
300        if ( in_array( $type, array( 'plugin', 'theme', 'core' ), true ) ) {
301            return array_filter(
302                $status->threats,
303                function ( $threat ) use ( $type ) {
304                    return isset( $threat->extension ) && $type === $threat->extension->type;
305                }
306            );
307        }
308
309        if ( 'files' === $type ) {
310            return array_filter(
311                $status->threats,
312                function ( $threat ) {
313                    return ! empty( $threat->filename );
314                }
315            );
316        }
317
318        if ( 'database' === $type ) {
319            return array_filter(
320                $status->threats,
321                function ( $threat ) {
322                    return ! empty( $threat->table );
323                }
324            );
325        }
326
327        return $status->threats;
328    }
329
330    /**
331     * Sort By Threats
332     *
333     * @deprecated 0.3.0
334     *
335     * @param array<object> $threats Array of threats to sort.
336     *
337     * @return array<object> The sorted $threats array.
338     */
339    protected static function sort_threats( $threats ) {
340        usort(
341            $threats,
342            function ( $a, $b ) {
343                // sort primarily based on the presence of threats
344                $ret = empty( $a->threats ) <=> empty( $b->threats );
345
346                // sort secondarily on whether the item has been checked
347                if ( ! $ret ) {
348                    $ret = $a->checked <=> $b->checked;
349                }
350
351                return $ret;
352            }
353        );
354
355        return $threats;
356    }
357}