Completed
Push — fix/widget-extend-and-fix-on-u... ( c16fa1...04870a )
by
unknown
74:08 queued 65:00
created

Jetpack_Widgets::set_widget_settings()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 2
dl 0
loc 25
rs 8.439
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 ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
392
			return new WP_Error( 'invalid_data', 'Invalid ID base.', 400 );
393
		}
394
395
		// if the widget's content already exists
396
		if ( isset( $widget_settings[ $instance_key ] ) ) {
397
			$old_settings = $widget_settings[ $instance_key ];
398
399
			if ( ! $settings = self::sanitize_widget_settings( $widget_id, $settings, $old_settings ) ) {
400
				return new WP_Error( 'invalid_data', 'Update failed.', 400 );
401
			}
402
			if ( is_array( $old_settings ) ) {
403
				// array_filter prevents empty arguments from replacing existing ones
404
				$settings = wp_parse_args( array_filter( $settings ), $old_settings );
405
			}
406
		}
407
		$widget_settings[ $instance_key ] = $settings;
408
409
		return update_option( $widget_option_name, $widget_settings );
410
	}
411
412
	/**
413
	 * Sanitize an associative array for saving.
414
	 *
415
	 * @param string $widget_id The id of a widget.
416
	 * @param array $settings A widget settings array.
417
	 * @param array $old_settings The existing widget settings array.
418
	 *
419
	 * @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
420
	 */
421
	private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
422
		if ( ! $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) ) ) {
423
			return false;
424
		}
425
		$new_settings = $widget->update( $settings, $old_settings );
426
		if ( ! is_array( $new_settings ) ) {
427
			return false;
428
		}
429
		return $new_settings;
430
	}
431
432
	/**
433
	 * Deletes settings for a widget. Does not remove that widget to a sidebar. Please
434
	 * do that with `remove_widget_from_sidebar` first.
435
	 *
436
	 * @param array $widget The widget which will have its settings removed (see format_widget).
437
	 */
438
	public static function remove_widget_settings( $widget ) {
439
		$widget_option_name = self::get_widget_option_name( $widget['id'] );
440
		$widget_settings = get_option( $widget_option_name );
441
		unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
442
		update_option( $widget_option_name, $widget_settings );
443
	}
444
445
	/**
446
	 * Update a widget's settings, sidebar, and position. Returns the (updated)
447
	 * formatted widget if successful or a WP_Error if it fails.
448
	 *
449
	 * @param string $widget_id The id of a widget to update.
450
	 * @param string $sidebar (Optional) A sidebar to which this widget will be moved.
451
	 * @param string|integer (Optional) A new position to which this widget will be moved within its new or existing sidebar.
452
	 * @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
453
	 *
454
	 * @return array|WP_Error The newly added widget as an associative array with all the above properties.
455
	 */
456
	public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
457
		$settings = self::decode_settings( $settings );
458
		if ( isset( $settings ) && ! is_array( $settings ) ) {
459
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
460
		}
461
		// Default to an empty array if nothing is specified.
462
		if ( ! is_array( $settings ) ) {
463
			$settings = array();
464
		}
465
		$widget = self::get_widget_by_id( $widget_id );
466
		if ( ! $widget ) {
467
			return new WP_Error( 'not_found', 'No widget found.', 400 );
468
		}
469
		if ( ! $sidebar ) {
470
			$sidebar = $widget['sidebar'];
471
		}
472
		if ( ! isset( $position ) ) {
473
			$position = $widget['position'];
474
		}
475
		if ( ! is_numeric( $position ) ) {
476
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
477
		}
478
479
		// return if no widgets are present in the sidebar
480
		$widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
481
		if ( ! isset( $widgets_in_sidebar ) ) {
482
			return new WP_Error( 'invalid_data', 'No widgets in sidebar', 400 );
483
		}
484
485
		// call when placing widget in a new sidebar, else obsolete
486
		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...
487
			self::move_widget_to_sidebar( $widget, $sidebar, $position );
488
		}
