Completed
Push — renovate/history-4.x ( 8706da...6c1ea7 )
by
unknown
17:57 queued 11:18
created

Jetpack_Widgets::update_widget_in_sidebar()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 12
nop 3
dl 0
loc 33
rs 8.4586
c 0
b 0
f 0
1
<?php
2
/**
3
 * Widgets and Sidebars Library
4
 *
5
 * Helper functions for manipulating widgets on a per-blog basis.
6
 * Only helpful on `wp_loaded` or later (currently requires widgets to be registered and the theme context to already be loaded).
7
 *
8
 * Used by the REST API
9
 *
10
 * @autounit api widgets
11
 */
12
13
class Jetpack_Widgets {
14
15
	/**
16
	 * Returns the `sidebars_widgets` option with the `array_version` element removed.
17
	 *
18
	 * @return array The current value of sidebars_widgets
19
	 */
20
	public static function get_sidebars_widgets() {
21
		$sidebars = get_option( 'sidebars_widgets', array() );
22
		if ( isset( $sidebars['array_version'] ) ) {
23
			unset( $sidebars['array_version'] );
24
		}
25
		return $sidebars;
26
	}
27
28
	/**
29
	 * Format widget data for output and for use by other widget functions.
30
	 *
31
	 * The output looks like:
32
	 *
33
	 * array(
34
	 *	'id' => 'text-3',
35
	 *	'sidebar' => 'sidebar-1',
36
	 *	'position' => '0',
37
	 *	'settings' => array(
38
	 *		'title' => 'hello world'
39
	 *	)
40
	 * )
41
	 *
42
	 *
43
	 * @param string|integer $position The position of the widget in its sidebar.
44
	 * @param string $widget_id The widget's id (eg: 'text-3').
45
	 * @param string $sidebar The widget's sidebar id (eg: 'sidebar-1').
46
	 * @param array (Optional) $settings The settings for the widget.
47
	 *
48
	 * @return array A normalized array representing this widget.
49
	 */
50
	public static function format_widget( $position, $widget_id, $sidebar, $settings = null ) {
51
		if ( ! $settings ) {
52
			$all_settings = get_option( self::get_widget_option_name( $widget_id ) );
53
			$instance = self::get_widget_instance_key( $widget_id );
54
			$settings = $all_settings[$instance];
55
		}
56
		$widget = array();
57
58
		$widget['id']       = $widget_id;
59
		$widget['id_base']  = self::get_widget_id_base( $widget_id );
60
		$widget['settings'] = $settings;
61
		$widget['sidebar']  = $sidebar;
62
		$widget['position'] = $position;
63
64
		return $widget;
65
	}
66
67
	/**
68
	 * Return a widget's id_base from its id.
69
	 *
70
	 * @param string $widget_id The id of a widget. (eg: 'text-3')
71
	 *
72
	 * @return string The id_base of a widget (eg: 'text').
73
	 */
74
	public static function get_widget_id_base( $widget_id ) {
75
		// Grab what's before the hyphen.
76
		return substr( $widget_id, 0, strrpos( $widget_id, '-' ) );
77
	}
78
79
	/**
80
	 * Determine a widget's option name (the WP option where the widget's settings
81
	 * are stored - generally `widget_` + the widget's id_base).
82
	 *
83
	 * @param string $widget_id The id of a widget. (eg: 'text-3')
84
	 *
85
	 * @return string The option name of the widget's settings. (eg: 'widget_text')
86
	 */
87
	public static function get_widget_option_name( $widget_id ) {
88
		return 'widget_' . self::get_widget_id_base( $widget_id );
89
	}
90
91
	/**
92
	 * Determine a widget instance key from its ID. (eg: 'text-3' becomes '3').
93
	 * Used to access the widget's settings.
94
	 *
95
	 * @param string $widget_id The id of a widget.
96
	 *
97
	 * @return integer The instance key of that widget.
98
	 */
99
	public static function get_widget_instance_key( $widget_id ) {
100
		// Grab all numbers from the end of the id.
101
		preg_match('/(\d+)$/', $widget_id, $matches );
102
103
		return intval( $matches[0] );
104
	}
105
106
	/**
107
	 * Return a widget by ID (formatted for output) or null if nothing is found.
108
	 *
109
	 * @param string $widget_id The id of a widget to look for.
110
	 *
111
	 * @return array|null The matching formatted widget (see format_widget).
112
	 */
113
	public static function get_widget_by_id( $widget_id ) {
114
		$found = null;
115
		foreach ( self::get_all_widgets() as $widget ) {
116
			if ( $widget['id'] === $widget_id ) {
117
				$found = $widget;
118
			}
119
		}
120
		return $found;
121
	}
122
123
	/**
124
	 * Return an array of all widgets (active and inactive) formatted for output.
125
	 *
126
	 * @return array An array of all widgets (see format_widget).
127
	 */
128
	public static function get_all_widgets() {
129
		$all_widgets = array();
130
		$sidebars_widgets = self::get_all_sidebars();
131
132
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
133
			if ( ! is_array( $widgets ) ) {
134
				continue;
135
			}
136
			foreach ( $widgets as $key => $widget_id ) {
137
				array_push( $all_widgets, self::format_widget( $key, $widget_id, $sidebar ) );
138
			}
139
		}
140
141
		return $all_widgets;
142
	}
