Completed
Push — add/e2e-connection-purchase-fl... ( d02cce...68beb5 )
by Yaroslav
14:07 queued 04:35
created

Callables::set_plugin_action_links()   C

Complexity

Conditions 16
Paths 7

Size

Total Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
nc 7
nop 0
dl 0
loc 73
rs 5.5666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	const ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK = array(
60
		'stylesheet',
61
	);
62
	/**
63
	 * Setting this value to true will make it so that the callables will not be unlocked
64
	 * but the lock will be removed after content is send so that callables will be
65
	 * sent in the next request.
66
	 *
67
	 * @var bool
68
	 */
69
	private $force_send_callables_on_next_tick = false;
70
71
	/**
72
	 * For some options, the callable key differs from the option name/key
73
	 *
74
	 * @access public
75
	 *
76
	 * @var array
77
	 */
78
	const OPTION_NAMES_TO_CALLABLE_NAMES = array(
79
		// @TODO: Audit the other option names for differences between the option names and callable names.
80
		'home'    => 'home_url',
81
		'siteurl' => 'site_url',
82
	);
83
84
	/**
85
	 * Sync module name.
86
	 *
87
	 * @access public
88
	 *
89
	 * @return string
90
	 */
91
	public function name() {
92
		return 'functions';
93
	}
94
95
	/**
96
	 * Set module defaults.
97
	 * Define the callable whitelist based on whether this is a single site or a multisite installation.
98
	 *
99
	 * @access public
100
	 */
101
	public function set_defaults() {
102
		if ( is_multisite() ) {
103
			$this->callable_whitelist = array_merge( Defaults::get_callable_whitelist(), Defaults::get_multisite_callable_whitelist() );
104
		} else {
105
			$this->callable_whitelist = Defaults::get_callable_whitelist();
106
		}
107
		$this->force_send_callables_on_next_tick = false; // Resets here as well mostly for tests.
108
	}
109
110
	/**
111
	 * Initialize callables action listeners.
112
	 *
113
	 * @access public
114
	 *
115
	 * @param callable $callable Action handler callable.
116
	 */
117
	public function init_listeners( $callable ) {
118
		add_action( 'jetpack_sync_callable', $callable, 10, 2 );
119
		add_action( 'current_screen', array( $this, 'set_plugin_action_links' ), 9999 ); // Should happen very late.
120
121 View Code Duplication
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option ) {
122
			add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable' ) );
123
			add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable' ) );
124
		}
125
126 View Code Duplication
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS_NEXT_TICK as $option ) {
127
			add_action( "update_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
128
			add_action( "delete_option_{$option}", array( $this, 'unlock_sync_callable_next_tick' ) );
129
		}
130
131
		// Provide a hook so that hosts can send changes to certain callables right away.
132
		// Especially useful when a host uses constants to change home and siteurl.
133
		add_action( 'jetpack_sync_unlock_sync_callable', array( $this, 'unlock_sync_callable' ) );
134
135
		// get_plugins and wp_version
136
		// gets fired when new code gets installed, updates etc.
137
		add_action( 'upgrader_process_complete', array( $this, 'unlock_plugin_action_link_and_callables' ) );
138
		add_action( 'update_option_active_plugins', array( $this, 'unlock_plugin_action_link_and_callables' ) );
139
	}
140
141
	/**
142
	 * Initialize callables action listeners for full sync.
143
	 *
144
	 * @access public
145
	 *
146
	 * @param callable $callable Action handler callable.
147
	 */
148
	public function init_full_sync_listeners( $callable ) {
149
		add_action( 'jetpack_full_sync_callables', $callable );
150
	}
151
152
	/**
153
	 * Initialize the module in the sender.
154
	 *
155
	 * @access public
156
	 */
157
	public function init_before_send() {
158
		add_action( 'jetpack_sync_before_send_queue_sync', array( $this, 'maybe_sync_callables' ) );
159
160
		// Full sync.
161
		add_filter( 'jetpack_sync_before_send_jetpack_full_sync_callables', array( $this, 'expand_callables' ) );
162
	}
163
164
	/**
165
	 * Perform module cleanup.
166
	 * Deletes any transients and options that this module uses.
167
	 * Usually triggered when uninstalling the plugin.
168
	 *
169
	 * @access public
170
	 */
171
	public function reset_data() {
172
		delete_option( self::CALLABLES_CHECKSUM_OPTION_NAME );
173
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
174
175
		$url_callables = array( 'home_url', 'site_url', 'main_network_site_url' );
176
		foreach ( $url_callables as $callable ) {
177
			delete_option( Functions::HTTPS_CHECK_OPTION_PREFIX . $callable );
178
		}
179
	}
