Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.83% covered (success)
94.83%
55 / 58
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Package_Version_Tracker
94.83% covered (success)
94.83%
55 / 58
60.00% covered (warning)
60.00%
3 / 5
24.08
0.00% covered (danger)
0.00%
0 / 1
 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     * Uses the jetpack_package_versions filter to obtain the package versions from packages that need
47     * version tracking. If the package versions have changed, updates the option and notifies WPCOM.
48     */
49    public function maybe_update_package_versions() {
50        // Do not run too early or all the modules may not be loaded.
51        if ( ! did_action( 'init' ) ) {
52            return;
53        }
54
55        // Only attempt to update the option on POST requests.
56        // This will prevent the option from being updated multiple times due to concurrent requests.
57        if ( ! ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) {
58            return;
59        }
60
61        // The version check is being rate limited.
62        if ( $this->is_rate_limiting() ) {
63            return;
64        }
65
66        /**
67         * Obtains the package versions.
68         *
69         * @since 1.30.2
70         *
71         * @param array An associative array of Jetpack package slugs and their corresponding versions as key/value pairs.
72         */
73        $filter_versions = apply_filters( 'jetpack_package_versions', array() );
74
75        if ( ! is_array( $filter_versions ) ) {
76            return;
77        }
78
79        $option_versions = get_option( self::PACKAGE_VERSION_OPTION, array() );
80
81        foreach ( $filter_versions as $package => $version ) {
82            if ( ! is_string( $package ) || ! is_string( $version ) ) {
83                unset( $filter_versions[ $package ] );
84            }
85        }
86
87        if ( ! is_array( $option_versions )
88            || count( array_diff_assoc( $filter_versions, $option_versions ) )
89            || count( array_diff_assoc( $option_versions, $filter_versions ) )
90        ) {
91            $this->update_package_versions_option( $filter_versions );
92        }
93    }
94
95    /**
96     * Updates the package versions option.
97     *
98     * @param array $package_versions The package versions.
99     */
100    protected function update_package_versions_option( $package_versions ) {
101        if ( ! $this->is_sync_enabled() ) {
102            $this->update_package_versions_via_remote_request( $package_versions );
103            // Remove the checksum for package versions, so it gets recalculated when sync gets activated.
104            $jetpack_callables_sync_checksum = Jetpack_Options::get_raw_option( 'jetpack_callables_sync_checksum' );
105            if ( isset( $jetpack_callables_sync_checksum['jetpack_package_versions'] ) ) {
106                unset( $jetpack_callables_sync_checksum['jetpack_package_versions'] );
107                Jetpack_Options::update_raw_option( 'jetpack_callables_sync_checksum', $jetpack_callables_sync_checksum );
108            }
109            return;
110        }
111
112        update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
113    }
114
115    /**
116     * Whether Jetpack Sync is enabled.
117     *
118     * @return boolean true if Sync is present and enabled, false otherwise
119     */
120    protected function is_sync_enabled() {
121        if ( class_exists( 'Automattic\Jetpack\Sync\Settings' ) && \Automattic\Jetpack\Sync\Settings::is_sync_enabled() ) {
122
123            return true;
124        }
125
126        return false;
127    }
128
129    /**
130     * Fallback for updating the package versions via a remote request when Sync is not present.
131     *
132     * Updates the package versions as follows:
133     *   - Sends the updated package versions to wpcom.
134     *   - Updates the 'jetpack_package_versions' option.
135     *
136     * @param array $package_versions The package versions.
137     */
138    protected function update_package_versions_via_remote_request( $package_versions ) {
139        $connection = new Manager();
140        if ( ! $connection->is_connected() ) {
141            return;
142        }
143
144        $site_id = \Jetpack_Options::get_option( 'id' );
145
146        $last_failed_attempt_within_hour = get_transient( self::CACHED_FAILED_REQUEST_KEY );
147
148        if ( $last_failed_attempt_within_hour ) {
149            return;
150        }
151
152        $body = wp_json_encode(
153            array(
154                'package_versions' => $package_versions,
155            ),
156            JSON_UNESCAPED_SLASHES
157        );
158
159        $response = Client::wpcom_json_api_request_as_blog(
160            sprintf( '/sites/%d/jetpack-package-versions', $site_id ),
161            '2',
162            array(
163                'headers' => array( 'content-type' => 'application/json' ),
164                'method'  => 'POST',
165            ),
166            $body,
167            'wpcom'
168        );
169
170        if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
171            update_option( self::PACKAGE_VERSION_OPTION, $package_versions );
172        } else {
173            set_transient( self::CACHED_FAILED_REQUEST_KEY, time(), self::CACHED_FAILED_REQUEST_EXPIRATION );
174        }
175    }
176
177    /**
178     * Check if version check is being rate limited, and update the rate limiting transient if needed.
179     *
180     * @return bool
181     */
182    private function is_rate_limiting() {
183        if ( get_transient( static::RATE_LIMITER_KEY ) ) {
184            return true;
185        }
186
187        set_transient( static::RATE_LIMITER_KEY, time(), static::RATE_LIMITER_TIMEOUT );
188
189        return false;
190    }
191}