143
144
	/**
145
	 * Return an array of all active widgets formatted for output.
146
	 *
147
	 * @return array An array of all active widgets (see format_widget).
148
	 */
149
	public static function get_active_widgets() {
150
		$active_widgets = array();
151
		$all_widgets = self::get_all_widgets();
152
		foreach( $all_widgets as $widget ) {
153
			if ( 'wp_inactive_widgets' === $widget['sidebar'] ) {
154
				continue;
155
			}
156
			array_push( $active_widgets, $widget );
157
		}
158
		return $active_widgets;
159
	}
160
161
	/**
162
	 * Return an array of all widget IDs (active and inactive)
163
	 *
164
	 * @return array An array of all widget IDs.
165
	 */
166
	public static function get_all_widget_ids() {
167
		$all_widgets = array();
168
		$sidebars_widgets = self::get_all_sidebars();
169
		foreach ( array_values( $sidebars_widgets ) as $widgets ) {
170
			if ( ! is_array( $widgets ) ) {
171
				continue;
172
			}
173
			foreach ( array_values( $widgets ) as $widget_id ) {
174
				array_push( $all_widgets, $widget_id );
175
			}
176
		}
177
		return $all_widgets;
178
	}
179
180
	/**
181
	 * Return an array of widgets with a specific id_base (eg: `text`).
182
	 *
183
	 * @param string $id_base The id_base of a widget type.
184
	 *
185
	 * @return array All the formatted widgets matching that widget type (see format_widget).
186
	 */
187
	public static function get_widgets_with_id_base( $id_base ) {
188
		$matching_widgets = array();
189
		foreach ( self::get_all_widgets() as $widget ) {
190
			if ( self::get_widget_id_base( $widget['id'] ) === $id_base ) {
191
				array_push( $matching_widgets, $widget );
192
			}
193
		}
194
		return $matching_widgets;
195
	}
196
197
	/**
198
	 * Return the array of widget IDs in a sidebar or null if that sidebar does
199
	 * not exist. Will return an empty array for an existing empty sidebar.
200
	 *
201
	 * @param string $sidebar The id of a sidebar.
202
	 *
203
	 * @return array|null The array of widget IDs in the sidebar.
204
	 */