180
181
	/**
182
	 * Set the callable whitelist.
183
	 *
184
	 * @access public
185
	 *
186
	 * @param array $callables The new callables whitelist.
187
	 */
188
	public function set_callable_whitelist( $callables ) {
189
		$this->callable_whitelist = $callables;
190
	}
191
192
	/**
193
	 * Get the callable whitelist.
194
	 *
195
	 * @access public
196
	 *
197
	 * @return array The callables whitelist.
198
	 */
199
	public function get_callable_whitelist() {
200
		return $this->callable_whitelist;
201
	}
202
203
	/**
204
	 * Retrieve all callables as per the current callables whitelist.
205
	 *
206
	 * @access public
207
	 *
208
	 * @return array All callables.
209
	 */
210
	public function get_all_callables() {
211
		// get_all_callables should run as the master user always.
212
		$current_user_id = get_current_user_id();
213
		wp_set_current_user( \Jetpack_Options::get_option( 'master_user' ) );
214
		$callables = array_combine(
215
			array_keys( $this->get_callable_whitelist() ),
216
			array_map( array( $this, 'get_callable' ), array_values( $this->get_callable_whitelist() ) )
217
		);
218
		wp_set_current_user( $current_user_id );
219
		return $callables;
220
	}
221
222
	/**
223
	 * Invoke a particular callable.
224
	 * Used as a wrapper to standartize invocation.
225
	 *
226
	 * @access private
227
	 *
228
	 * @param callable $callable Callable to invoke.
229
	 * @return mixed Return value of the callable.
230
	 */
231
	private function get_callable( $callable ) {
232
		return call_user_func( $callable );
233
	}
234
235
	/**
236
	 * Enqueue the callable actions for full sync.
237
	 *
238
	 * @access public
239
	 *
240
	 * @param array   $config               Full sync configuration for this sync module.
241
	 * @param int     $max_items_to_enqueue Maximum number of items to enqueue.
242
	 * @param boolean $state                True if full sync has finished enqueueing this module, false otherwise.
243
	 * @return array Number of actions enqueued, and next module state.
244
	 */
245
	public function enqueue_full_sync_actions( $config, $max_items_to_enqueue, $state ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
246
		/**
247
		 * Tells the client to sync all callables to the server
248
		 *
249
		 * @since 4.2.0
250
		 *
251
		 * @param boolean Whether to expand callables (should always be true)
252
		 */
253
		do_action( 'jetpack_full_sync_callables', true );
254
255
		// The number of actions enqueued, and next module state (true == done).
256
		return array( 1, true );
257
	}
258
259
	/**
260
	 * Send the callable actions for full sync.
261
	 *
262
	 * @access public
263
	 *
264
	 * @param array $config Full sync configuration for this sync module.
265
	 * @param int   $send_until The timestamp until the current request can send.
266
	 * @param array $status This Module Full Sync Status.
267
	 *
268
	 * @return array This Module Full Sync Status.
269
	 */
270
	public function send_full_sync_actions( $config, $send_until, $status ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
271
		// we call this instead of do_action when sending immediately.
272
		$this->send_action( 'jetpack_full_sync_callables', array( true ) );
273
274
		// The number of actions enqueued, and next module state (true == done).
275
		return array( 'finished' => true );
276
	}
277
278
	/**
279
	 * Retrieve an estimated number of actions that will be enqueued.
280
	 *
281
	 * @access public
282
	 *
283
	 * @param array $config Full sync configuration for this sync module.
284
	 * @return array Number of items yet to be enqueued.
285
	 */
286
	public function estimate_full_sync_actions( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
287
		return 1;
288
	}
289
290
	/**
291
	 * Retrieve the actions that will be sent for this module during a full sync.
292
	 *
293
	 * @access public
294
	 *
295
	 * @return array Full sync actions of this module.
296
	 */
297
	public function get_full_sync_actions() {
298
		return array( 'jetpack_full_sync_callables' );
299
	}
300
301
	/**
302
	 * Unlock callables so they would be available for syncing again.
303
	 *
304
	 * @access public
305
	 */
306
	public function unlock_sync_callable() {
307
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
308
	}
309
310
	/**
311
	 * Unlock callables on the next tick.
312
	 * Sometime the true callable values are only present on the next tick.
313
	 * When switching themes for example.
314
	 *
315
	 * @access public
316
	 */
317
	public function unlock_sync_callable_next_tick() {
318
		$this->force_send_callables_on_next_tick = true;
319
	}
320
321
	/**
322
	 * Unlock callables and plugin action links.
323
	 *
324
	 * @access public
325
	 */
326
	public function unlock_plugin_action_link_and_callables() {
327
		delete_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME );
