Completed
Push — fix/block-upgrade-nudge-loadin... ( d043c1...597ec8 )
by Bernhard
06:35
created

Callables::set_callable_whitelist()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
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
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
	);
69
70
	/**
71
	 * Sync module name.
72
	 *
73
	 * @access public
74
	 *
75
	 * @return string
76
	 */
77
	public function name() {
78
		return 'functions';
79
	}
80
81
	/**
82
	 * Set module defaults.
83
	 * Define the callable whitelist based on whether this is a single site or a multisite installation.
84
	 *
85
	 * @access public
86
	 */
87
	public function set_defaults() {
88
		if ( is_multisite() ) {
89
			$this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() );
90
		} else {
91
			$this->callable_whitelist = Defaults::get_callable_whitelist();
92
		}
93
	}
94
95
	/**
96
	 * Initialize callables action listeners.
97
	 *
98
	 * @access public
99
	 *
100
	 * @param callable $callable Action handler callable.
101
	 */
102
	public function init_listeners( $callable ) {
103
		add_action( 'jetpack_sync_callable', $callable, 10, 2 );
104
		add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late.
105
106
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) {
107
			add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
108
			add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) );
109
		}
110
111
		// Provide a hook so that hosts can send changes to certain callables right away.
112
		// Especially useful when a host uses constants to change home and siteurl.
113
		add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
114
115
		// get_plugins and wp_version
116
		// gets fired when new code gets installed, updates etc.
117
		add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
118
		add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
119
	}
120
121
	/**
122
	 * Initialize callables action listeners for full sync.
123
	 *
124
	 * @access public
125
	 *
126
	 * @param callable $callable Action handler callable.
127
	 */
128
	public function init_full_sync_listeners( $callable ) {
129
		add_action( 'jetpack_full_sync_callables', $callable );
130
	}
131
132
	/**
133
	 * Initialize the module in the sender.
134
	 *
135
	 * @access public
136
	 */
137
	public function init_before_send() {
138
		add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
139
140
		// Full sync.
141
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
142
	}
143
144
	/**
145
	 * Perform module cleanup.
146
	 * Deletes any transients and options that this module uses.
147
	 * Usually triggered when uninstalling the plugin.
148
	 *
149
	 * @access public
150
	 */
151
	public function reset_data() {
152
		delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
153
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
154
155
		$url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
156
		foreach ( $url_callables as $callable ) {
157
			delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
158
		}
159
	}
160
161
	/**
162
	 * Set the callable whitelist.
163
	 *
164
	 * @access public
165
	 *
166
	 * @param array $callables The new callables whitelist.
167
	 */
168
	public function set_callable_whitelist( $callables ) {
169
		$this->callable_whitelist = $callables;
170
	}
171
172
	/**
173
	 * Get the callable whitelist.
174
	 *
175
	 * @access public
176
	 *
177
	 * @return array The callables whitelist.
178
	 */
179
	public function get_callable_whitelist() {
180
		return $this->callable_whitelist;
181
	}
182
183
	/**
184
	 * Retrieve all callables as per the current callables whitelist.
185
	 *
186
	 * @access public
187
	 *
188
	 * @return array All callables.
189
	 */
190
	public function get_all_callables() {
191
		// get_all_callables should run as the master user always.
192
		$current_user_id = get_current_user_id();
193
		wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
194
		$callables = array_combine(
195
			array_keys( $this->get_callable_whitelist() ),
196
			array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
197
		);
198
		wp_set_current_user( $current_user_id );
199
		return $callables;
200
	}
201
202
	/**
203
	 * Invoke a particular callable.
204
	 * Used as a wrapper to standartize invocation.
205
	 *
206
	 * @access private
207
	 *
208
	 * @param callable $callable Callable to invoke.
209
	 * @return mixed Return value of the callable.
210
	 */
211
	private function get_callable( $callable ) {
212
		return call_user_func( $callable );
213
	}
