Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
25.00% covered (danger)
25.00%
41 / 164
13.04% covered (danger)
13.04%
3 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Updates
24.69% covered (danger)
24.69%
40 / 162
13.04% covered (danger)
13.04%
3 / 23
2676.51
0.00% covered (danger)
0.00%
0 / 1
 set_defaults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init_listeners
94.87% covered (success)
94.87%
37 / 39
0.00% covered (danger)
0.00%
0 / 1
2.00
 init_full_sync_listeners
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init_before_send
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update_core_network_event
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 update_core
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 get_update_checksum
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
870
 validate_update_change
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 sync_last_event
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_full_sync_actions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 send_full_sync_actions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 estimate_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_full_sync_actions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_all_updates
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 filter_update_keys
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 filter_upgrader_process_complete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 expand_updates
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 expand_themes
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 reset_data
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_objects_by_id
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 get_object_by_id
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Updates sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync\Modules;
9
10use Automattic\Jetpack\Constants as Jetpack_Constants;
11
12if ( ! defined( 'ABSPATH' ) ) {
13    exit( 0 );
14}
15
16/**
17 * Class to handle sync for updates.
18 */
19class Updates extends Module {
20    /**
21     * Name of the updates checksum option.
22     *
23     * @var string
24     */
25    const UPDATES_CHECKSUM_OPTION_NAME = 'jetpack_updates_sync_checksum';
26
27    /**
28     * WordPress Version.
29     *
30     * @access private
31     *
32     * @var string
33     */
34    private $old_wp_version = null;
35
36    /**
37     * The current updates.
38     *
39     * @access private
40     *
41     * @var array
42     */
43    private $updates = array();
44
45    /**
46     * Set module defaults.
47     *
48     * @access public
49     */
50    public function set_defaults() {
51        $this->updates = array();
52    }
53
54    /**
55     * Sync module name.
56     *
57     * @access public
58     *
59     * @return string
60     */
61    public function name() {
62        return 'updates';
63    }
64
65    /**
66     * Initialize updates action listeners.
67     *
68     * @access public
69     *
70     * @param callable $callable Action handler callable.
71     */
72    public function init_listeners( $callable ) {
73        global $wp_version;
74        $this->old_wp_version = $wp_version;
75        add_action( 'set_site_transient_update_plugins', array( $this, 'validate_update_change' ), 10, 3 );
76        add_action( 'set_site_transient_update_themes', array( $this, 'validate_update_change' ), 10, 3 );
77        add_action( 'set_site_transient_update_core', array( $this, 'validate_update_change' ), 10, 3 );
78
79        add_action( 'jetpack_update_plugins_change', $callable );
80        add_action( 'jetpack_update_themes_change', $callable );
81        add_action( 'jetpack_update_core_change', $callable );
82
83        add_filter(
84            'jetpack_sync_before_enqueue_jetpack_update_themes_change',
85            array(
86                $this,
87                'expand_themes',
88            )
89        );
90
91        add_filter(
92            'jetpack_sync_before_enqueue_jetpack_update_plugins_change',
93            array(
94                $this,
95                'filter_update_keys',
96            ),
97            10,
98            2
99        );
100        add_filter(
101            'jetpack_sync_before_enqueue_upgrader_process_complete',
102            array(
103                $this,
104                'filter_upgrader_process_complete',
105            ),
106            10,
107            2
108        );
109
110        if ( is_multisite() ) {
111            add_filter( 'pre_update_site_option_wpmu_upgrade_site', array( $this, 'update_core_network_event' ), 10, 2 );
112            add_action( 'jetpack_sync_core_update_network', $callable, 10, 3 );
113        }
114
115        // Send data when update completes.
116        add_action( '_core_updated_successfully', array( $this, 'update_core' ) );
117        add_action( 'jetpack_sync_core_reinstalled_successfully', $callable );
118        add_action( 'jetpack_sync_core_autoupdated_successfully', $callable, 10, 2 );
119        add_action( 'jetpack_sync_core_updated_successfully', $callable, 10, 2 );
120    }
121
122    /**
123     * Initialize updates action listeners for full sync.
124     *
125     * @access public
126     *
127     * @param callable $callable Action handler callable.
128     */
129    public function init_full_sync_listeners( $callable ) {
130        add_action( 'jetpack_full_sync_updates', $callable );
131    }
132
133    /**
134     * Initialize the module in the sender.
135     *
136     * @access public
137     */
138    public function init_before_send() {
139        add_filter( 'jetpack_sync_before_send_jetpack_full_sync_updates', array( $this, 'expand_updates' ) );
140    }
141
142    /**
143     * Handle a core network update.
144     *
145     * @access public
146     *
147     * @param int $wp_db_version     Current version of the WordPress database.
148     * @param int $old_wp_db_version Old version of the WordPress database.
149     * @return int Current version of the WordPress database.
150     */
151    public function update_core_network_event( $wp_db_version, $old_wp_db_version ) {
152        global $wp_version;
153        /**
154         * Sync event for when core wp network updates to a new db version
155         *
156         * @since 1.6.3
157         * @since-jetpack 5.0.0
158         *
159         * @param int $wp_db_version the latest wp_db_version
160         * @param int $old_wp_db_version previous wp_db_version
161         * @param string $wp_version the latest wp_version
162         */
163        do_action( 'jetpack_sync_core_update_network', $wp_db_version, $old_wp_db_version, $wp_version );
164        return $wp_db_version;
165    }
166
167    /**
168     * Handle a core update.
169     *
170     * @access public
171     *
172     * @todo Implement nonce or refactor to use `admin_post_{$action}` hooks instead.
173     *
174     * @param string $new_wp_version The new WP core version.
175     */
176    public function update_core( $new_wp_version ) {
177        global $pagenow;
178
179        // // phpcs:ignore WordPress.Security.NonceVerification.Recommended
180        if ( isset( $_GET['action'] ) && 'do-core-reinstall' === $_GET['action'] ) {
181            /**
182             * Sync event that fires when core reinstall was successful
183             *
184             * @since 1.6.3
185             * @since-jetpack 5.0.0
186             *
187             * @param string $new_wp_version the updated WordPress version
188             */
189            do_action( 'jetpack_sync_core_reinstalled_successfully', $new_wp_version );
190            return;
191        }
192
193        // Core was autoupdated.
194        if (
195            'update-core.php' !== $pagenow &&
196            ! Jetpack_Constants::is_true( 'REST_API_REQUEST' ) // WP.com rest api calls should never be marked as a core autoupdate.
197        ) {
198            /**
199             * Sync event that fires when core autoupdate was successful
200             *
201             * @since 1.6.3
202             * @since-jetpack 5.0.0
203             *
204             * @param string $new_wp_version the updated WordPress version
205             * @param string $old_wp_version the previous WordPress version
206             */
207            do_action( 'jetpack_sync_core_autoupdated_successfully', $new_wp_version, $this->old_wp_version );
208            return;
209        }
210        /**
211         * Sync event that fires when core update was successful
212         *
213         * @since 1.6.3
214         * @since-jetpack 5.0.0
215         *
216         * @param string $new_wp_version the updated WordPress version
217         * @param string $old_wp_version the previous WordPress version
218         */
219        do_action( 'jetpack_sync_core_updated_successfully', $new_wp_version, $this->old_wp_version );
220    }
221
222    /**
223     * Retrieve the checksum for an update.
224     *
225     * @access public
226     *
227     * @param object $update    The update object.
228     * @param string $transient The transient we're retrieving a checksum for.
229     * @return int The checksum.
230     */
231    public function get_update_checksum( $update, $transient ) {
232        $updates    = array();
233        $no_updated = array();
234        switch ( $transient ) {
235            case 'update_plugins':
236                if ( ! empty( $update->response ) && is_array( $update->response ) ) {
237                    foreach ( $update->response as $plugin_slug => $plugin_data ) {
238                        $plugin_data = (array) $plugin_data;
239                        if ( ! empty( $plugin_slug ) && isset( $plugin_data['new_version'] ) ) {
240                            $updates[] = array( $plugin_slug => $plugin_data['new_version'] );
241                        }
242                    }
243                }
244                if ( ! empty( $update->no_update ) ) {
245                    $no_updated = array_keys( $update->no_update );
246                }
247
248                if ( ! isset( $no_updated['jetpack/jetpack.php'] ) && isset( $updates['jetpack/jetpack.php'] ) ) {
249                    return false;
250                }
251
252                break;
253            case 'update_themes':
254                if ( ! empty( $update->response ) && is_array( $update->response ) ) {
255                    foreach ( $update->response as $theme_slug => $theme_data ) {
256                        $theme_data = (array) $theme_data;
257                        if ( ! empty( $theme_slug ) && isset( $theme_data['new_version'] ) ) {
258                            $updates[] = array( $theme_slug => $theme_data['new_version'] );
259                        }
260                    }
261                }
262
263                if ( ! empty( $update->checked ) ) {
264                    $no_updated = $update->checked;
265                }
266
267                break;
268            case 'update_core':
269                if ( ! empty( $update->updates ) && is_array( $update->updates ) ) {
270                    foreach ( $update->updates as $core_update ) {
271                        if ( ! empty( $core_update->response ) && 'latest' === $core_update->response ) {
272                            continue;
273                        }
274                        if ( ! empty( $core_update->response ) && isset( $core_update->packages->full ) ) {
275                            $updates[] = array( $core_update->response => $core_update->packages->full );
276                        }
277                    }
278                }
279
280                if ( ! empty( $update->version_checked ) ) {
281                    $no_updated = $update->version_checked;
282                }
283
284                if ( empty( $updates ) ) {
285                    return false;
286                }
287                break;
288
289        }
290        if ( empty( $updates ) && empty( $no_updated ) ) {
291            return false;
292        }
293        return $this->get_check_sum( array( $no_updated, $updates ) );
294    }
295
296    /**
297     * Validate a change coming from an update before sending for sync.
298     *
299     * @access public
300     *
301     * @param mixed  $value      Site transient value.
302     * @param int    $expiration Time until transient expiration in seconds.
303     * @param string $transient  Transient name.
304     */
305    public function validate_update_change( $value, $expiration, $transient ) {
306        $new_checksum = $this->get_update_checksum( $value, $transient );
307
308        if ( false === $new_checksum ) {
309            return;
310        }
311
312        $checksums = get_option( self::UPDATES_CHECKSUM_OPTION_NAME, array() );
313
314        if ( isset( $checksums[ $transient ] ) && $checksums[ $transient ] === $new_checksum ) {
315            return;
316        }
317
318        $checksums[ $transient ] = $new_checksum;
319
320        update_option( self::UPDATES_CHECKSUM_OPTION_NAME, $checksums );
321        if ( 'update_core' === $transient ) {
322            /**
323             * Trigger a change to core update that we want to sync.
324             *
325             * @since 1.6.3
326             * @since-jetpack 5.1.0
327             *
328             * @param array $value Contains info that tells us what needs updating.
329             */
330            do_action( 'jetpack_update_core_change', $value );
331            return;
332        }
333        if ( empty( $this->updates ) ) {
334            // Lets add the shutdown method once and only when the updates move from empty to filled with something.
335            add_action( 'shutdown', array( $this, 'sync_last_event' ), 9 );
336        }
337        if ( ! isset( $this->updates[ $transient ] ) ) {
338            $this->updates[ $transient ] = array();
339        }
340        $this->updates[ $transient ][] = $value;
341    }
342
343    /**
344     * Sync the last update only.
345     *
346     * @access public
347     */
348    public function sync_last_event() {
349        foreach ( $this->updates as $transient => $values ) {
350            $value = end( $values ); // Only send over the last value.
351            /**
352             * Trigger a change to a specific update that we want to sync.
353             * Triggers one of the following actions:
354             * - jetpack_{$transient}_change
355             * - jetpack_update_plugins_change
356             * - jetpack_update_themes_change
357             *
358             * @since 1.6.3
359             * @since-jetpack 5.1.0
360             *
361             * @param array $value Contains info that tells us what needs updating.
362             */
363            do_action( "jetpack_{$transient}_change", $value );
364        }
365    }
366
367    /**
368     * Enqueue the updates actions for full sync.
369     *
370     * @access public
371     *
372     * @param array   $config               Full sync configuration for this sync module.
373     * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
374     * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
375     * @return array Number of actions enqueued, and next module state.
376     */
377    public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
378        /**
379         * Tells the client to sync all updates to the server
380         *
381         * @since 1.6.3
382         * @since-jetpack 4.2.0
383         *
384         * @param boolean Whether to expand updates (should always be true)
385         */
386        do_action( 'jetpack_full_sync_updates', true );
387
388        // The number of actions enqueued, and next module state (true == done).
389        return array( 1, true );
390    }
391
392    /**
393     * Send the updates actions for full sync.
394     *
395     * @access public
396     *
397     * @param array $config Full sync configuration for this sync module.
398     * @param array $status This module Full Sync status.
399     * @param int   $send_until The timestamp until the current request can send.
400     * @param int   $started The timestamp when the full sync started.
401     *
402     * @return array This module Full Sync status.
403     */
404    public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
405        // we call this instead of do_action when sending immediately.
406        $result = $this->send_action( 'jetpack_full_sync_updates', array( true ) );
407
408        if ( is_wp_error( $result ) ) {
409            $status['error'] = true;
410            return $status;
411        }
412        $status['finished'] = true;
413        $status['sent']     = $status['total'];
414        return $status;
415    }
416
417    /**
418     * Retrieve an estimated number of actions that will be enqueued.
419     *
420     * @access public
421     *
422     * @param array $config Full sync configuration for this sync module.
423     * @return int Number of items yet to be enqueued.
424     */
425    public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
426        return 1;
427    }
428
429    /**
430     * Retrieve the actions that will be sent for this module during a full sync.
431     *
432     * @access public
433     *
434     * @return array Full sync actions of this module.
435     */
436    public function get_full_sync_actions() {
437        return array( 'jetpack_full_sync_updates' );
438    }
439
440    /**
441     * Retrieve all updates that we're interested in.
442     *
443     * @access public
444     *
445     * @return array All updates.
446     */
447    public function get_all_updates() {
448        return array(
449            'core'    => get_site_transient( 'update_core' ),
450            'plugins' => get_site_transient( 'update_plugins' ),
451            'themes'  => get_site_transient( 'update_themes' ),
452        );
453    }
454
455    /**
456     * Remove unnecessary keys from synced updates data.
457     *
458     * @access public
459     *
460     * @param array $args Hook arguments.
461     * @return array $args Hook arguments.
462     */
463    public function filter_update_keys( $args ) {
464        $updates = $args[0];
465
466        if ( isset( $updates->no_update ) ) {
467            unset( $updates->no_update );
468        }
469
470        return $args;
471    }
472
473    /**
474     * Filter out upgrader object from the completed upgrader action args.
475     *
476     * @access public
477     *
478     * @param array $args Hook arguments.
479     * @return array $args Filtered hook arguments.
480     */
481    public function filter_upgrader_process_complete( $args ) {
482        array_shift( $args );
483
484        return $args;
485    }
486
487    /**
488     * Expand the updates within a hook before they are serialized and sent to the server.
489     *
490     * @access public
491     *
492     * @param array $args The hook parameters.
493     * @return array $args The hook parameters.
494     */
495    public function expand_updates( $args ) {
496        if ( $args[0] ) {
497            return $this->get_all_updates();
498        }
499
500        return $args;
501    }
502
503    /**
504     * Expand the themes within a hook before they are serialized and sent to the server.
505     *
506     * @access public
507     *
508     * @param array $args The hook parameters.
509     * @return array $args The hook parameters.
510     */
511    public function expand_themes( $args ) {
512        if ( ! isset( $args[0]->response ) ) {
513            return $args;
514        }
515        if ( ! is_array( $args[0]->response ) ) {
516            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
517            trigger_error( 'Warning: Not an Array as expected but -> ' . wp_json_encode( $args[0]->response, JSON_UNESCAPED_SLASHES ) . ' instead', E_USER_WARNING );
518            return $args;
519        }
520        foreach ( $args[0]->response as $stylesheet => &$theme_data ) {
521            $theme_data = (array) $theme_data;
522            // Make sure the theme data array is not empty and has data that would indicate it is in the correct format.
523            if ( isset( $theme_data['theme'] ) ) {
524                $theme              = wp_get_theme( $stylesheet );
525                $theme_data['name'] = $theme->name;
526            }
527        }
528        return $args;
529    }
530
531    /**
532     * Perform module cleanup.
533     * Deletes any transients and options that this module uses.
534     * Usually triggered when uninstalling the plugin.
535     *
536     * @access public
537     */
538    public function reset_data() {
539        delete_option( self::UPDATES_CHECKSUM_OPTION_NAME );
540    }
541
542    /**
543     * Return Total number of objects.
544     *
545     * @param array $config Full Sync config.
546     *
547     * @return int total
548     */
549    public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
550        return 3;
551    }
552
553    /**
554     * Retrieve a set of updates by their IDs.
555     *
556     * @access public
557     *
558     * @param string $object_type Object type.
559     * @param array  $ids         Object IDs.
560     * @return array Array of objects.
561     */
562    public function get_objects_by_id( $object_type, $ids ) {
563        if ( empty( $ids ) || empty( $object_type ) || 'update' !== $object_type ) {
564            return array();
565        }
566
567        $objects = array();
568        foreach ( (array) $ids as $id ) {
569            $object = $this->get_object_by_id( $object_type, $id );
570
571            if ( 'all' === $id ) {
572                // If all was requested it contains all updates and can simply be returned.
573                return $object;
574            }
575            $objects[ $id ] = $object;
576        }
577
578        return $objects;
579    }
580
581    /**
582     * Retrieve a update by its id.
583     *
584     * @access public
585     *
586     * @param string $object_type Type of the sync object.
587     * @param string $id          ID of the sync object.
588     * @return mixed              Value of Update.
589     */
590    public function get_object_by_id( $object_type, $id ) {
591        if ( 'update' === $object_type ) {
592
593            // Only whitelisted constants can be returned.
594            if ( in_array( $id, array( 'core', 'plugins', 'themes' ), true ) ) {
595                return get_site_transient( 'update_' . $id );
596            } elseif ( 'all' === $id ) {
597                return $this->get_all_updates();
598            }
599        }
600
601        return false;
602    }
603}