328
		delete_transient( 'jetpack_plugin_api_action_links_refresh' );
329
		add_filter( 'jetpack_check_and_send_callables', '__return_true' );
330
	}
331
332
	/**
333
	 * Parse and store the plugin action links if on the plugins page.
334
	 *
335
	 * @uses \DOMDocument
336
	 * @uses libxml_use_internal_errors
337
	 * @uses mb_convert_encoding
338
	 *
339
	 * @access public
340
	 */
341
	public function set_plugin_action_links() {
342
		if (
343
			! class_exists( '\DOMDocument' ) ||
344
			! function_exists( 'libxml_use_internal_errors' ) ||
345
			! function_exists( 'mb_convert_encoding' )
346
		) {
347
			return;
348
		}
349
350
		$current_screeen = get_current_screen();
351
352
		$plugins_action_links = array();
353
		// Is the transient lock in place?
354
		$plugins_lock = get_transient( 'jetpack_plugin_api_action_links_refresh', false );
355
		if ( ! empty( $plugins_lock ) && ( isset( $current_screeen->id ) && 'plugins' !== $current_screeen->id ) ) {
356
			return;
357
		}
358
		$plugins = array_keys( Functions::get_plugins() );
359
		foreach ( $plugins as $plugin_file ) {
360
			/**
361
			 *  Plugins often like to unset things but things break if they are not able to.
362
			 */
363
			$action_links = array(
364
				'deactivate' => '',
365
				'activate'   => '',
366
				'details'    => '',
367
				'delete'     => '',
368
				'edit'       => '',
369
			);
370
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
371
			$action_links = apply_filters( 'plugin_action_links', $action_links, $plugin_file, null, 'all' );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $plugin_file.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
372
			/** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
373
			$action_links           = apply_filters( "plugin_action_links_{$plugin_file}", $action_links, $plugin_file, null, 'all' );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $plugin_file.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
374
			$action_links           = array_filter( $action_links );
375
			$formatted_action_links = null;
376
			if ( ! empty( $action_links ) && count( $action_links ) > 0 ) {
377
				$dom_doc = new \DOMDocument();
378
				foreach ( $action_links as $action_link ) {
379
					// The @ is not enough to suppress errors when dealing with libxml,
380
					// we have to tell it directly how we want to handle errors.
381
					libxml_use_internal_errors( true );
382
					$dom_doc->loadHTML( mb_convert_encoding( $action_link, 'HTML-ENTITIES', 'UTF-8' ) );
383
					libxml_use_internal_errors( false );
384
385
					$link_elements = $dom_doc->getElementsByTagName( 'a' );
386
					if ( 0 === $link_elements->length ) {
387
						continue;
388
					}
389
390
					$link_element = $link_elements->item( 0 );
391
					// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
392
					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...
393
						$link_url = trim( $link_element->getAttribute( 'href' ) );
394
395
						// Add the full admin path to the url if the plugin did not provide it.
396
						$link_url_scheme = wp_parse_url( $link_url, PHP_URL_SCHEME );
0 ignored issues
show
Unused Code introduced by
The call to wp_parse_url() has too many arguments starting with PHP_URL_SCHEME.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
397
						if ( empty( $link_url_scheme ) ) {
398
							$link_url = admin_url( $link_url );
399
						}
400
401
						// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
402
						$formatted_action_links[ $link_element->nodeValue ] = $link_url;
403
					}
404
				}
405
			}
406
			if ( $formatted_action_links ) {
407
				$plugins_action_links[ $plugin_file ] = $formatted_action_links;
408
			}
409
		}
410
		// Cache things for a long time.
411
		set_transient( 'jetpack_plugin_api_action_links_refresh', time(), DAY_IN_SECONDS );
412
		update_option( 'jetpack_plugin_api_action_links', $plugins_action_links );
413
	}
414
415
	/**
416
	 * Whether a certain callable should be sent.
417
	 *
418
	 * @access public
419
	 *
420
	 * @param array  $callable_checksums Callable checksums.
421
	 * @param string $name               Name of the callable.
422
	 * @param string $checksum           A checksum of the callable.
423
	 * @return boolean Whether to send the callable.
424
	 */
425
	public function should_send_callable( $callable_checksums, $name, $checksum ) {
426
		$idc_override_callables = array(
427
			'main_network_site',
428
			'home_url',
429
			'site_url',
430
		);
431
		if ( in_array( $name, $idc_override_callables, true ) && \Jetpack_Options::get_option( 'migrate_for_idc' ) ) {
432
			return true;
433
		}
434
435
		return ! $this->still_valid_checksum( $callable_checksums, $name, $checksum );
436
	}
437
438
	/**
439
	 * Sync the callables if we're supposed to.
440
	 *
441
	 * @access public
442
	 */
443
	public function maybe_sync_callables() {
444
		$callables = $this->get_all_callables();
445
		if ( ! apply_filters( 'jetpack_check_and_send_callables', false ) ) {
446
			if ( ! is_admin() ) {
447
				// If we're not an admin and we're not doing cron and this isn't WP_CLI, don't sync anything.
448
				if ( ! Settings::is_doing_cron() && ! Jetpack_Constants::get_constant( 'WP_CLI' ) ) {
449
					return;
450
				}
451
				// 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 ).
452
				$callables = $this->get_always_sent_callables();
453
			}
454
			if ( get_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME ) ) {
455
				if ( $this->force_send_callables_on_next_tick ) {
456
					$this->unlock_sync_callable();
457
				}
458
				return;
459
			}
460
		}
461
462
		if ( empty( $callables ) ) {
463
			return;
464
		}
465
		// No need to set the transiant we are trying to remove it anyways.
466
		if ( ! $this->force_send_callables_on_next_tick ) {
467
			set_transient( self::CALLABLES_AWAIT_TRANSIENT_NAME, microtime( true ), Defaults::$default_sync_callables_wait_time );
468
		}
469
470
		$callable_checksums = (array) \Jetpack_Options::get_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, array() );
471
		$has_changed        = false;
472
		// Only send the callables that have changed.
473 View Code Duplication
		foreach ( $callables as $name => $value ) {
474
			$checksum = $this->get_check_sum( $value );
475
			// Explicitly not using Identical comparison as get_option returns a string.
476
			if ( ! is_null( $value ) && $this->should_send_callable( $callable_checksums, $name, $checksum ) ) {
477
				/**
478
				 * Tells the client to sync a callable (aka function) to the server
479
				 *
480
				 * @since 4.2.0
481
				 *
482
				 * @param string The name of the callable
483
				 * @param mixed The value of the callable
484
				 */
485
				do_action( 'jetpack_sync_callable', $name, $value );
486
				$callable_checksums[ $name ] = $checksum;
487
				$has_changed                 = true;
488
			} else {
489
				$callable_checksums[ $name ] = $checksum;
490
			}
491
		}
492
		if ( $has_changed ) {
493
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callable_checksums );
494
		}
