Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.35% covered (danger)
40.35%
23 / 57
64.29% covered (warning)
64.29%
9 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Status
40.35% covered (danger)
40.35%
23 / 57
64.29% covered (warning)
64.29%
9 / 14
294.98
0.00% covered (danger)
0.00%
0 / 1
 get_status
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 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
 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.8.1';
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 $$next-version$$
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 $$next-version$$
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 array $status The new status to be cached.
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     * Checks the current status to see if there are any threats found
186     *
187     * @return boolean
188     */
189    public static function has_threats() {
190        return 0 < self::get_total_threats();
191    }
192
193    /**
194     * Gets the total number of threats found
195     *
196     * @return integer
197     */
198    public static function get_total_threats() {
199        $status = static::get_status();
200        return count( $status->threats );
201    }
202
203    /**
204     * Get all threats combined
205     *
206     * @return array
207     */
208    public static function get_all_threats() {
209        $status = static::get_status();
210        return $status->threats;
211    }
212
213    /**
214     * Get threats found for WordPress core
215     *
216     * @deprecated 0.3.0
217     *
218     * @return array
219     */
220    public static function get_wordpress_threats() {
221        return self::get_threats( 'core' );
222    }
223
224    /**
225     * Get threats found for themes
226     *
227     * @deprecated 0.3.0
228     *
229     * @return array
230     */
231    public static function get_themes_threats() {
232        return self::get_threats( 'themes' );
233    }
234
235    /**
236     * Get threats found for plugins
237     *
238     * @deprecated 0.3.0
239     *
240     * @return array
241     */
242    public static function get_plugins_threats() {
243        return self::get_threats( 'plugins' );
244    }
245
246    /**
247     * Get threats found for files
248     *
249     * @deprecated 0.3.0
250     *
251     * @return array
252     */
253    public static function get_files_threats() {
254        return self::get_threats( 'files' );
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_database_threats() {
265        return self::get_threats( 'database' );
266    }
267
268    /**
269     * Get the threats for one type of extension or core
270     *
271     * @param string $type What threats you want to get. Possible values are 'core', 'themes' and 'plugins'.
272     *
273     * @return array
274     */
275    public static function get_threats( $type ) {
276        $status = static::get_status();
277
278        if ( in_array( $type, array( 'plugin', 'theme', 'core' ), true ) ) {
279            return array_filter(
280                $status->threats,
281                function ( $threat ) use ( $type ) {
282                    return isset( $threat->extension ) && $type === $threat->extension->type;
283                }
284            );
285        }
286
287        if ( 'files' === $type ) {
288            return array_filter(
289                $status->threats,
290                function ( $threat ) {
291                    return ! empty( $threat->filename );
292                }
293            );
294        }
295
296        if ( 'database' === $type ) {
297            return array_filter(
298                $status->threats,
299                function ( $threat ) {
300                    return ! empty( $threat->table );
301                }
302            );
303        }
304
305        return $status->threats;
306    }
307
308    /**
309     * Sort By Threats
310     *
311     * @deprecated 0.3.0
312     *
313     * @param array<object> $threats Array of threats to sort.
314     *
315     * @return array<object> The sorted $threats array.
316     */
317    protected static function sort_threats( $threats ) {
318        usort(
319            $threats,
320            function ( $a, $b ) {
321                // sort primarily based on the presence of threats
322                $ret = empty( $a->threats ) <=> empty( $b->threats );
323
324                // sort secondarily on whether the item has been checked
325                if ( ! $ret ) {
326                    $ret = $a->checked <=> $b->checked;
327                }
328
329                return $ret;
330            }
331        );
332
333        return $threats;
334    }
335}