Completed
Push — renovate/gridicons-3.x ( c004c1...f8ccd4 )
by
unknown
284:06 queued 275:32
created

_inc/lib/widgets.php (17 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 ) ) {
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
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 );
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
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
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
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
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
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
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
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
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
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
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
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->jetpack_tracks_record_event( wp_get_current_user(), 'wpcom_widgets_activate_widget', array(
0 ignored issues
show
The method jetpack_tracks_record_event() does not exist on Automattic\Jetpack\Tracking. Did you maybe mean tracks_record_event()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
562
				'widget' => $id_base,
563
				'settings' => json_encode( $settings ),
564
			) );
565
		}
566
567
		return self::get_widget_by_id( $widget_id );
568
	}
569
570
	/**
571
	 * Activate an array of new widgets. Like calling `activate_widget` multiple times.
572
	 *
573
	 * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
574
	 *
575
	 * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
576
	 */
577
	public static function activate_widgets( $widgets ) {
578
		if ( ! is_array( $widgets ) ) {
579
			return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
0 ignored issues
show
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...
580
		}
581
582
		$added_widgets = array();
583
584
		foreach( $widgets as $widget ) {
585
			$added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
586
		}
587
588
		return $added_widgets;
589
	}
590
591
	/**
592
	 * Return the last instance key (integer) of an existing widget matching
593
	 * `$id_base`. So if you pass in `text`, and there is a widget with the id
594
	 * `text-2`, this function will return `2`.
595
	 *
596
	 * @param string $id_base The id_base of a type of widget. (eg: 'rss')
597
	 *
598
	 * @return integer The last instance key of that type of widget.
599
	 */
600
	public static function get_last_widget_instance_key_with_id_base( $id_base ) {
601
		$similar_widgets = self::get_widgets_with_id_base( $id_base );
602
603
		if ( ! empty( $similar_widgets ) ) {
604
			// If the last widget with the same name is `text-3`, we want `text-4`
605
			usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
606
607
			$last_widget = array_pop( $similar_widgets );
608
			$last_val = intval( self::get_widget_instance_key( $last_widget['id'] ) );
609
610
			return $last_val;
611
		}
612
613
		return 0;
614
	}
615
616
	/**
617
	 * Method used to sort widgets
618
	 *
619
	 * @since 5.4
620
	 *
621
	 * @param array $a
622
	 * @param array $b
623
	 *
624
	 * @return int
625
	 */
626
	public static function sort_widgets( $a, $b ) {
627
		$a_val = intval( self::get_widget_instance_key( $a['id'] ) );
628
		$b_val = intval( self::get_widget_instance_key( $b['id'] ) );
629
		if ( $a_val > $b_val ) {
630
			return 1;
631
		}
632
		if ( $a_val < $b_val ) {
633
			return -1;
634
		}
635
		return 0;
636
	}
637
638
	/**
639
	 * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
640
	 *
641
	 * @param string $id_base The id_base of a type of widget.
642
	 *
643
	 * @return WP_Widget|false The found widget object or false if the id_base was not found.
644
	 */
645
	public static function get_registered_widget_object( $id_base ) {
646
		if ( ! $id_base ) {
647
			return false;
648
		}
649
650
		// Get all of the registered widgets.
651
		global $wp_widget_factory;
652
		if ( ! isset( $wp_widget_factory ) ) {
653
			return false;
654
		}
655
656
		$registered_widgets = $wp_widget_factory->widgets;
657
		if ( empty( $registered_widgets ) ) {
658
			return false;
659
		}
660
661
		foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
662
			if ( $registered_widget_object->id_base === $id_base ) {
663
				return $registered_widget_object;
664
			}
665
		}
666
		return false;
667
	}
668
669
	/**
670
	 * Validate a given widget ID base (eg. 'text' or 'archives').
671
	 *
672
	 * @param string $id_base The id_base of a type of widget.
673
	 *
674
	 * @return boolean True if the widget is of a known type.
675
	 */
