Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.63% covered (danger)
9.63%
18 / 187
11.54% covered (danger)
11.54%
3 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Callables
9.19% covered (danger)
9.19%
17 / 185
11.54% covered (danger)
11.54%
3 / 26
5368.10
0.00% covered (danger)
0.00%
0 / 1
 name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_defaults
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 set_late_default
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 init_listeners
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 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 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 reset_data
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 set_callable_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_callable_whitelist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_all_callables
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 get_callable
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
 unlock_sync_callable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unlock_sync_callable_next_tick
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unlock_plugin_action_link_and_callables
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 set_plugin_action_links
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
342
 should_send_callable
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_sync_callables
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
272
 get_always_sent_callables
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 expand_callables
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 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 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 get_object_by_id
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Callables sync module.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8namespace Automattic\Jetpack\Sync\Modules;
9
10use Automattic\Jetpack\Constants as Jetpack_Constants;
11use Automattic\Jetpack\Sync\Dedicated_Sender;
12use Automattic\Jetpack\Sync\Defaults;
13use Automattic\Jetpack\Sync\Functions;
14use Automattic\Jetpack\Sync\Settings;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * Class to handle sync for callables.
22 */
23class Callables extends Module {
24    /**
25     * Name of the callables checksum option.
26     *
27     * @var string
28     */
29    const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum';
30
31    /**
32     * Name of the transient for locking callables.
33     *
34     * @var string
35     */
36    const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await';
37
38    /**
39     * Whitelist for callables we want to sync.
40     *
41     * @access private
42     *
43     * @var array
44     */
45    private $callable_whitelist;
46
47    /**
48     * For some options, we should always send the change right away!
49     *
50     * @access public
51     *
52     * @var array
53     */
54    const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS = array(
55        'jetpack_active_modules',
56        'home', // option is home, callable is home_url.
57        'siteurl',
58        'jetpack_sync_error_idc',
59        'paused_plugins',
60        'paused_themes',
61        'blog_public', // Callable is jetpack_site_visibility; privacy has to travel as fast as the option.
62
63    );
64
65    const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK = array(
66        'stylesheet',
67    );
68    /**
69     * Setting this value to true will make it so that the callables will not be unlocked
70     * but the lock will be removed after content is send so that callables will be
71     * sent in the next request.
72     *
73     * @var bool
74     */
75    private $force_send_callables_on_next_tick = false;
76
77    /**
78     * For some options, the callable key differs from the option name/key
79     *
80     * @access public
81     *
82     * @var array
83     */
84    const OPTION_NAMES_TO_CALLABLE_NAMES = array(
85        // @TODO: Audit the other option names for differences between the option names and callable names.
86        'home'                   => 'home_url',
87        'siteurl'                => 'site_url',
88        'jetpack_active_modules' => 'active_modules',
89        'blog_public'            => 'jetpack_site_visibility',
90    );
91
92    /**
93     * Sync module name.
94     *
95     * @access public
96     *
97     * @return string
98     */
99    public function name() {
100        return 'functions';
101    }
102
103    /**
104     * Set module defaults.
105     * Define the callable whitelist based on whether this is a single site or a multisite installation.
106     *
107     * @access public
108     */
109    public function set_defaults() {
110        if ( is_multisite() ) {
111            $this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() );
112        } else {
113            $this->callable_whitelist = Defaults::get_callable_whitelist();
114        }
115        $this->force_send_callables_on_next_tick = false; // Resets here as well mostly for tests.
116    }
117
118    /**
119     * Set module defaults at a later time.
120     * Reset the callable whitelist if needed to account for plugins adding the 'jetpack_sync_callable_whitelist'
121     * and 'jetpack_sync_multisite_callable_whitelist' filters late.
122     *
123     * @see Automattic\Jetpack\Sync\Modules::set_module_defaults
124     * @access public
125     */
126    public function set_late_default() {
127        if ( is_multisite() ) {
128            $late_callables = array_merge(
129                apply_filters( 'jetpack_sync_callable_whitelist', array() ),
130                apply_filters( 'jetpack_sync_multisite_callable_whitelist', array() )
131            );
132        } else {
133            $late_callables = apply_filters( 'jetpack_sync_callable_whitelist', array() );
134        }
135        if ( ! empty( $late_callables ) && is_array( $late_callables ) ) {
136            $this->callable_whitelist = array_merge( $this->callable_whitelist, $late_callables );
137        }
138    }
139
140    /**
141     * Initialize callables action listeners.
142     *
143     * @access public
144     *
145     * @param callable $callable Action handler callable.
146     */
147    public function init_listeners( $callable ) {
148        add_action( 'jetpack_sync_callable', $callable, 10, 2 );
149        add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late.
150
151        foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) {
152            add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
153            add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) );
154        }
155
156        foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK as $option ) {
157            add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
158            add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
159        }
160
161        // Provide a hook so that hosts can send changes to certain callables right away.
162        // Especially useful when a host uses constants to change home and siteurl.
163        add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
164
165        // get_plugins and wp_version
166        // gets fired when new code gets installed, updates etc.
167        add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
168        add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
169        // Deleting an inactive plugin fires neither of the above, so without this the synced
170        // get_plugins callable stays stale until the periodic re-check happens to run.
171        add_action( 'deleted_plugin', array( $this, 'unlock_plugin_action_link_and_callables' ) );
172    }
173
174    /**
175     * Initialize callables action listeners for full sync.
176     *
177     * @access public
178     *
179     * @param callable $callable Action handler callable.
180     */
181    public function init_full_sync_listeners( $callable ) {
182        add_action( 'jetpack_full_sync_callables', $callable );
183    }
184
185    /**
186     * Initialize the module in the sender.
187     *
188     * @access public
189     */
190    public function init_before_send() {
191        add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
192
193        // Full sync.
194        add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
195    }
196
197    /**
198     * Perform module cleanup.
199     * Deletes any transients and options that this module uses.
200     * Usually triggered when uninstalling the plugin.
201     *
202     * @access public
203     */
204    public function reset_data() {
205        delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
206        delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
207
208        $url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
209        foreach ( $url_callables as $callable ) {
210            delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
211        }
212    }
213
214    /**
215     * Set the callable whitelist.
216     *
217     * @access public
218     *
219     * @param array $callables The new callables whitelist.
220     */
221    public function set_callable_whitelist( $callables ) {
222        $this->callable_whitelist = $callables;
223    }
224
225    /**
226     * Get the callable whitelist.
227     *
228     * @access public
229     *
230     * @return array The callables whitelist.
231     */
232    public function get_callable_whitelist() {
233        return $this->callable_whitelist;
234    }
235
236    /**
237     * Retrieve all callables as per the current callables whitelist.
238     *
239     * @access public
240     *
241     * @return array All callables.
242     */
243    public function get_all_callables() {
244        // get_all_callables should run as the master user always.
245        $current_user_id = get_current_user_id();
246        wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
247        $callables = array_combine(
248            array_keys( $this->get_callable_whitelist() ),
249            array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
250        );
251        wp_set_current_user( $current_user_id );
252        return $callables;
253    }
254
255    /**
256     * Invoke a particular callable.
257     * Used as a wrapper to standartize invocation.
258     *
259     * @access private
260     *
261     * @param callable $callable Callable to invoke.
262     * @return mixed Return value of the callable, null if not callable.
263     */
264    private function get_callable( $callable ) {
265        if ( is_callable( $callable ) ) {
266            return call_user_func( $callable );
267        } else {
268            return null;
269        }
270    }
271
272    /**
273     * Enqueue the callable actions for full sync.
274     *
275     * @access public
276     *
277     * @param array   $config               Full sync configuration for this sync module.
278     * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
279     * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
280     * @return array Number of actions enqueued, and next module state.
281     */
282    public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
283        /**
284         * Tells the client to sync all callables to the server
285         *
286         * @since 1.6.3
287         * @since-jetpack 4.2.0
288         *
289         * @param boolean Whether to expand callables (should always be true)
290         */
291        do_action( 'jetpack_full_sync_callables', true );
292
293        // The number of actions enqueued, and next module state (true == done).
294        return array( 1, true );
295    }
296
297    /**
298     * Send the callable actions for full sync.
299     *
300     * @access public
301     *
302     * @param array $config Full sync configuration for this sync module.
303     * @param array $status This Module Full Sync Status.
304     * @param int   $send_until The timestamp until the current request can send.
305     * @param int   $started The timestamp when the full sync started.
306     *
307     * @return array This Module Full Sync Status.
308     */
309    public function send_full_sync_actions( $config, $status, $send_until, $started ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
310        // we call this instead of do_action when sending immediately.
311        $result = $this->send_action( 'jetpack_full_sync_callables', array( true ) );
312
313        if ( is_wp_error( $result ) ) {
314            $status['error'] = true;
315            return $status;
316        }
317        $status['finished'] = true;
318        $status['sent']     = $status['total'];
319        return $status;
320    }
321
322    /**
323     * Retrieve an estimated number of actions that will be enqueued.
324     *
325     * @access public
326     *
327     * @param array $config Full sync configuration for this sync module.
328     * @return int Number of items yet to be enqueued.
329     */
330    public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
331        return 1;
332    }
333
334    /**
335     * Retrieve the actions that will be sent for this module during a full sync.
336     *
337     * @access public
338     *
339     * @return array Full sync actions of this module.
340     */
341    public function get_full_sync_actions() {
342        return array( 'jetpack_full_sync_callables' );
343    }
344
345    /**
346     * Unlock callables so they would be available for syncing again.
347     *
348     * @access public
349     */
350    public function unlock_sync_callable() {
351        delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
352    }
353
354    /**
355     * Unlock callables on the next tick.
356     * Sometime the true callable values are only present on the next tick.
357     * When switching themes for example.
358     *
359     * @access public
360     */
361    public function unlock_sync_callable_next_tick() {
362        $this->force_send_callables_on_next_tick = true;
363    }
364
365    /**
366     * Unlock callables and plugin action links.
367     *
368     * @access public
369     */
370    public function unlock_plugin_action_link_and_callables() {
371        delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
372        delete_transient( 'jetpack_plugin_api_action_links_refresh' );
373        add_filter( 'jetpack_check_and_send_callables', '__return_true' );
374    }
375
376    /**
377     * Parse and store the plugin action links if on the plugins page.
378     *
379     * @uses \DOMDocument
380     * @uses libxml_use_internal_errors
381     * @uses mb_convert_encoding
382     *
383     * @access public
384     */
385    public function set_plugin_action_links() {
386        if (
387            ! class_exists( '\DOMDocument' ) ||
388            ! function_exists( 'mb_convert_encoding' )
389        ) {
390            return;
391        }
392
393        $current_screeen = get_current_screen();
394
395        $plugins_action_links = array();
396        // Is the transient lock in place?
397        $plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh' );
398        if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) {
399            return;
400        }
401        $plugins = Functions::get_plugins();
402        if ( ! is_array( $plugins ) ) {
403            return;
404        }
405        foreach ( $plugins as $plugin_file => $plugin_data ) {
406            /**
407             *  Plugins often like to unset things but things break if they are not able to.
408             */
409            $action_links = array(
410                'deactivate' => '',
411                'activate'   => '',
412                'details'    => '',
413                'delete'     => '',
414                'edit'       => '',
415            );
416            /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
417            $action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, $plugin_data, 'all' );
418            // Verify $action_links is still an array.
419            if ( ! is_array( $action_links ) ) {
420                $action_links = array();
421            }
422            /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
423            $action_links = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, $plugin_data, 'all' );
424            // Verify $action_links is still an array to resolve warnings from filters not returning an array.
425            if ( is_array( $action_links ) ) {
426                $action_links = array_filter( $action_links );
427            } else {
428                $action_links = array();
429            }
430            $formatted_action_links = null;
431            if ( $action_links ) {
432                $dom_doc = new \DOMDocument();
433                foreach ( $action_links as $action_link ) {
434                    // The @ is not enough to suppress errors when dealing with libxml,
435                    // we have to tell it directly how we want to handle errors.
436                    libxml_use_internal_errors( true );
437                    $dom_doc->loadHTML( '<?xml encoding="utf-8" ?>' . $action_link );
438                    libxml_use_internal_errors( false );
439
440                    $link_elements = $dom_doc->getElementsByTagName( 'a' );
441                    if ( 0 === $link_elements->length ) {
442                        continue;
443                    }
444
445                    $link_element = $link_elements->item( 0 );
446                    // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
447                    if ( $link_element instanceof \DOMElement && $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) {
448                        $link_url = trim( $link_element->getAttribute( 'href' ) );
449
450                        // Add the full admin path to the url if the plugin did not provide it.
451                        $link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
452                        if ( empty( $link_url_scheme ) ) {
453                            $link_url = admin_url( $link_url );
454                        }
455
456                        // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
457                        $formatted_action_links[ $link_element->nodeValue ] = $link_url;
458                    }
459                }
460            }
461            if ( $formatted_action_links ) {
462                $plugins_action_links[ $plugin_file ] = $formatted_action_links;
463            }
464        }
465        // Cache things for a long time.
466        set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
467        update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
468    }
469
470    /**
471     * Whether a certain callable should be sent.
472     *
473     * @access public
474     *
475     * @param array  $callable_checksums Callable checksums.
476     * @param string $name               Name of the callable.
477     * @param string $checksum           A checksum of the callable.
478     * @return boolean Whether to send the callable.
479     */
480    public function should_send_callable( $callable_checksums, $name, $checksum ) {
481        $idc_override_callables = array(
482            'main_network_site',
483            'home_url',
484            'site_url',
485        );
486        if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
487            return true;
488        }
489
490        return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
491    }
492
493    /**
494     * Sync the callables if we're supposed to.
495     *
496     * @access public
497     */
498    public function maybe_sync_callables() {
499        $callables = $this->get_all_callables();
500        if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
501            /**
502             * Treating Dedicated Sync requests a bit differently from normal. We want to send callables
503             * normally with all Sync actions, no matter if they were with admin action origin or not,
504             * since Dedicated Sync runs out of bound and the requests are never coming from an admin.
505             */
506            if ( ! is_admin() && ! Dedicated_Sender::is_dedicated_sync_request() ) {
507                // If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything.
508                if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) {
509                    return;
510                }
511                // If we're not an admin and we are doing cron, sync the Callables that are always supposed to sync ( See https://github.com/Automattic/jetpack/issues/12924 ).
512                $callables = $this->get_always_sent_callables();
513            }
514            if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
515                if ( $this->force_send_callables_on_next_tick ) {
516                    $this->unlock_sync_callable();
517                }
518                return;
519            }
520        }
521
522        if ( empty( $callables ) ) {
523            return;
524        }
525        // No need to set the transiant we are trying to remove it anyways.
526        if ( ! $this->force_send_callables_on_next_tick ) {
527            set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_callables_wait_time );
528        }
529
530        $callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
531        $has_changed        = false;
532        // Only send the callables that have changed.
533        foreach ( $callables as $name => $value ) {
534            $checksum = $this->get_check_sum( $value );
535
536            // Explicitly not using Identical comparison as get_option returns a string.
537            if ( $value !== null && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
538
539                // Only send callable if the non sorted checksum also does not match.
540                if ( $this->should_send_callable( $callable_checksums, $name, $this->get_check_sum( $value, false ) ) ) {
541
542                    /**
543                     * Tells the client to sync a callable (aka function) to the server
544                     *
545                     * @param string The name of the callable
546                     * @param mixed The value of the callable
547                     *
548                     * @since 1.6.3
549                     * @since-jetpack 4.2.0
550                     */
551                    do_action( 'jetpack_sync_callable', $name, $value );
552                }
553
554                $callable_checksums[ $name ] = $checksum;
555                $has_changed                 = true;
556            } else {
557                $callable_checksums[ $name ] = $checksum;
558            }
559        }
560        if ( $has_changed ) {
561            \Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
562        }
563
564        if ( $this->force_send_callables_on_next_tick ) {
565            $this->unlock_sync_callable();
566        }
567    }
568
569    /**
570     * Get the callables that should always be sent, e.g. on cron.
571     *
572     * @return array Callables that should always be sent
573     */
574    protected function get_always_sent_callables() {
575        $callables      = $this->get_all_callables();
576        $cron_callables = array();
577        foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
578            if ( array_key_exists( $option_name, $callables ) ) {
579                $cron_callables[ $option_name ] = $callables[ $option_name ];
580                continue;
581            }
582
583            // Check for the Callable name/key for the option, if different from option name.
584            if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
585                $callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
586                if ( array_key_exists( $callable_name, $callables ) ) {
587                    $cron_callables[ $callable_name ] = $callables[ $callable_name ];
588                }
589            }
590        }
591        return $cron_callables;
592    }
593
594    /**
595     * Expand the callables within a hook before they are serialized and sent to the server.
596     *
597     * @access public
598     *
599     * @param array $args The hook parameters.
600     * @return array $args The hook parameters.
601     */
602    public function expand_callables( $args ) {
603        if ( $args[0] ) {
604            $callables           = $this->get_all_callables();
605            $callables_checksums = array();
606            foreach ( $callables as $name => $value ) {
607                $callables_checksums[ $name ] = $this->get_check_sum( $value );
608            }
609            \Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
610            return $callables;
611        }
612
613        return $args;
614    }
615
616    /**
617     * Return Total number of objects.
618     *
619     * @param array $config Full Sync config.
620     *
621     * @return int total
622     */
623    public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
624        return count( $this->get_callable_whitelist() );
625    }
626
627    /**
628     * Retrieve a set of callables by their IDs.
629     *
630     * @access public
631     *
632     * @param string $object_type Object type.
633     * @param array  $ids         Object IDs.
634     * @return array Array of objects.
635     */
636    public function get_objects_by_id( $object_type, $ids ) {
637        if ( empty( $ids ) || empty( $object_type ) || 'callable' !== $object_type ) {
638            return array();
639        }
640
641        $objects = array();
642        foreach ( (array) $ids as $id ) {
643            $object = $this->get_object_by_id( $object_type, $id );
644
645            if ( 'CALLABLE-DOES-NOT-EXIST' !== $object ) {
646                if ( 'all' === $id ) {
647                    // If all was requested it contains all options and can simply be returned.
648                    return $object;
649                }
650                $objects[ $id ] = $object;
651            }
652        }
653
654        return $objects;
655    }
656
657    /**
658     * Retrieve a callable by its name.
659     *
660     * @access public
661     *
662     * @param string $object_type Type of the sync object.
663     * @param string $id          ID of the sync object.
664     * @return mixed              Value of Callable.
665     */
666    public function get_object_by_id( $object_type, $id ) {
667        if ( 'callable' === $object_type ) {
668
669            // Only whitelisted options can be returned.
670            if ( array_key_exists( $id, $this->get_callable_whitelist() ) ) {
671                // requires master user to be in context.
672                $current_user_id = get_current_user_id();
673                wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
674                $callable = $this->get_callable( $this->callable_whitelist[ $id ] );
675                wp_set_current_user( $current_user_id );
676                return $callable;
677            } elseif ( 'all' === $id ) {
678                return $this->get_all_callables();
679            }
680        }
681
682        return 'CALLABLE-DOES-NOT-EXIST';
683    }
684}