Completed
Push — fix/widget-extend-and-fix-on-u... ( 434ae4...62c46a )
by
unknown
43:14 queued 14:28
created

Jetpack_Widgets::remove_widget_from_sidebar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
rs 9.4285
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
		foreach ( self::get_all_widgets() as $widget ) {
115
			if ( $widget['id'] === $widget_id ) {
116
				return $widget;
117
			}
118
		}
119
		return null;
120
	}
121
122
	/**
123
	 * Return a widget by base ID or null if nothing is found.
124
	 *
125
	 * @param string $id_base The id of a widget to look for.
126
	 *
127
	 * @return array|null The matching formatted widget (see format_widget).
128
	 */
129
	public static function get_widget_by_id_base( $id_base ) {
130
		foreach ( self::get_all_widgets() as $widget ) {
131
			if ( $widget['id_base'] === $id_base ) {
132
				return $widget;
133
			}
134
		}
135
		return null;
136
	}
137
138
	/**
139
	 * Return an array of all widgets (active and inactive) formatted for output.
140
	 *
141
	 * @return array An array of all widgets (see format_widget).
142
	 */
143
	public static function get_all_widgets() {
144
		$all_widgets = array();
145
		$sidebars_widgets = self::get_all_sidebars();
146
147
		foreach ( $sidebars_widgets as $sidebar => $widgets ) {
148
			if ( ! is_array( $widgets ) ) {
149
				continue;
150
			}
151
			foreach ( $widgets as $key => $widget_id ) {
152
				array_push( $all_widgets, self::format_widget( $key, $widget_id, $sidebar ) );
153
			}
154
		}
155
156
		return $all_widgets;
157
	}
158
159
	/**
160
	 * Return an array of all active widgets formatted for output.
161
	 *
162
	 * @return array An array of all active widgets (see format_widget).
163
	 */
164
	public static function get_active_widgets() {
165
		$active_widgets = array();
166
		$all_widgets = self::get_all_widgets();
167
		foreach( $all_widgets as $widget ) {
168
			if ( 'wp_inactive_widgets' === $widget['sidebar'] ) {
169
				continue;
170
			}
171
			array_push( $active_widgets, $widget );
172
		}
173
		return $active_widgets;
174
	}
175
176
	/**
177
	 * Return an array of all widget IDs (active and inactive)
178
	 *
179
	 * @return array An array of all widget IDs.
180
	 */
181
	public static function get_all_widget_ids() {
182
		$all_widgets = array();
183
		$sidebars_widgets = self::get_all_sidebars();
184
		foreach ( array_values( $sidebars_widgets ) as $widgets ) {
185
			if ( ! is_array( $widgets ) ) {
186
				continue;
187
			}
188
			foreach ( array_values( $widgets ) as $widget_id ) {
189
				array_push( $all_widgets, $widget_id );
190
			}
191
		}
192
		return $all_widgets;
193
	}
194
195
	/**
196
	 * Return an array of widgets with a specific id_base (eg: `text`).
197
	 *
198
	 * @param string $id_base The id_base of a widget type.
199
	 *
200
	 * @return array All the formatted widgets matching that widget type (see format_widget).
201
	 */
202
	public static function get_widgets_with_id_base( $id_base ) {
203
		$matching_widgets = array();
204
		foreach ( self::get_all_widgets() as $widget ) {
205
			if ( self::get_widget_id_base( $widget['id'] ) === $id_base ) {
206
				array_push( $matching_widgets, $widget );
207
			}
208
		}
209
		return $matching_widgets;
210
	}
211
212
	/**
213
	 * Return the array of widget IDs in a sidebar or null if that sidebar does
214
	 * not exist. Will return an empty array for an existing empty sidebar.
215
	 *
216
	 * @param string $sidebar The id of a sidebar.
217
	 *
218
	 * @return array|null The array of widget IDs in the sidebar.
219
	 */
220
	public static function get_widgets_in_sidebar( $sidebar ) {
221
		$sidebars = self::get_all_sidebars();
222
223
		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...
224
			return null;
225
		}
226
		if ( ! $sidebars[ $sidebar ] && array_key_exists( $sidebar, $sidebars ) ) {
227
			return array();
228
		}
229
		return $sidebars[ $sidebar ];
230
	}
231
232
	/**
233
	 * Return an associative array of all registered sidebars for this theme,
234
	 * active and inactive, including the hidden disabled widgets sidebar (keyed
235
	 * by `wp_inactive_widgets`). Each sidebar is keyed by the ID of the sidebar
236
	 * and its value is an array of widget IDs for that sidebar.
237
	 *
238
	 * @return array An associative array of all sidebars and their widget IDs.
239
	 */