205
	public static function get_widgets_in_sidebar( $sidebar ) {
206
		$sidebars = self::get_all_sidebars();
207
208
209
		if ( ! $sidebars || ! is_array( $sidebars ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sidebars of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
210
			return null;
211
		}
212
		if ( ! $sidebars[ $sidebar ] && array_key_exists( $sidebar, $sidebars ) ) {
213
			return array();
214
		}
215
		return $sidebars[ $sidebar ];
216
	}
217
218
	/**
219
	 * Return an associative array of all registered sidebars for this theme,
220
	 * active and inactive, including the hidden disabled widgets sidebar (keyed
221
	 * by `wp_inactive_widgets`). Each sidebar is keyed by the ID of the sidebar
222
	 * and its value is an array of widget IDs for that sidebar.
223
	 *
224
	 * @return array An associative array of all sidebars and their widget IDs.
225
	 */
226
	public static function get_all_sidebars() {
227
		$sidebars_widgets = self::get_sidebars_widgets();
228
229
		if ( ! is_array( $sidebars_widgets ) ) {
230
			return array();
231
		}
232
		return $sidebars_widgets;
233
	}
234
235
	/**
236
	 * Return an associative array of all active sidebars for this theme, Each
237
	 * sidebar is keyed by the ID of the sidebar and its value is an array of
238
	 * widget IDs for that sidebar.
239
	 *
240
	 * @return array An associative array of all active sidebars and their widget IDs.
241
	 */
242
	public static function get_active_sidebars() {
243
		$sidebars = array();
244
		foreach ( self::get_all_sidebars() as $sidebar => $widgets ) {
245
			if ( 'wp_inactive_widgets' === $sidebar || ! isset( $widgets ) || ! is_array( $widgets ) ) {
246
				continue;
247
			}
248
			$sidebars[ $sidebar ] = $widgets;
249
		}
250
		return $sidebars;
251
	}
252
253
	/**
254
	 * Activates a widget in a sidebar. Does not validate that the sidebar exists,
255
	 * so please do that first. Also does not save the widget's settings. Please
256
	 * do that with `set_widget_settings`.
257
	 *
258
	 * If position is not set, it will be set to the next available position.
259
	 *
260
	 * @param string         $widget_id The newly-formed id of the widget to be added.
261
	 * @param string         $sidebar   The id of the sidebar where the widget will be added.
262
	 * @param string|integer $position  (Optional) The position within the sidebar where the widget will be added.
263
	 *
264
	 * @return bool
265
	 */
266
	public static function add_widget_to_sidebar( $widget_id, $sidebar, $position ) {
267
		return self::move_widget_to_sidebar( array( 'id' => $widget_id ), $sidebar, $position );
268
	}
269
270
	/**
271
	 * Removes a widget from a sidebar. Does not validate that the sidebar exists
272
	 * or remove any settings from the widget, so please do that separately.
273
	 *
274
	 * @param array $widget The widget to be removed.
275
	 */
276
	public static function remove_widget_from_sidebar( $widget ) {
277
		$sidebars_widgets = self::get_sidebars_widgets();
278
		// Remove the widget from its old location and reflow the positions of the remaining widgets.
279
		array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
280
281
		update_option( 'sidebars_widgets', $sidebars_widgets );
282
	}
283
284
	/**
285
	 * Moves a widget to a sidebar. Does not validate that the sidebar exists,
286
	 * so please do that first. Also does not save the widget's settings. Please
287
	 * do that with `set_widget_settings`. The first argument should be a
288
	 * widget as returned by `format_widget` including `id`, `sidebar`, and
289
	 * `position`.
290
	 *
291
	 * If $position is not set, it will be set to the next available position.
292
	 *
293
	 * Can be used to add a new widget to a sidebar if
294
	 * $widget['sidebar'] === NULL
295
	 *
296
	 * Can be used to move a widget within a sidebar as well if
297
	 * $widget['sidebar'] === $sidebar.
298
	 *
299
	 * @param array          $widget   The widget to be moved (see format_widget).
300
	 * @param string         $sidebar  The sidebar where this widget will be moved.
301
	 * @param string|integer $position (Optional) The position where this widget will be moved in the sidebar.
302
	 *
303
	 * @return bool
304
	 */
305
	public static function move_widget_to_sidebar( $widget, $sidebar, $position ) {
306
		$sidebars_widgets = self::get_sidebars_widgets();
307
308
		// If a position is passed and the sidebar isn't empty,
309
		// splice the widget into the sidebar, update the sidebar option, and return the result
310
		if ( isset( $widget['sidebar'] ) && isset( $widget['position'] ) ) {
311
			array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
312
		}
313
314
		// Sometimes an existing empty sidebar is NULL, so initialize it.
315
		if ( array_key_exists( $sidebar, $sidebars_widgets ) && ! is_array( $sidebars_widgets[ $sidebar ] ) ) {
316
			$sidebars_widgets[ $sidebar ] = array();
317
		}
318
319
		// If no position is passed, set one from items in sidebar
320
		if ( ! isset( $position ) ) {
321
			$position = 0;
322
			$last_position = self::get_last_position_in_sidebar( $sidebar );
323
			if ( isset( $last_position ) && is_numeric( $last_position ) ) {
324
				$position = $last_position + 1;
325
			}
326
		}
327
328
		// Add the widget to the sidebar and reflow the positions of the other widgets.
329
		if ( empty( $sidebars_widgets[ $sidebar ] ) ) {
330
			$sidebars_widgets[ $sidebar ][] = $widget['id'];
331
		} else {
332
			array_splice( $sidebars_widgets[ $sidebar ], (int)$position, 0, $widget['id'] );
333
		}
334
335
		set_theme_mod( 'sidebars_widgets', array( 'time' => time(), 'data' => $sidebars_widgets ) );
336
		return update_option( 'sidebars_widgets', $sidebars_widgets );
337
	}
338
339
	/**
340
	 * Return an integer containing the largest position number in a sidebar or
341
	 * null if there are no widgets in that sidebar.
342
	 *
343
	 * @param string $sidebar The id of a sidebar.
344
	 *
345
	 * @return integer|null The last index position of a widget in that sidebar.
346
	 */
347
	public static function get_last_position_in_sidebar( $sidebar ) {
348
		$widgets = self::get_widgets_in_sidebar( $sidebar );
349
		if ( ! $widgets ) {
350
			return null;
351
		}
352
		$last_position = 0;
353
		foreach ( $widgets as $widget_id ) {
354
			$widget = self::get_widget_by_id( $widget_id );
355
			if ( intval( $widget['position'] ) > intval( $last_position ) ) {
356
				$last_position = intval( $widget['position'] );
357
			}
358
		}
359
		return $last_position;
360
	}
361
362
	/**
363
	 * Saves settings for a widget. Does not add that widget to a sidebar. Please
364
	 * do that with `move_widget_to_sidebar` first. Will merge the settings of
365
	 * any existing widget with the same `$widget_id`.
366
	 *
367
	 * @param string $widget_id The id of a widget.
368
	 * @param array $settings An associative array of settings to merge with any existing settings on this widget.
369
	 *
370
	 * @return boolean|WP_Error True if update was successful.
371
	 */
372
	public static function set_widget_settings( $widget_id, $settings ) {
373
		$widget_option_name = self::get_widget_option_name( $widget_id );
374
		$widget_settings = get_option( $widget_option_name );
375
		$instance_key = self::get_widget_instance_key( $widget_id );
376
		$old_settings = $widget_settings[ $instance_key ];
377
378
		if ( ! $settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings ) ) {
379
			return new WP_Error( 'invalid_data', 'Update failed.', 500 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
380
		}
381
		if ( is_array( $old_settings ) ) {
382
			// array_filter prevents empty arguments from replacing existing ones
383
			$settings = wp_parse_args( array_filter( $settings ), $old_settings );
0 ignored issues
show
Documentation introduced by
$old_settings is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
384
		}
385
386
		$widget_settings[ $instance_key ] = $settings;
387
388
		return update_option( $widget_option_name, $widget_settings );
389
	}
390
391
	/**
392
	 * Sanitize an associative array for saving.
393
	 *
394
	 * @param string $widget_id The id of a widget.
395
	 * @param array $settings A widget settings array.
396
	 * @param array $old_settings The existing widget settings array.
397
	 *
398
	 * @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
399
	 */
400
	private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
401
		if ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
402
			return false;
403
		}
