Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.22% covered (success)
93.22%
55 / 59
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Package_Version_Tracker
93.22% covered (success)
93.22%
55 / 59
50.00% covered (danger)
50.00%
3 / 6
25.19
0.00% covered (danger)
0.00%
0 / 1
 update_on_shutdown
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybe_update_package_versions
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
12
 update_package_versions_option
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 is_sync_enabled
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 update_package_versions_via_remote_request
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
4
 is_rate_limiting
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * The Package_Version_Tracker class.
4 *
5 * @package automattic/jetpack-connection
6 */
7
8namespace Automattic\Jetpack\Connection;
9
10use Jetpack_Options;
11
12/**
13 * The Package_Version_Tracker class.
14 */
15class Package_Version_Tracker {
16
17    const PACKAGE_VERSION_OPTION = 'jetpack_package_versions';
18
19    /**
20     * The cache key for storing a failed request to update remote package versions.
21     * The caching logic is that when a failed request occurs, we cache it temporarily
22     * with a set expiration time.
23     * Only after the key has expired, we'll be able to repeat a remote request.
24     * This also implies that the cached value is redundant, however we chose the datetime
25     * of the failed request to avoid using booleans.
26     */
27    const CACHED_FAILED_REQUEST_KEY = 'jetpack_failed_update_remote_package_versions';
28
29    /**
30     * The min time difference in seconds for attempting to
31     * update remote tracked package versions after a failed remote request.
32     */
33    const CACHED_FAILED_REQUEST_EXPIRATION = 1 * HOUR_IN_SECONDS;
34
35    /**
36     * Transient key for rate limiting the package version requests;
37     */
38    const RATE_LIMITER_KEY = 'jetpack_update_remote_package_last_query';
39
40    /**
41     * Only allow one versions check (and request) per minute.
42     */
43    const RATE_LIMITER_TIMEOUT = MINUTE_IN_SECONDS;
44
45    /**
46     * Runs the package version check on the `shutdown` hook.
47     *
48     * Static so the callback can be unregistered, and so the tracker is only
49     * constructed when the hook fires rather than during configuration.
50     */
51    public static function update_on_shutdown() {
52        ( new self() )->maybe_update_package_versions();
53    }
54
55    /**
56     * Uses the jetpack_package_versions filter to obtain the package versions from packages that need
57     * version tracking. If the package versions have changed, updates the option and notifies WPCOM.
58     */
59    public function maybe_update_package_versions() {
60        // Do not run too early or all the modules may not be loaded.
61        if ( ! did_action( 'init' ) ) {
62            return;
63        }
64
65        // Only attempt to update the option on POST requests.
66        // This will prevent the option from being updated multiple times due to concurrent requests.
67        if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
68            return;
69        }
70
71        // The version check is being rate limited.
72        if ( $this->is_rate_limiting() ) {
73            return;
74        }
75
76        /**
77         * Obtains the package versions.
78         *
79         * @since 1.30.2
80         *
81         * @param array An associative array of Jetpack package slugs and their corresponding versions as key/value pairs.
82         */
83        $filter_versions = apply_filters( 'jetpack_package_versions', array() );
84
85        if ( ! is_array( $filter_versions ) ) {
86            return;
87        }
88
89        $option_versions = get_option( self::PACKAGE_VERSION_OPTION, array() );
90
91        foreach ( $filter_versions as $package => $version ) {
92            if ( ! is_string( $package ) || ! is_string( $version ) ) {
93                unset( $filter_versions[ $package ] );
94            }
95        }
96
97        if ( ! is_array( $option_versions )
98            || count( array_diff_assoc( $filter_versions, $option_versions ) )
99            || count( array_diff_assoc( $option_versions, $filter_versions ) )
100        ) {
101            $this->update_package_versions_option( $filter_versions );
102        }
103    }
104
105    /**
106     * Updates the package versions option.
107     *
108     * @param array $package_versions The package versions.
109     */
110    protected function update_package_versions_option( $package_versions ) {
111        if ( ! $this->is_sync_enabled() ) {
112            $this->update_package_versions_via_remote_request( $package_versions );
113            // Remove the checksum for package versions, so it gets recalculated when sync gets activated.
114            $jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
115            if ( isset( $jetpack_callables_sync_checksum['jetpack_package_versions'] ) ) {
116                unset( $jetpack_callables_sync_checksum['jetpack_package_versions'] );
117                Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
118            }
119            return;
120        }
121
122        update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
123    }
124
125    /**
126     * Whether Jetpack Sync is enabled.
127     *
128     * @return boolean true if Sync is present and enabled, false otherwise
129     */
130    protected function is_sync_enabled() {
131        if ( class_exists( 'Automattic\Jetpack\Sync\Settings' ) && \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
132
133            return true;
134        }
135
136        return false;
137    }
138
139    /**
140     * Fallback for updating the package versions via a remote request when Sync is not present.
141     *
142     * Updates the package versions as follows:
143     *   - Sends the updated package versions to wpcom.
144     *   - Updates the 'jetpack_package_versions' option.
145     *
146     * @param array $package_versions The package versions.
147     */
148    protected function update_package_versions_via_remote_request( $package_versions ) {
149        $connection = new Manager();
150        if ( ! $connection->is_connected() ) {
151            return;
152        }
153
154        $site_id = \Jetpack_Options::get_option( 'id' );
155
156        $last_failed_attempt_within_hour = get_transient( self::CACHED_FAILED_REQUEST_KEY );
157
158        if ( $last_failed_attempt_within_hour ) {
159            return;
160        }
161
162        $body = wp_json_encode(
163            array(
164                'package_versions' => $package_versions,
165            ),
166            JSON_UNESCAPED_SLASHES
167        );
168
169        $response = Client::wpcom_json_api_request_as_blog(
170            sprintf( '/sites/%d/jetpack-package-versions', $site_id ),
171            '2',
172            array(
173                'headers' => array( 'content-type' => 'application/json' ),
174                'method'  => 'POST',
175            ),
176            $body,
177            'wpcom'
178        );
179
180        if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
181            update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
182        } else {
183            set_transient( self::CACHED_FAILED_REQUEST_KEY, time(), self::CACHED_FAILED_REQUEST_EXPIRATION );
184        }
185    }
186
187    /**
188     * Check if version check is being rate limited, and update the rate limiting transient if needed.
189     *
190     * @return bool
191     */
192    private function is_rate_limiting() {
193        if ( get_transient( static::RATE_LIMITER_KEY ) ) {
194            return true;
195        }
196
197        set_transient( static::RATE_LIMITER_KEY, time(), static::RATE_LIMITER_TIMEOUT );
198
199        return false;
200    }
201}