214
215
	/**
216
	 * Enqueue the callable actions for full sync.
217
	 *
218
	 * @access public
219
	 *
220
	 * @param array   $config               Full sync configuration for this sync module.
221
	 * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
222
	 * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
223
	 * @return array Number of actions enqueued, and next module state.
224
	 */
225
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
226
		/**
227
		 * Tells the client to sync all callables to the server
228
		 *
229
		 * @since 4.2.0
230
		 *
231
		 * @param boolean Whether to expand callables (should always be true)
232
		 */
233
		do_action( 'jetpack_full_sync_callables', true );
234
235
		// The number of actions enqueued, and next module state (true == done).
236
		return array( 1, true );
237
	}
238
239
	/**
240
	 * Retrieve an estimated number of actions that will be enqueued.
241
	 *
242
	 * @access public
243
	 *
244
	 * @param array $config Full sync configuration for this sync module.
245
	 * @return array Number of items yet to be enqueued.
246
	 */
247
	public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
248
		return 1;
249
	}
250
251
	/**
252
	 * Retrieve the actions that will be sent for this module during a full sync.
253
	 *
254
	 * @access public
255
	 *
256
	 * @return array Full sync actions of this module.
257
	 */
258
	public function get_full_sync_actions() {
259
		return array( 'jetpack_full_sync_callables' );
260
	}
261
262
	/**
263
	 * Unlock callables so they would be available for syncing again.
264
	 *
265
	 * @access public
266
	 */
267
	public function unlock_sync_callable() {
268
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
269
	}
270
271
	/**
272
	 * Unlock callables and plugin action links.
273
	 *
274
	 * @access public
275
	 */
276
	public function unlock_plugin_action_link_and_callables() {
277
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
278
		delete_transient( 'jetpack_plugin_api_action_links_refresh' );
279
		add_filter( 'jetpack_check_and_send_callables', '__return_true' );
280
	}
281
282
	/**
283
	 * Parse and store the plugin action links if on the plugins page.
284
	 *
285
	 * @uses \DOMDocument
286
	 * @uses libxml_use_internal_errors
287
	 * @uses mb_convert_encoding
288
	 *
289
	 * @access public
290
	 */
291
	public function set_plugin_action_links() {
292
		if (
293
			! class_exists( '\DOMDocument' ) ||
294
			! function_exists( 'libxml_use_internal_errors' ) ||
295
			! function_exists( 'mb_convert_encoding' )
296
		) {
297
			return;
298
		}
299
300
		$current_screeen = get_current_screen();
301
302
		$plugins_action_links = array();
303
		// Is the transient lock in place?
304
		$plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false );
305
		if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) {
306
			return;
307
		}
308
		$plugins = array_keys( Functions::get_plugins() );
309
		foreach ( $plugins as $plugin_file ) {
310
			/**
311
			 *  Plugins often like to unset things but things break if they are not able to.
312
			 */
313
			$action_links = array(
314
				'deactivate' => '',
315
				'activate'   => '',
316
				'details'    => '',
317
				'delete'     => '',
318
				'edit'       => '',
319
			);
320
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
321
			$action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' );
322
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
323
			$action_links           = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' );
324
			$action_links           = array_filter( $action_links );
325
			$formatted_action_links = null;
326
			if ( ! empty( $action_links ) && count( $action_links ) > 0 ) {
327
				$dom_doc = new \DOMDocument();
328
				foreach ( $action_links as $action_link ) {
329
					// The @ is not enough to suppress errors when dealing with libxml,
330
					// we have to tell it directly how we want to handle errors.
331
					libxml_use_internal_errors( true );
332
					$dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) );
333
					libxml_use_internal_errors( false );
334
335
					$link_elements = $dom_doc->getElementsByTagName( 'a' );
336
					if ( 0 === $link_elements->length ) {
337
						continue;
338
					}
339
340
					$link_element = $link_elements->item( 0 );
341
					// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
342
					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...
343
						$link_url = trim( $link_element->getAttribute( 'href' ) );
344
345
						// Add the full admin path to the url if the plugin did not provide it.
346
						$link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
347
						if ( empty( $link_url_scheme ) ) {
348
							$link_url = admin_url( $link_url );
349
						}
350
351
						// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
352
						$formatted_action_links[ $link_element->nodeValue ] = $link_url;
353
					}
