Completed
Push — update/sync-histogram-term-tax... ( 6843f3...53f83e )
by
unknown
06:43
created

Callables::init_before_send()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
414
415
		$callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
416
		$has_changed        = false;
417
		// Only send the callables that have changed.
418 View Code Duplication
		foreach ( $callables as $name => $value ) {
419
			$checksum = $this->get_check_sum( $value );
420
			// Explicitly not using Identical comparison as get_option returns a string.
421
			if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
422
				/**
423
				 * Tells the client to sync a callable (aka function) to the server
424
				 *
425
				 * @since 4.2.0
426
				 *
427
				 * @param string The name of the callable
428
				 * @param mixed The value of the callable
429
				 */
430
				do_action( 'jetpack_sync_callable', $name, $value );
431
				$callable_checksums[ $name ] = $checksum;
432
				$has_changed                 = true;
433
			} else {
434
				$callable_checksums[ $name ] = $checksum;
435
			}
436
		}
437
		if ( $has_changed ) {
438
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
439
		}
440
441
	}
442
443
	/**
444
	 * Get the callables that should always be sent, e.g. on cron.
445
	 *
446
	 * @return array Callables that should always be sent
447
	 */
448
	protected function get_always_sent_callables() {
449
		$callables      = $this->get_all_callables();
450
		$cron_callables = array();
451
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
452
			if ( array_key_exists( $option_name, $callables ) ) {
453
				$cron_callables[ $option_name ] = $callables[ $option_name ];
454
				continue;
455
			}
456
457
			// Check for the Callable name/key for the option, if different from option name.
458
			if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
459
				$callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
460
				if ( array_key_exists( $callable_name, $callables ) ) {
461
					$cron_callables[ $callable_name ] = $callables[ $callable_name ];
462
				}
463
			}
464
		}
465
		return $cron_callables;
466
	}
467
468
	/**
469
	 * Expand the callables within a hook before they are serialized and sent to the server.
470
	 *
471
	 * @access public
472
	 *
473
	 * @param array $args The hook parameters.
474
	 * @return array $args The hook parameters.
475
	 */
476
	public function expand_callables( $args ) {
477
		if ( $args[0] ) {
478
			$callables           = $this->get_all_callables();
479
			$callables_checksums = array();
480
			foreach ( $callables as $name => $value ) {
481
				$callables_checksums[ $name ] = $this->get_check_sum( $value );
482
			}
483
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
484
			return $callables;
485
		}
486
487
		return $args;
488
	}
489
}
490