Completed
Push — add/script-update-package-vers... ( 38150b...f5f944 )
by
unknown
06:18
created

Callables::expand_callables()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
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\Sync\Functions;
11
use Automattic\Jetpack\Sync\Defaults;
12
use Automattic\Jetpack\Sync\Settings;
13
use Automattic\Jetpack\Constants as Jetpack_Constants;
14
15
/**
16
 * Class to handle sync for callables.
17
 */
18
class Callables extends Module {
19
	/**
20
	 * Name of the callables checksum option.
21
	 *
22
	 * @var string
23
	 */
24
	const CALLABLES_CHECKSUM_OPTION_NAME = 'jetpack_callables_sync_checksum';
25
26
	/**
27
	 * Name of the transient for locking callables.
28
	 *
29
	 * @var string
30
	 */
31
	const CALLABLES_AWAIT_TRANSIENT_NAME = 'jetpack_sync_callables_await';
32
33
	/**
34
	 * Whitelist for callables we want to sync.
35
	 *
36
	 * @access private
37
	 *
38
	 * @var array
39
	 */
40
	private $callable_whitelist;
41
42
	/**
43
	 * For some options, we should always send the change right away!
44
	 *
45
	 * @access public
46
	 *
47
	 * @var array
48
	 */
49
	const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS = array(
50
		'jetpack_active_modules',
51
		'home', // option is home, callable is home_url.
52
		'siteurl',
53
		'jetpack_sync_error_idc',
54
		'paused_plugins',
55
		'paused_themes',
56
	);
57
58
	/**
59
	 * For some options, the callable key differs from the option name/key
60
	 *
61
	 * @access public
62
	 *
63
	 * @var array
64
	 */
65
	const OPTION_NAMES_TO_CALLABLE_NAMES = array(
66
		// @TODO: Audit the other option names for differences between the option names and callable names.
67
		'home'    => 'home_url',
68
		'siteurl' => 'site_url',
69
	);
70
71
	/**
72
	 * Sync module name.
73
	 *
74
	 * @access public
75
	 *
76
	 * @return string
77
	 */
78
	public function name() {
79
		return 'functions';
80
	}
81
82
	/**
83
	 * Set module defaults.
84
	 * Define the callable whitelist based on whether this is a single site or a multisite installation.
85
	 *
86
	 * @access public
87
	 */
88
	public function set_defaults() {
89
		if ( is_multisite() ) {
90
			$this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() );
91
		} else {
92
			$this->callable_whitelist = Defaults::get_callable_whitelist();
93
		}
94
	}
95
96
	/**
97
	 * Initialize callables action listeners.
98
	 *
99
	 * @access public
100
	 *
101
	 * @param callable $callable Action handler callable.
102
	 */
103
	public function init_listeners( $callable ) {
104
		add_action( 'jetpack_sync_callable', $callable, 10, 2 );
105
		add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late.
106
107
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) {
108
			add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
109
			add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) );
110
		}
111
112
		// Provide a hook so that hosts can send changes to certain callables right away.
113
		// Especially useful when a host uses constants to change home and siteurl.
114
		add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
115
116
		// get_plugins and wp_version
117
		// gets fired when new code gets installed, updates etc.
118
		add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
119
		add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
120
	}
121
122
	/**
123
	 * Initialize callables 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_callables', $callable );
131
	}
132
133
	/**
134
	 * Initialize the module in the sender.
135
	 *
136
	 * @access public
137
	 */
138
	public function init_before_send() {
139
		add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
140
141
		// Full sync.
142
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
143
	}
144
145
	/**
146
	 * Perform module cleanup.
147
	 * Deletes any transients and options that this module uses.
148
	 * Usually triggered when uninstalling the plugin.
149
	 *
150
	 * @access public
151
	 */
152
	public function reset_data() {
153
		delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
154
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
155
156
		$url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
157
		foreach ( $url_callables as $callable ) {
158
			delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
159
		}
160
	}
161
162
	/**
163
	 * Set the callable whitelist.
164
	 *
165
	 * @access public
166
	 *
167
	 * @param array $callables The new callables whitelist.
168
	 */
169
	public function set_callable_whitelist( $callables ) {
170
		$this->callable_whitelist = $callables;
171
	}
172
173
	/**
174
	 * Get the callable whitelist.
175
	 *
176
	 * @access public
177
	 *
178
	 * @return array The callables whitelist.
179
	 */
180
	public function get_callable_whitelist() {
181
		return $this->callable_whitelist;
182
	}
183
184
	/**
185
	 * Retrieve all callables as per the current callables whitelist.
186
	 *
187
	 * @access public
188
	 *
189
	 * @return array All callables.
190
	 */