404
		$new_settings = $widget->update( $settings, $old_settings );
405
		if ( ! is_array( $new_settings ) ) {
406
			return false;
407
		}
408
		return $new_settings;
409
	}
410
411
	/**
412
	 * Deletes settings for a widget. Does not remove that widget to a sidebar. Please
413
	 * do that with `remove_widget_from_sidebar` first.
414
	 *
415
	 * @param array $widget The widget which will have its settings removed (see format_widget).
416
	 */
417
	public static function remove_widget_settings( $widget ) {
418
		$widget_option_name = self::get_widget_option_name( $widget['id'] );
419
		$widget_settings = get_option( $widget_option_name );
420
		unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
421
		update_option( $widget_option_name, $widget_settings );
422
	}
423
424
	/**
425
	 * Update a widget's settings, sidebar, and position. Returns the (updated)
426
	 * formatted widget if successful or a WP_Error if it fails.
427
	 *
428
	 * @param string $widget_id The id of a widget to update.
429
	 * @param string $sidebar (Optional) A sidebar to which this widget will be moved.
430
	 * @param string|integer (Optional) A new position to which this widget will be moved within its new or existing sidebar.
431
	 * @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
432
	 *
433
	 * @return array|WP_Error The newly added widget as an associative array with all the above properties.
434
	 */
