PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 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 All 506 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.7, at jetpack_vendor/automattic/jetpack-sync/src/modules/class-callables.php

685 lines 21.2 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 '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 }
685