240
	public static function get_all_sidebars() {
241
		$sidebars_widgets = self::get_sidebars_widgets();
242
243
		if ( ! is_array( $sidebars_widgets ) ) {
244
			return array();
245
		}
246
		return $sidebars_widgets;
247
	}
248
249
	/**
250
	 * Return an associative array of all active sidebars for this theme, Each
251
	 * sidebar is keyed by the ID of the sidebar and its value is an array of
252
	 * widget IDs for that sidebar.
253
	 *
254
	 * @return array An associative array of all active sidebars and their widget IDs.
255
	 */
256
	public static function get_active_sidebars() {
257
		$sidebars = array();
258
		foreach ( self::get_all_sidebars() as $sidebar => $widgets ) {
259
			if ( 'wp_inactive_widgets' === $sidebar || ! isset( $widgets ) || ! is_array( $widgets ) ) {
260
				continue;
261
			}
262
			$sidebars[ $sidebar ] = $widgets;
263
		}
264
		return $sidebars;
265
	}
266
267
	/**
268
	 * Activates a widget in a sidebar. Does not validate that the sidebar exists,
269
	 * so please do that first. Also does not save the widget's settings. Please
270
	 * do that with `set_widget_settings`.
271
	 *
272
	 * If position is not set, it will be set to the next available position.
273
	 *
274
	 * @param string         $widget_id The newly-formed id of the widget to be added.
275
	 * @param string         $sidebar   The id of the sidebar where the widget will be added.
276
	 * @param string|integer $position  (Optional) The position within the sidebar where the widget will be added.
277
	 *
278
	 * @return bool
279
	 */
280
	public static function add_widget_to_sidebar( $widget_id, $sidebar, $position ) {
281
		return self::move_widget_to_sidebar( array( 'id' => $widget_id ), $sidebar, $position );
282
	}
283
284
	/**
285
	 * Removes a widget from a sidebar. Does not validate that the sidebar exists
286
	 * or remove any settings from the widget, so please do that separately.
287
	 *
288
	 * @param array $widget The widget to be removed.
289
	 */
290
	public static function remove_widget_from_sidebar( $widget ) {
291
		$sidebars_widgets = self::get_sidebars_widgets();
292
		// Remove the widget from its old location and reflow the positions of the remaining widgets.
293
		array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
294
295
		update_option( 'sidebars_widgets', $sidebars_widgets );
296
	}
297
298
	/**
299
	 * Moves a widget to a sidebar. Does not validate that the sidebar exists,
300
	 * so please do that first. Also does not save the widget's settings. Please
301
	 * do that with `set_widget_settings`. The first argument should be a
302
	 * widget as returned by `format_widget` including `id`, `sidebar`, and
303
	 * `position`.
304
	 *
305
	 * If $position is not set, it will be set to the next available position.
306
	 *
307
	 * Can be used to add a new widget to a sidebar if
308
	 * $widget['sidebar'] === NULL
309
	 *
310
	 * Can be used to move a widget within a sidebar as well if
311
	 * $widget['sidebar'] === $sidebar.
312
	 *
313
	 * @param array          $widget   The widget to be moved (see format_widget).
314
	 * @param string         $sidebar  The sidebar where this widget will be moved.
315
	 * @param string|integer $position (Optional) The position where this widget will be moved in the sidebar.
316
	 *
317
	 * @return bool
318
	 */
319
	public static function move_widget_to_sidebar( $widget, $sidebar, $position ) {
320
		$sidebars_widgets = self::get_sidebars_widgets();
321
322
		// If a position is passed and the sidebar isn't empty,
323
		// splice the widget into the sidebar, update the sidebar option, and return the result
324
		if ( isset( $widget['sidebar'] ) && isset( $widget['position'] ) ) {
325
			array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
326
		}
327
328
		// Sometimes an existing empty sidebar is NULL, so initialize it.
329
		if ( array_key_exists( $sidebar, $sidebars_widgets ) && ! is_array( $sidebars_widgets[ $sidebar ] ) ) {
330
			$sidebars_widgets[ $sidebar ] = array();
331
		}
332
333
		// If no position is passed, set one from items in sidebar
334
		if ( ! isset( $position ) ) {
335
			$position = 0;
336
			$last_position = self::get_last_position_in_sidebar( $sidebar );
337
			if ( isset( $last_position ) && is_numeric( $last_position ) ) {
338
				$position = $last_position + 1;
339
			}
340
		}
341
342
		// Add the widget to the sidebar and reflow the positions of the other widgets.
343
		if ( empty( $sidebars_widgets[ $sidebar ] ) ) {
344
			$sidebars_widgets[ $sidebar ][] = $widget['id'];
345
		} else {
346
			array_splice( $sidebars_widgets[ $sidebar ], (int)$position, 0, $widget['id'] );
347
		}
348
349
		set_theme_mod( 'sidebars_widgets', array( 'time' => time(), 'data' => $sidebars_widgets ) );
350
		return update_option( 'sidebars_widgets', $sidebars_widgets );
351
	}