489
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
490
		if ( is_wp_error( $widget_save_status ) ) {
491
			return $widget_save_status;
492
		}
493
		return self::get_widget_by_id( $widget_id );
494
	}
495
496
	/**
497
	 * Retrieve the first active sidebar
498
	 *
499
	 * @return array|false Active sidebar or false if none exists
500
	 */
501
	public function get_first_sidebar() {
502
		$active_sidebars = Jetpack_Widgets::get_active_sidebars();
503
504
		unset( $active_sidebars[ 'array_version' ] );
505
506
		if ( empty( $active_sidebars ) ) {
507
			return false;
508
		}
509
		return array_shift( array_keys( $active_sidebars ) );
0 ignored issues
show
Bug introduced by
array_keys($active_sidebars) cannot be passed to array_shift() as the parameter $array expects a reference.
Loading history...
510
	}
511
512
	/**
513
	 * Find a widget in a list of all widgets retrieved from a sidebar
514
	 * using get_widgets_in_sidebar()
515
	 * @param string $widget The widget we want to look up in the sidebar
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...
516
	 *
517
	 * @param string $widgets_all the array of widget is' in a given sidebar
518
	 *
519
	 * @return bool Whether present.
520
	 */
521
	public static function is_widget_in_sidebar( $widgets_all, $widget_id ) {
522
		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...
523
			if ( $widget_el_id  === $widget_id ) {
524
				return true;
525
			}
526
		}
527
		return false;
528
	}
529
530
	/**
531
	 * Deletes a widget entirely including all its settings. Returns a WP_Error if
532
	 * the widget could not be found. Otherwise returns an empty array.
533
	 *
534
	 * @param string $widget_id The id of a widget to delete. (eg: 'text-2')
535
	 *
536
	 * @return array|WP_Error An empty array if successful.
537
	 */
538
	public static function delete_widget( $widget_id ) {
539
		$widget = self::get_widget_by_id( $widget_id );
540
		if ( ! $widget ) {
541
			return new WP_Error( 'not_found', 'No widget found.', 400 );
542
		}
543
		self::remove_widget_from_sidebar( $widget );
544
		self::remove_widget_settings( $widget );
545
		return array();
546
	}
547
548
	/**
549
	 * Return an array of settings. The input can be either an object, a JSON
550
	 * string, or an array.
551
	 *
552
	 * @param array|string|object $settings The settings of a widget as passed into the API.
553
	 *
554
	 * @return array Decoded associative array of settings.
555
	 */
556
	public static function decode_settings( $settings ) {
557
		// Treat as string in case JSON was passed
558
		if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
559
			$settings = $settings->scalar;
560
		}
561
		if ( is_object( $settings ) ) {
562
			$settings = (array) $settings;
563
		}
564
		// Attempt to decode JSON string
565
		if ( is_string( $settings ) ) {
566
			$settings = (array) json_decode( $settings );
567
		}
568
		return $settings;
569
	}
570
571
	/**
572
	 * Activate a new widget.
573
	 *
574
	 * @param string $id_base The id_base of the new widget (eg: 'text')
575
	 * @param string $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1')
576
	 * @param string|integer $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
577
	 * @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
578
	 *
579
	 * @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'.
580
	 */
581
	public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
582
		if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
583
			return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
584
		}
585
586
		if ( ! isset( $sidebar ) ) {
587
			return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
588
		}
589
590
		if ( isset( $position ) && ! is_numeric( $position ) ) {
591
			return new WP_Error( 'invalid_data', 'Invalid position', 400 );
592
		}
593
594
		$settings = self::decode_settings( $settings );
595
		if ( isset( $settings ) && ! is_array( $settings ) ) {
596
			return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
597
		}
598
599
		// Default to an empty array if nothing is specified.
600
		if ( ! is_array( $settings ) ) {
601
			$settings = array();
602
		}
603
604
		$widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
605
		$widget_id = $id_base . '-' . $widget_counter;
606
		if ( 0 >= $widget_counter ) {
607
			return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
608
		}
609
		if ( self::get_widget_by_id( $widget_id ) ) {
610
			return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
611
		}