191
	public function get_all_callables() {
192
		// get_all_callables should run as the master user always.
193
		$current_user_id = get_current_user_id();
194
		wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
195
		$callables = array_combine(
196
			array_keys( $this->get_callable_whitelist() ),
197
			array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
198
		);
199
		wp_set_current_user( $current_user_id );
200
		return $callables;
201
	}
202
203
	/**
204
	 * Invoke a particular callable.
205
	 * Used as a wrapper to standartize invocation.
206
	 *
207
	 * @access private
208
	 *
209
	 * @param callable $callable Callable to invoke.
210
	 * @return mixed Return value of the callable.
211
	 */
212
	private function get_callable( $callable ) {
213
		return call_user_func( $callable );
214
	}
215
216
	/**
217
	 * Enqueue the callable actions for full sync.
218
	 *
219
	 * @access public
220
	 *
221
	 * @param array   $config               Full sync configuration for this sync module.
222
	 * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
223
	 * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
224
	 * @return array Number of actions enqueued, and next module state.
225
	 */
226
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
227
		/**
228
		 * Tells the client to sync all callables to the server
229
		 *
230
		 * @since 4.2.0
231
		 *
232
		 * @param boolean Whether to expand callables (should always be true)
233
		 */
234
		do_action( 'jetpack_full_sync_callables', true );
235
236
		// The number of actions enqueued, and next module state (true == done).
237
		return array( 1, true );
238
	}
239
240
	/**
241
	 * Retrieve an estimated number of actions that will be enqueued.
242
	 *
243
	 * @access public
244
	 *
245
	 * @param array $config Full sync configuration for this sync module.
246
	 * @return array Number of items yet to be enqueued.
247
	 */
248
	public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
249
		return 1;
250
	}
251
252
	/**
253
	 * Retrieve the actions that will be sent for this module during a full sync.
254
	 *
255
	 * @access public
256
	 *
257
	 * @return array Full sync actions of this module.
258
	 */
259
	public function get_full_sync_actions() {
260
		return array( 'jetpack_full_sync_callables' );
261
	}
262
263
	/**
264
	 * Unlock callables so they would be available for syncing again.
265
	 *
266
	 * @access public
267
	 */
268
	public function unlock_sync_callable() {
269
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
270
	}
271
272
	/**
273
	 * Unlock callables and plugin action links.
274
	 *
275
	 * @access public
276
	 */
277
	public function unlock_plugin_action_link_and_callables() {
278
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
279
		delete_transient( 'jetpack_plugin_api_action_links_refresh' );
280
		add_filter( 'jetpack_check_and_send_callables', '__return_true' );
281
	}
282
283
	/**
284
	 * Parse and store the plugin action links if on the plugins page.
285
	 *
286
	 * @uses \DOMDocument
287
	 * @uses libxml_use_internal_errors
288
	 * @uses mb_convert_encoding
289
	 *
290
	 * @access public
291
	 */
292
	public function set_plugin_action_links() {
293
		if (
294
			! class_exists( '\DOMDocument' ) ||
295
			! function_exists( 'libxml_use_internal_errors' ) ||
296
			! function_exists( 'mb_convert_encoding' )
297
		) {
298
			return;
299
		}
300
301
		$current_screeen = get_current_screen();
302
303
		$plugins_action_links = array();
304
		// Is the transient lock in place?
305
		$plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false );
306
		if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) {
307
			return;
308
		}
309
		$plugins = array_keys( Functions::get_plugins() );
310
		foreach ( $plugins as $plugin_file ) {
311
			/**
312
			 *  Plugins often like to unset things but things break if they are not able to.
313
			 */
314
			$action_links = array(
315
				'deactivate' => '',
316
				'activate'   => '',
317
				'details'    => '',
318
				'delete'     => '',
319
				'edit'       => '',
320
			);
321
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
322
			$action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' );
323
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
324
			$action_links           = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' );
325
			$action_links           = array_filter( $action_links );
326
			$formatted_action_links = null;
327
			if ( ! empty( $action_links ) && count( $action_links ) > 0 ) {
328
				$dom_doc = new \DOMDocument();
329
				foreach ( $action_links as $action_link ) {
330
					// The @ is not enough to suppress errors when dealing with libxml,
331
					// we have to tell it directly how we want to handle errors.
332
					libxml_use_internal_errors( true );
333
					$dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) );
334
					libxml_use_internal_errors( false );
335
336
					$link_elements = $dom_doc->getElementsByTagName( 'a' );
337
					if ( 0 === $link_elements->length ) {
338
						continue;
339
					}
340
341
					$link_element = $link_elements->item( 0 );
342
					// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
343
					if ( $link_element->hasAttribute( 'href' ) && $link_element->nodeValue ) {
0 ignored issues
show
Bug introduced by
The method hasAttribute() does not exist on DOMNode. Did you maybe mean hasAttributes()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
344
						$link_url = trim( $link_element->getAttribute( 'href' ) );
345
346
						// Add the full admin path to the url if the plugin did not provide it.
347
						$link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
348
						if ( empty( $link_url_scheme ) ) {
349
							$link_url = admin_url( $link_url );
350
						}
351
352
						// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
353
						$formatted_action_links[ $link_element->nodeValue ] = $link_url;
354
					}
355
				}