676
	public static function validate_id_base( $id_base ) {
677
		return ( false !== self::get_registered_widget_object( $id_base ) );
678
	}
679
680
	/**
681
	 * Insert a new widget in a given sidebar.
682
	 *
683
	 * @param string $widget_id ID of the widget.
684
	 * @param array $widget_options Content of the widget.
685
 	 * @param string $sidebar ID of the sidebar to which the widget will be added.
686
 	 *
687
 	 * @return WP_Error|true True when data has been saved correctly, error otherwise.
688
	*/
689
	static function insert_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
690
		// Retrieve sidebars, widgets and their instances
691
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
692
		$widget_instances = get_option( 'widget_' . $widget_id, array() );
693
694
		// Retrieve the key of the next widget instance
695
		$numeric_keys = array_filter( array_keys( $widget_instances ), 'is_int' );
696
		$next_key = $numeric_keys ? max( $numeric_keys ) + 1 : 2;
697
698
		// Add this widget to the sidebar
699
		if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
700
			$sidebars_widgets[ $sidebar ] = array();
701
		}
702
		$sidebars_widgets[ $sidebar ][] = $widget_id . '-' . $next_key;
703
704
		// Add the new widget instance
705
		$widget_instances[ $next_key ] = $widget_options;
706
707
		// Store updated sidebars, widgets and their instances
708
		if (
709
			! ( update_option( 'sidebars_widgets', $sidebars_widgets ) )
710
			|| ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) )
711
		) {
712
			return new WP_Error( 'widget_update_failed', 'Failed to update widget or sidebar.', 400 );
0 ignored issues
show
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...
713
		};
714
715
		return true;
716
	}
717
718
	/**
719
	 * Update the content of an existing widget in a given sidebar.
720
	 *
721
	 * @param string $widget_id ID of the widget.
722
	 * @param array $widget_options New content for the update.
723
 	 * @param string $sidebar ID of the sidebar to which the widget will be added.
724
 	 *
725
 	 * @return WP_Error|true True when data has been updated correctly, error otherwise.
726
	*/
727
	static function update_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
728
		// Retrieve sidebars, widgets and their instances
729
		$sidebars_widgets = get_option( 'sidebars_widgets', array() );
730
		$widget_instances = get_option( 'widget_' . $widget_id, array() );
731
732
		// Retrieve index of first widget instance in that sidebar
733
		$widget_key = false;
734
		foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
735
			if ( strpos( $widget, $widget_id ) !== false ) {
736
				$widget_key = absint( str_replace( $widget_id . '-', '', $widget ) );
737
				break;
738
			}
739
		}
740
741
		// There is no widget instance
742
		if ( ! $widget_key ) {
743
			return new WP_Error( 'invalid_data', 'No such widget.', 400 );
0 ignored issues
show
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...
744
		}
745
746
		// Update the widget instance and option if the data has changed
747
		if ( $widget_instances[ $widget_key ]['title'] !== $widget_options['title']
748
			|| $widget_instances[ $widget_key ]['address'] !== $widget_options['address']
749
		) {
750
751
			$widget_instances[ $widget_key ] = array_merge( $widget_instances[ $widget_key ], $widget_options );
752
753
			// Store updated widget instances and return Error when not successful
754
			if ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) ) {
755
				return new WP_Error( 'widget_update_failed', 'Failed to update widget.', 400 );
0 ignored issues
show
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...
756
			};
757
		};
758
		return true;
759
	}
760
761
	/**
762
	 * Retrieve the first active sidebar.
763
	 *
764
	 * @return string|WP_Error First active sidebar, error if none exists.
765
	*/
766
	static function get_first_sidebar() {
767
		$active_sidebars = get_option( 'sidebars_widgets', array() );
768
		unset( $active_sidebars[ 'wp_inactive_widgets' ], $active_sidebars[ 'array_version' ] );
769
770
		if ( empty( $active_sidebars ) ) {
771
			return false;
772
		}
773
		$active_sidebars_keys = array_keys( $active_sidebars );
774
		return array_shift( $active_sidebars_keys );
775
	}
776
}
777