352
353
	/**
354
	 * Return an integer containing the largest position number in a sidebar or
355
	 * null if there are no widgets in that sidebar.
356
	 *
357
	 * @param string $sidebar The id of a sidebar.
358
	 *
359
	 * @return integer|null The last index position of a widget in that sidebar.
360
	 */
361
	public static function get_last_position_in_sidebar( $sidebar ) {
362
		$widgets = self::get_widgets_in_sidebar( $sidebar );
363
		if ( ! $widgets ) {
364
			return null;
365
		}
366
		$last_position = 0;
367
		foreach ( $widgets as $widget_id ) {
368
			$widget = self::get_widget_by_id( $widget_id );
369
			if ( intval( $widget['position'] ) > intval( $last_position ) ) {
370
				$last_position = intval( $widget['position'] );
371
			}
372
		}
373
		return $last_position;
374
	}
375
376
	/**
377
	 * Saves settings for a widget. Does not add that widget to a sidebar. Please
378
	 * do that with `move_widget_to_sidebar` first. Will merge the settings of
379
	 * any existing widget with the same `$widget_id`.
380
	 *
381
	 * @param string $widget_id The id of a widget.
382
	 * @param array $settings An associative array of settings to merge with any existing settings on this widget.
383
	 *
384
	 * @return boolean|WP_Error True if update was successful.
385
	 */
386
	public static function set_widget_settings( $widget_id, $settings ) {
387
		$widget_option_name = self::get_widget_option_name( $widget_id );
388
		$widget_settings = get_option( $widget_option_name );
389
		$instance_key = self::get_widget_instance_key( $widget_id );
390
391
		// if the widget's content already exists
392
		if ( isset( $widget_settings[ $instance_key ] ) ) {
393
			$old_settings = $widget_settings[ $instance_key ];
394
395
			if ( ! $settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings ) ) {
396
				return new WP_Error( 'invalid_data', 'Update failed.', 400 );
397
			}
398
			if ( is_array( $old_settings ) ) {
399
				// array_filter prevents empty arguments from replacing existing ones
400
				$settings = wp_parse_args( array_filter( $settings ), $old_settings );
401
			}
402
		}
403
		$widget_settings[ $instance_key ] = $settings;
404
405
		return update_option( $widget_option_name, $widget_settings );
406
	}
407
408
	/**
409
	 * Sanitize an associative array for saving.
410
	 *
411
	 * @param string $widget_id The id of a widget.
412
	 * @param array $settings A widget settings array.
413
	 * @param array $old_settings The existing widget settings array.
414
	 *
415
	 * @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
416
	 */
417
	private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
418
		if ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
419
			return false;
420
		}
421
		$new_settings = $widget->update( $settings, $old_settings );
422
		if ( ! is_array( $new_settings ) ) {
423
			return false;
424
		}
425
		return $new_settings;
426
	}
427
428
	/**
429
	 * Deletes settings for a widget. Does not remove that widget to a sidebar. Please
430
	 * do that with `remove_widget_from_sidebar` first.
431
	 *
432
	 * @param array $widget The widget which will have its settings removed (see format_widget).
433
	 */
434
	public static function remove_widget_settings( $widget ) {
435
		$widget_option_name = self::get_widget_option_name( $widget['id'] );
436
		$widget_settings = get_option( $widget_option_name );
437
		unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
438
		update_option( $widget_option_name, $widget_settings );
439
	}
440
441
	/**
442
	 * Update a widget's settings, sidebar, and position. Returns the (updated)
443
	 * formatted widget if successful or a WP_Error if it fails.
444
	 *
445
	 * @param string $widget_id The id of a widget to update.
446
	 * @param string $sidebar (Optional) A sidebar to which this widget will be moved.
447
	 * @param string|integer (Optional) A new position to which this widget will be moved within its new or existing sidebar.
448
	 * @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
449
	 *
450
	 * @return array|WP_Error The newly added widget as an associative array with all the above properties.
451
	 */