612
613
		self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
614
		$widget_save_status = self::set_widget_settings( $widget_id, $settings );
615
		if ( is_wp_error( $widget_save_status ) ) {
616
			return $widget_save_status;
617
		}
618
619
		// Add a Tracks event for non-Headstart activity.
620
		if ( ! defined( 'HEADSTART' ) ) {
621
			jetpack_require_lib( 'tracks/client' );
622
			jetpack_tracks_record_event( wp_get_current_user(), 'wpcom_widgets_activate_widget', array(
623
				'widget' => $id_base,
624
				'settings' => json_encode( $settings ),
625
			) );
626
		}
627
628
		return self::get_widget_by_id( $widget_id );
629
	}
630
631
	/**
632
	 * Activate an array of new widgets. Like calling `activate_widget` multiple times.
633
	 *
634
	 * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
635
	 *
636
	 * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
637
	 */
638
	public static function activate_widgets( $widgets ) {
639
		if ( ! is_array( $widgets ) ) {
640
			return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
641
		}
642
643
		$added_widgets = array();
644
645
		foreach( $widgets as $widget ) {
646
			$added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
647
		}
648
649
		return $added_widgets;
650
	}
651
652
	/**
653
	 * Return the last instance key (integer) of an existing widget matching
654
	 * `$id_base`. So if you pass in `text`, and there is a widget with the id
655
	 * `text-2`, this function will return `2`.
656
	 *
657
	 * @param string $id_base The id_base of a type of widget. (eg: 'rss')
658
	 *
659
	 * @return integer The last instance key of that type of widget.
660
	 */
661
	public static function get_last_widget_instance_key_with_id_base( $id_base ) {
662
		$similar_widgets = self::get_widgets_with_id_base( $id_base );
663
664
		if ( ! empty( $similar_widgets ) ) {
665
			// If the last widget with the same name is `text-3`, we want `text-4`
666
			usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
667
668
			$last_widget = array_pop( $similar_widgets );
669
			$last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
670
671
			return $last_val;
672
		}
673
674
		return 0;
675
	}
676
677
	/**
678
	 * Method used to sort widgets
679
	 *
680
	 * @since 5.4
681
	 *
682
	 * @param array $a
683
	 * @param array $b
684
	 *
685
	 * @return int
686
	 */
687
	public static function sort_widgets( $a, $b ) {
688
		$a_val = intval( self::get_widget_instance_key( $a['id'] ) );
689
		$b_val = intval( self::get_widget_instance_key( $b['id'] ) );
690
		if ( $a_val > $b_val ) {
691
			return 1;
692
		}
693
		if ( $a_val < $b_val ) {
694
			return -1;
695
		}
696
		return 0;
697
	}
698
699
	/**
700
	 * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
701
	 *
702
	 * @param string $id_base The id_base of a type of widget.
703
	 *
704
	 * @return WP_Widget|false The found widget object or false if the id_base was not found.
705
	 */
706
	public static function get_registered_widget_object( $id_base ) {
707
		if ( ! $id_base ) {
708
			return false;
709
		}
710
711
		// Get all of the registered widgets.
712
		global $wp_widget_factory;
713
		if ( ! isset( $wp_widget_factory ) ) {
714
			return false;
715
		}
716
717
		$registered_widgets = $wp_widget_factory->widgets;
718
		if ( empty( $registered_widgets ) ) {
719
			return false;
720
		}
721
722
		foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
723
			if ( $registered_widget_object->id_base === $id_base ) {
724
				return $registered_widget_object;
725
			}
726
		}
727
		return false;
728
	}
729
730
	/**
731
	 * Validate a given widget ID base (eg. 'text' or 'archives').
732
	 *
733
	 * @param string $id_base The id_base of a type of widget.
734
	 *
735
	 * @return boolean True if the widget is of a known type.
736
	 */
737
	public static function validate_id_base( $id_base ) {
738
		return ( false !== self::get_registered_widget_object( $id_base ) );
739
	}
740
}
741