Completed
Push — try/full-sync-term-relationshi... ( 8354f3...3b4441 )
by Marin
36:39 queued 28:53
created

Callables::get_always_sent_callables()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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