435
	public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
436
		$settings = self::decode_settings( $settings );
437
		if ( isset( $settings ) && ! is_array( $settings ) ) {
438
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
439
		}
440
		// Default to an empty array if nothing is specified.
441
		if ( ! is_array( $settings ) ) {
442
			$settings = array();
443
		}
444
		$widget = self::get_widget_by_id( $widget_id );
445
		if ( ! $widget ) {
446
			return new WP_Error( 'not_found', 'No widget found.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'not_found'.

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...
447
		}
448
		if ( ! $sidebar ) {
449
			$sidebar = $widget['sidebar'];
450
		}
451
		if ( ! isset( $position ) ) {
452
			$position = $widget['position'];
453
		}
454
		if ( ! is_numeric( $position ) ) {
455
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
456
		}
457
		$widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
458
		if ( ! isset( $widgets_in_sidebar ) ) {
459
			return new WP_Error( 'invalid_data', 'No such sidebar exists', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
460
		}
461
		self::move_widget_to_sidebar( $widget, $sidebar, $position );
462
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
463
		if ( is_wp_error( $widget_save_status ) ) {
464
			return $widget_save_status;
465
		}
466
		return self::get_widget_by_id( $widget_id );
467
	}
468
469
	/**
470
	 * Deletes a widget entirely including all its settings. Returns a WP_Error if
471
	 * the widget could not be found. Otherwise returns an empty array.
472
	 *
473
	 * @param string $widget_id The id of a widget to delete. (eg: 'text-2')
474
	 *
475
	 * @return array|WP_Error An empty array if successful.
476
	 */
477
	public static function delete_widget( $widget_id ) {
478
		$widget = self::get_widget_by_id( $widget_id );
479
		if ( ! $widget ) {
480
			return new WP_Error( 'not_found', 'No widget found.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'not_found'.

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...
481
		}
482
		self::remove_widget_from_sidebar( $widget );
483
		self::remove_widget_settings( $widget );
484
		return array();
485
	}
486
487
	/**
488
	 * Return an array of settings. The input can be either an object, a JSON
489
	 * string, or an array.
490
	 *
491
	 * @param array|string|object $settings The settings of a widget as passed into the API.
492
	 *
493
	 * @return array Decoded associative array of settings.
494
	 */
495
	public static function decode_settings( $settings ) {
496
		// Treat as string in case JSON was passed
497
		if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
498
			$settings = $settings->scalar;
499
		}
500
		if ( is_object( $settings ) ) {
501
			$settings = (array) $settings;
502
		}
503
		// Attempt to decode JSON string
504
		if ( is_string( $settings ) ) {
505
			$settings = (array) json_decode( $settings );
506
		}
507
		return $settings;
508
	}
509
510
	/**
511
	 * Activate a new widget.
512
	 *
513
	 * @param string $id_base The id_base of the new widget (eg: 'text')
514
	 * @param string $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1')
515
	 * @param string|integer $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
516
	 * @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
517
	 *
518
	 * @return array|WP_Error The newly added widget as an associative array with all the above properties except 'id_base' replaced with the generated 'id'.
519
	 */
520
	public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
521
		if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
522
			return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
523
		}
524
525
		if ( ! isset( $sidebar ) ) {
526
			return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
527
		}
528
529
		if ( isset( $position ) && ! is_numeric( $position ) ) {
530
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
531
		}
532
533
		$settings = self::decode_settings( $settings );
534
		if ( isset( $settings ) && ! is_array( $settings ) ) {
535
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
536
		}