452
	public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
453
		$settings = self::decode_settings( $settings );
454
		if ( isset( $settings ) && ! is_array( $settings ) ) {
455
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
456
		}
457
		// Default to an empty array if nothing is specified.
458
		if ( ! is_array( $settings ) ) {
459
			$settings = array();
460
		}
461
		$widget = self::get_widget_by_id( $widget_id );
462
		if ( ! $widget ) {
463
			return new WP_Error( 'not_found', 'No widget found.', 400 );
464
		}
465
		if ( ! $sidebar ) {
466
			$sidebar = $widget['sidebar'];
467
		}
468
		if ( ! isset( $position ) ) {
469
			$position = $widget['position'];
470
		}
471
		if ( ! is_numeric( $position ) ) {
472
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
473
		}
474
475
		// return if no widgets are present in the sidebar
476
		$widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
477
		if ( ! isset( $widgets_in_sidebar ) ) {
478
			return new WP_Error( 'invalid_data', 'No widgets in sidebar', 400 );
479
		}
480
481
		// call when placing widget in a new sidebar, else obsolete
482
		if ( ! self::is_widget_in_sidebar( $widgets_in_sidebar, $widget_id ) ) {
0 ignored issues
show
Documentation introduced by
$widgets_in_sidebar 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...
483
			self::move_widget_to_sidebar( $widget, $sidebar, $position );
484
		}
485
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
486
		if ( is_wp_error( $widget_save_status ) ) {
487
			return $widget_save_status;
488
		}
489
		return self::get_widget_by_id( $widget_id );
490
	}
491
492
	/**
493
	 * Find a widget in a list of all widgets retrieved from a sidebar
494
	 * using get_widgets_in_sidebar()
495
	 * @param string $widget The widget we want to look up in the sidebar
496
	 *
0 ignored issues
show
Documentation introduced by
There is no parameter named $widget. Did you maybe mean $widgets_all?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
497
	 * @param string $widgets_all the array of widget is' in a given sidebar
498
	 *
499
	 * @return bool Whether present.
500
	 */
501
502
	public static function is_widget_in_sidebar( $widgets_all, $widget_id ) {
503
		foreach ( $widgets_all as $widget_el_id ) {
0 ignored issues
show
Bug introduced by
The expression $widgets_all of type string is not traversable.
Loading history...
504
			if ( $widget_el_id  === $widget_id ) {
505
				return true;
506
			}
507
		}
508
		return false;
509
	}
510
	
511
	/**
512
	 * Deletes a widget entirely including all its settings. Returns a WP_Error if
513
	 * the widget could not be found. Otherwise returns an empty array.
514
	 *
515
	 * @param string $widget_id The id of a widget to delete. (eg: 'text-2')
516
	 *
517
	 * @return array|WP_Error An empty array if successful.
518
	 */
519
	public static function delete_widget( $widget_id ) {
520
		$widget = self::get_widget_by_id( $widget_id );
521
		if ( ! $widget ) {
522
			return new WP_Error( 'not_found', 'No widget found.', 400 );
523
		}
524
		self::remove_widget_from_sidebar( $widget );
525
		self::remove_widget_settings( $widget );
526
		return array();
527
	}
528
529
	/**
530
	 * Return an array of settings. The input can be either an object, a JSON
531
	 * string, or an array.
532
	 *
533
	 * @param array|string|object $settings The settings of a widget as passed into the API.
534
	 *
535
	 * @return array Decoded associative array of settings.
536
	 */
537
	public static function decode_settings( $settings ) {
538
		// Treat as string in case JSON was passed
539
		if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
540
			$settings = $settings->scalar;
541
		}
542
		if ( is_object( $settings ) ) {
543
			$settings = (array) $settings;
544
		}
545
		// Attempt to decode JSON string
546
		if ( is_string( $settings ) ) {
547
			$settings = (array) json_decode( $settings );
548
		}
549
		return $settings;
550
	}
551
552
	/**
553
	 * Activate a new widget.
554
	 *
555
	 * @param string $id_base The id_base of the new widget (eg: 'text')
556
	 * @param string $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1')
557
	 * @param string|integer $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
558
	 * @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
559
	 *
560
	 * @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'.
561
	 */
562
	public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
563
		if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
564
			return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
565
		}
566
567
		if ( ! isset( $sidebar ) ) {
568
			return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
569
		}
570
571
		if ( isset( $position ) && ! is_numeric( $position ) ) {
572
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
573
		}