495
496
		if ( $this->force_send_callables_on_next_tick ) {
497
			$this->unlock_sync_callable();
498
		}
499
	}
500
501
	/**
502
	 * Get the callables that should always be sent, e.g. on cron.
503
	 *
504
	 * @return array Callables that should always be sent
505
	 */
506
	protected function get_always_sent_callables() {
507
		$callables      = $this->get_all_callables();
508
		$cron_callables = array();
509
		foreach ( self::ALWAYS_SEND_UPDATES_TO_THESE_OPTIONS as $option_name ) {
510
			if ( array_key_exists( $option_name, $callables ) ) {
511
				$cron_callables[ $option_name ] = $callables[ $option_name ];
512
				continue;
513
			}
514
515
			// Check for the Callable name/key for the option, if different from option name.
516
			if ( array_key_exists( $option_name, self::OPTION_NAMES_TO_CALLABLE_NAMES ) ) {
517
				$callable_name = self::OPTION_NAMES_TO_CALLABLE_NAMES[ $option_name ];
518
				if ( array_key_exists( $callable_name, $callables ) ) {
519
					$cron_callables[ $callable_name ] = $callables[ $callable_name ];
520
				}
521
			}
522
		}
523
		return $cron_callables;
524
	}
525
526
	/**
527
	 * Expand the callables within a hook before they are serialized and sent to the server.
528
	 *
529
	 * @access public
530
	 *
531
	 * @param array $args The hook parameters.
532
	 * @return array $args The hook parameters.
533
	 */
534
	public function expand_callables( $args ) {
535
		if ( $args[0] ) {
536
			$callables           = $this->get_all_callables();
537
			$callables_checksums = array();
538
			foreach ( $callables as $name => $value ) {
539
				$callables_checksums[ $name ] = $this->get_check_sum( $value );
540
			}
541
			\Jetpack_Options::update_raw_option( self::CALLABLES_CHECKSUM_OPTION_NAME, $callables_checksums );
542
			return $callables;
543
		}
544
545
		return $args;
546
	}
547
548
	/**
549
	 * Return Total number of objects.
550
	 *
551
	 * @param array $config Full Sync config.
552
	 *
553
	 * @return int total
554
	 */
555
	public function total( $config ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
556
		return count( $this->get_callable_whitelist() );
557
	}
558
559
}
560