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