574
575
		$settings = self::decode_settings( $settings );
576
		if ( isset( $settings ) && ! is_array( $settings ) ) {
577
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
578
		}
579
580
		// Default to an empty array if nothing is specified.
581
		if ( ! is_array( $settings ) ) {
582
			$settings = array();
583
		}
584
585
		$widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
586
		$widget_id = $id_base . '-' . $widget_counter;
587
		if ( 0 >= $widget_counter ) {
588
			return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
589
		}
590
		if ( self::get_widget_by_id( $widget_id ) ) {
591
			return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
592
		}
593
594
		self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
595
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
596
		if ( is_wp_error( $widget_save_status ) ) {
597
			return $widget_save_status;
598
		}
599
600
		// Add a Tracks event for non-Headstart activity.
601
		if ( ! defined( 'HEADSTART' ) ) {
602
			jetpack_require_lib( 'tracks/client' );
603
			jetpack_tracks_record_event( wp_get_current_user(), 'wpcom_widgets_activate_widget', array(
604
				'widget' => $id_base,
605
				'settings' => json_encode( $settings ),
606
			) );
607
		}
608
609
		return self::get_widget_by_id( $widget_id );
610
	}
611
612
	/**
613
	 * Activate an array of new widgets. Like calling `activate_widget` multiple times.
614
	 *
615
	 * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
616
	 *
617
	 * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
618
	 */
619
	public static function activate_widgets( $widgets ) {
620
		if ( ! is_array( $widgets ) ) {
621
			return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
622
		}
623
624
		$added_widgets = array();
625
626
		foreach( $widgets as $widget ) {
627
			$added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
628
		}
629
630
		return $added_widgets;
631
	}
632
633
	/**
634
	 * Return the last instance key (integer) of an existing widget matching
635
	 * `$id_base`. So if you pass in `text`, and there is a widget with the id
636
	 * `text-2`, this function will return `2`.
637
	 *
638
	 * @param string $id_base The id_base of a type of widget. (eg: 'rss')
639
	 *
640
	 * @return integer The last instance key of that type of widget.
641
	 */
642
	public static function get_last_widget_instance_key_with_id_base( $id_base ) {
643
		$similar_widgets = self::get_widgets_with_id_base( $id_base );
644
645
		if ( ! empty( $similar_widgets ) ) {
646
			// If the last widget with the same name is `text-3`, we want `text-4`
647
			usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
648
649
			$last_widget = array_pop( $similar_widgets );
650
			$last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
651
652
			return $last_val;
653
		}
654
655
		return 0;
656
	}
657
658
	/**
659
	 * Method used to sort widgets
660
	 *
661
	 * @since 5.4
662
	 *
663
	 * @param array $a
664
	 * @param array $b
665
	 *
666
	 * @return int
667
	 */
668
	public static function sort_widgets( $a, $b ) {
669
		$a_val = intval( self::get_widget_instance_key( $a['id'] ) );
670
		$b_val = intval( self::get_widget_instance_key( $b['id'] ) );
671
		if ( $a_val > $b_val ) {
672
			return 1;
673
		}
674
		if ( $a_val < $b_val ) {
675
			return -1;
676
		}
677
		return 0;
678
	}
679
680
	/**
681
	 * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
682
	 *
683
	 * @param string $id_base The id_base of a type of widget.
684
	 *
685
	 * @return WP_Widget|false The found widget object or false if the id_base was not found.
686
	 */
687
	public static function get_registered_widget_object( $id_base ) {
688
		if ( ! $id_base ) {
689
			return false;
690
		}
691
692
		// Get all of the registered widgets.
693
		global $wp_widget_factory;
694
		if ( ! isset( $wp_widget_factory ) ) {
695
			return false;
696
		}
697
698
		$registered_widgets = $wp_widget_factory->widgets;
699
		if ( empty( $registered_widgets ) ) {
700
			return false;
701
		}
702
703
		foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
704
			if ( $registered_widget_object->id_base === $id_base ) {
705
				return $registered_widget_object;
706
			}
707
		}
708
		return false;
709
	}
710
711
	/**
712
	 * Validate a given widget ID base (eg. 'text' or 'archives').
713
	 *
714
	 * @param string $id_base The id_base of a type of widget.
715
	 *
716
	 * @return boolean True if the widget is of a known type.
717
	 */
718
	public static function validate_id_base( $id_base ) {
719
		return ( false !== self::get_registered_widget_object( $id_base ) );
720
	}
721
}
722