356
			}
357
			if ( $formatted_action_links ) {
358
				$plugins_action_links[ $plugin_file ] = $formatted_action_links;
359
			}
360
		}
361
		// Cache things for a long time.
362
		set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
363
		update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
364
	}
365
366
	/**
367
	 * Whether a certain callable should be sent.
368
	 *
369
	 * @access public
370
	 *
371
	 * @param array  $callable_checksums Callable checksums.
372
	 * @param string $name               Name of the callable.
373
	 * @param string $checksum           A checksum of the callable.
374
	 * @return boolean Whether to send the callable.
375
	 */
376
	public function should_send_callable( $callable_checksums, $name, $checksum ) {
377
		$idc_override_callables = array(
378
			'main_network_site',
379
			'home_url',
380
			'site_url',
381
		);
382
		if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
383
			return true;
384
		}
385
386
		return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
387
	}
388
389
	/**
390
	 * Sync the callables if we're supposed to.
391
	 *
392
	 * @access public
393
	 */
394
	public function maybe_sync_callables() {
395
396
		$callables = $this->get_all_callables();
397
		if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
398
			if ( ! is_admin() ) {
399
				// If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything.
400
				if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) {
401
					return;
402
				}
403
				// 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 ).
404
				$callables = $this->get_always_sent_callables();
405
			}
406
			if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
407
				return;
408
			}
409
		}
410
411
		if ( empty( $callables ) ) {
412
			return;
413
		}
414
415
		set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_callables_wait_time );
416
417
		$callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
418
		$has_changed        = false;
419
		// Only send the callables that have changed.
420 View Code Duplication
		foreach ( $callables as $name => $value ) {
421
			$checksum = $this->get_check_sum( $value );
422
			// Explicitly not using Identical comparison as get_option returns a string.
423
			if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
424
				/**
425
				 * Tells the client to sync a callable (aka function) to the server
426
				 *
427
				 * @since 4.2.0
428
				 *
429
				 * @param string The name of the callable
430
				 * @param mixed The value of the callable
431
				 */
432
				do_action( 'jetpack_sync_callable', $name, $value );
433
				$callable_checksums[ $name ] = $checksum;
434
				$has_changed                 = true;
435
			} else {
436
				$callable_checksums[ $name ] = $checksum;
437
			}
438
		}
439
		if ( $has_changed ) {
440
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
441
		}
442
443
	}
444
445
	/**
446
	 * Get the callables that should always be sent, e.g. on cron.
447
	 *
448
	 * @return array Callables that should always be sent
449
	 */
450
	protected function get_always_sent_callables() {
451
		$callables      = $this->get_all_callables();
452
		$cron_callables = array();
453
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
454
			if ( array_key_exists( $option_name, $callables ) ) {
455
				$cron_callables[ $option_name ] = $callables[ $option_name ];
456
				continue;
457
			}
458
459
			// Check for the Callable name/key for the option, if different from option name.
460
			if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
461
				$callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
462
				if ( array_key_exists( $callable_name, $callables ) ) {
463
					$cron_callables[ $callable_name ] = $callables[ $callable_name ];
464
				}
465
			}
466
		}
467
		return $cron_callables;
468
	}
469
470
	/**
471
	 * Expand the callables within a hook before they are serialized and sent to the server.
472
	 *
473
	 * @access public
474
	 *
475
	 * @param array $args The hook parameters.
476
	 * @return array $args The hook parameters.
477
	 */
478
	public function expand_callables( $args ) {
479
		if ( $args[0] ) {
480
			$callables           = $this->get_all_callables();
481
			$callables_checksums = array();
482
			foreach ( $callables as $name => $value ) {
483
				$callables_checksums[ $name ] = $this->get_check_sum( $value );
484
			}
485
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
486
			return $callables;
487
		}
488
489
		return $args;
490
	}
491
}
492