537
538
		// Default to an empty array if nothing is specified.
539
		if ( ! is_array( $settings ) ) {
540
			$settings = array();
541
		}
542
543
		$widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
544
		$widget_id = $id_base . '-' . $widget_counter;
545
		if ( 0 >= $widget_counter ) {
546
			return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
547
		}
548
		if ( self::get_widget_by_id( $widget_id ) ) {
549
			return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
550
		}
551
552
		self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
553
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
554
		if ( is_wp_error( $widget_save_status ) ) {
555
			return $widget_save_status;
556
		}
557
558
		// Add a Tracks event for non-Headstart activity.
559
		if ( ! defined( 'HEADSTART' ) ) {
560
			$tracking = new Automattic\Jetpack\Tracking();
561
			$tracking->tracks_record_event(
562
				wp_get_current_user(),
563
				'wpcom_widgets_activate_widget',
564
				array(
565
					'widget'   => $id_base,
566
					'settings' => wp_json_encode( $settings ),
567
				)
568
			);
569
		}
570
571
		return self::get_widget_by_id( $widget_id );
572
	}
573
574
	/**
575
	 * Activate an array of new widgets. Like calling `activate_widget` multiple times.
576
	 *
577
	 * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
578
	 *
579
	 * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
580
	 */
581
	public static function activate_widgets( $widgets ) {
582
		if ( ! is_array( $widgets ) ) {
583
			return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
584
		}
585
586
		$added_widgets = array();
587
588
		foreach( $widgets as $widget ) {
589
			$added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
590
		}
591
592
		return $added_widgets;
593
	}
594
595
	/**
596
	 * Return the last instance key (integer) of an existing widget matching
597
	 * `$id_base`. So if you pass in `text`, and there is a widget with the id
598
	 * `text-2`, this function will return `2`.
599
	 *
600
	 * @param string $id_base The id_base of a type of widget. (eg: 'rss')
601
	 *
602
	 * @return integer The last instance key of that type of widget.
603
	 */
604
	public static function get_last_widget_instance_key_with_id_base( $id_base ) {
605
		$similar_widgets = self::get_widgets_with_id_base( $id_base );
606
607
		if ( ! empty( $similar_widgets ) ) {
608
			// If the last widget with the same name is `text-3`, we want `text-4`
609
			usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
610
611
			$last_widget = array_pop( $similar_widgets );
612
			$last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
613
614
			return $last_val;
615
		}
616
617
		return 0;
618
	}
619
620
	/**
621
	 * Method used to sort widgets
622
	 *
623
	 * @since 5.4
624
	 *
625
	 * @param array $a
626
	 * @param array $b
627
	 *
628
	 * @return int
629
	 */
630
	public static function sort_widgets( $a, $b ) {
631
		$a_val = intval( self::get_widget_instance_key( $a['id'] ) );
632
		$b_val = intval( self::get_widget_instance_key( $b['id'] ) );
633
		if ( $a_val > $b_val ) {
634
			return 1;
635
		}
636
		if ( $a_val < $b_val ) {
637
			return -1;
638
		}
639
		return 0;
640
	}
641
642
	/**
643
	 * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
644
	 *
645
	 * @param string $id_base The id_base of a type of widget.
646
	 *
647
	 * @return WP_Widget|false The found widget object or false if the id_base was not found.
648
	 */
649
	public static function get_registered_widget_object( $id_base ) {
650
		if ( ! $id_base ) {
651
			return false;
652
		}
653
654
		// Get all of the registered widgets.
655
		global $wp_widget_factory;
656
		if ( ! isset( $wp_widget_factory ) ) {
657
			return false;
658
		}
659
660
		$registered_widgets = $wp_widget_factory->widgets;
661
		if ( empty( $registered_widgets ) ) {
662
			return false;
663
		}
664
665
		foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
666
			if ( $registered_widget_object->id_base === $id_base ) {
667
				return $registered_widget_object;
668
			}
669
		}
670
		return false;
671
	}
672
673
	/**
674
	 * Validate a given widget ID base (eg. 'text' or 'archives').
675
	 *
676
	 * @param string $id_base The id_base of a type of widget.
677
	 *
678
	 * @return boolean True if the widget is of a known type.
679
	 */