354
				}
355
			}
356
			if ( $formatted_action_links ) {
357
				$plugins_action_links[ $plugin_file ] = $formatted_action_links;
358
			}
359
		}
360
		// Cache things for a long time.
361
		set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
362
		update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
363
	}
364
365
	/**
366
	 * Whether a certain callable should be sent.
367
	 *
368
	 * @access public
369
	 *
370
	 * @param array  $callable_checksums Callable checksums.
371
	 * @param string $name               Name of the callable.
372
	 * @param string $checksum           A checksum of the callable.
373
	 * @return boolean Whether to send the callable.
374
	 */
375
	public function should_send_callable( $callable_checksums, $name, $checksum ) {
376
		$idc_override_callables = array(
377
			'main_network_site',
378
			'home_url',
379
			'site_url',
380
		);
381
		if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
382
			return true;
383
		}
384
385
		return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
386
	}
387
388
	/**
389
	 * Sync the callables if we're supposed to.
390
	 *
391
	 * @access public
392
	 */
393
	public function maybe_sync_callables() {
394
395
		$callables = $this->get_all_callables();
396
		if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
397
			if ( ! is_admin() ) {
398
				// If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything.
399
				if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) {
400
					return;
401
				}
402
				// 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 ).
403
				$callables = $this->get_always_sent_callables();
404
			}
405
			if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
406
				return;
407
			}
408
		}
409
410
		if ( empty( $callables ) ) {
411
			return;
412
		}
413
414
		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...
415
416
		$callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
417
		$has_changed        = false;
418
		// Only send the callables that have changed.
419 View Code Duplication
		foreach ( $callables as $name => $value ) {
420
			$checksum = $this->get_check_sum( $value );
421
			// Explicitly not using Identical comparison as get_option returns a string.
422
			if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
423
				/**
424
				 * Tells the client to sync a callable (aka function) to the server
425
				 *
426
				 * @since 4.2.0
427
				 *
428
				 * @param string The name of the callable
429
				 * @param mixed The value of the callable
430
				 */
431
				do_action( 'jetpack_sync_callable', $name, $value );
432
				$callable_checksums[ $name ] = $checksum;
433
				$has_changed                 = true;
434
			} else {
435
				$callable_checksums[ $name ] = $checksum;
436
			}
437
		}
438
		if ( $has_changed ) {
439
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
440
		}
441
442
	}
443
444
	/**
445
	 * Get the callables that should always be sent, e.g. on cron.
446
	 *
447
	 * @return array Callables that should always be sent
448
	 */
449
	protected function get_always_sent_callables() {
450
		$callables      = $this->get_all_callables();
451
		$cron_callables = array();
452
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
453
			if ( array_key_exists( $option_name, $callables ) ) {
454
				$cron_callables[ $option_name ] = $callables[ $option_name ];
455
				continue;
456
			}
457
458
			// Check for the Callable name/key for the option, if different from option name.
459
			if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
460
				$callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
461
				if ( array_key_exists( $callable_name, $callables ) ) {
462
					$cron_callables[ $callable_name ] = $callables[ $callable_name ];
463
				}
464
			}
465
		}
466
		return $cron_callables;
467
	}
468
469
	/**
470
	 * Expand the callables within a hook before they are serialized and sent to the server.
471
	 *
472
	 * @access public
473
	 *
474
	 * @param array $args The hook parameters.
475
	 * @return array $args The hook parameters.
476
	 */
477
	public function expand_callables( $args ) {
478
		if ( $args[0] ) {
479
			$callables           = $this->get_all_callables();
480
			$callables_checksums = array();
481
			foreach ( $callables as $name => $value ) {
482
				$callables_checksums[ $name ] = $this->get_check_sum( $value );
483
			}
484
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
485
			return $callables;
486
		}
487
488
		return $args;
489
	}
490
}
491