PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 All 504 releases
jetpack / jetpack_vendor / automattic / jetpack-sync / src / modules / class-callables.php

class-callables.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.1, at jetpack_vendor/automattic/jetpack-sync/src/modules/class-callables.php

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