680
	public static function validate_id_base( $id_base ) {
681
		return ( false !== self::get_registered_widget_object( $id_base ) );
682
	}
683
684
	/**
685
	 * Insert a new widget in a given sidebar.
686
	 *
687
	 * @param string $widget_id ID of the widget.
688
	 * @param array $widget_options Content of the widget.
689
 	 * @param string $sidebar ID of the sidebar to which the widget will be added.
690
 	 *
691
 	 * @return WP_Error|true True when data has been saved correctly, error otherwise.
692
	*/
693
	static function insert_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
694
		// Retrieve sidebars, widgets and their instances
695
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
696
		$widget_instances = get_option( 'widget_' . $widget_id, array() );
697
698
		// Retrieve the key of the next widget instance
699
		$numeric_keys = array_filter( array_keys( $widget_instances ), 'is_int' );
700
		$next_key = $numeric_keys ? max( $numeric_keys ) + 1 : 2;
701
702
		// Add this widget to the sidebar
703
		if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
704
			$sidebars_widgets[ $sidebar ] = array();
705
		}
706
		$sidebars_widgets[ $sidebar ][] = $widget_id . '-' . $next_key;
707
708
		// Add the new widget instance
709
		$widget_instances[ $next_key ] = $widget_options;
710
711
		// Store updated sidebars, widgets and their instances
712
		if (
713
			! ( update_option( 'sidebars_widgets', $sidebars_widgets ) )
714
			|| ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) )
715
		) {
716
			return new WP_Error( 'widget_update_failed', 'Failed to update widget or sidebar.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'widget_update_failed'.

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...
717
		};
718
719
		return true;
720
	}
721
722
	/**
723
	 * Update the content of an existing widget in a given sidebar.
724
	 *
725
	 * @param string $widget_id ID of the widget.
726
	 * @param array $widget_options New content for the update.
727
 	 * @param string $sidebar ID of the sidebar to which the widget will be added.
728
 	 *
729
 	 * @return WP_Error|true True when data has been updated correctly, error otherwise.
730
	*/
731
	static function update_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
732
		// Retrieve sidebars, widgets and their instances
733
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
734
		$widget_instances = get_option( 'widget_' . $widget_id, array() );
735
736
		// Retrieve index of first widget instance in that sidebar
737
		$widget_key = false;
738
		foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
739
			if ( strpos( $widget, $widget_id ) !== false ) {
740
				$widget_key = absint( str_replace( $widget_id . '-', '', $widget ) );
741
				break;
742
			}
743
		}
744
745
		// There is no widget instance
746
		if ( ! $widget_key ) {
747
			return new WP_Error( 'invalid_data', 'No such widget.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'invalid_data'.

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...
748
		}
749
750
		// Update the widget instance and option if the data has changed
751
		if ( $widget_instances[ $widget_key ]['title'] !== $widget_options['title']
752
			|| $widget_instances[ $widget_key ]['address'] !== $widget_options['address']
753
		) {
754
755
			$widget_instances[ $widget_key ] = array_merge( $widget_instances[ $widget_key ], $widget_options );
756
757
			// Store updated widget instances and return Error when not successful
758
			if ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) ) {
759
				return new WP_Error( 'widget_update_failed', 'Failed to update widget.', 400 );
0 ignored issues
show
Unused Code introduced by
The call to WP_Error::__construct() has too many arguments starting with 'widget_update_failed'.

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...
760
			};
761
		};
762
		return true;
763
	}
764
765
	/**
766
	 * Retrieve the first active sidebar.
767
	 *
768
	 * @return string|WP_Error First active sidebar, error if none exists.
769
	*/
770
	static function get_first_sidebar() {
771
		$active_sidebars = get_option( 'sidebars_widgets', array() );
772
		unset( $active_sidebars[ 'wp_inactive_widgets' ], $active_sidebars[ 'array_version' ] );
773
774
		if ( empty( $active_sidebars ) ) {
775
			return false;
776
		}
777
		$active_sidebars_keys = array_keys( $active_sidebars );
778
		return array_shift( $active_sidebars_keys );
779
	}
780
}
781