Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 253
0.00% covered (danger)
0.00%
0 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Widgets
0.00% covered (danger)
0.00%
0 / 253
0.00% covered (danger)
0.00%
0 / 32
13340
0.00% covered (danger)
0.00%
0 / 1
 get_sidebars_widgets
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 format_widget
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 get_widget_id_base
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_widget_option_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_widget_instance_key
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_widget_by_id
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_all_widgets
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 get_active_widgets
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 get_all_widget_ids
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 get_widgets_with_id_base
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_widgets_in_sidebar
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 get_all_sidebars
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_active_sidebars
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 add_widget_to_sidebar
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove_widget_from_sidebar
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 move_widget_to_sidebar
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 get_last_position_in_sidebar
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 set_widget_settings
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 sanitize_widget_settings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 remove_widget_settings
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 update_widget
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 delete_widget
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 decode_settings
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 activate_widget
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
182
 activate_widgets
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_last_widget_instance_key_with_id_base
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 sort_widgets
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_registered_widget_object
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 validate_id_base
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 insert_widget_in_sidebar
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 update_widget_in_sidebar
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 get_first_sidebar
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
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 * @package automattic/jetpack
11 */
12
13/**
14 * Widgets and Sidebars Library
15 */
16class Jetpack_Widgets {
17
18    /**
19     * Returns the `sidebars_widgets` option with the `array_version` element removed.
20     *
21     * @return array The current value of sidebars_widgets
22     */
23    public static function get_sidebars_widgets() {
24        $sidebars = get_option( 'sidebars_widgets', array() );
25        if ( isset( $sidebars['array_version'] ) ) {
26            unset( $sidebars['array_version'] );
27        }
28        return $sidebars;
29    }
30
31    /**
32     * Format widget data for output and for use by other widget functions.
33     *
34     * The output looks like:
35     *
36     * array(
37     *  'id' => 'text-3',
38     *  'sidebar' => 'sidebar-1',
39     *  'position' => '0',
40     *  'settings' => array(
41     *      'title' => 'hello world'
42     *  )
43     * )
44     *
45     * @param string|integer $position The position of the widget in its sidebar.
46     * @param string         $widget_id The widget's id (eg: 'text-3').
47     * @param string         $sidebar The widget's sidebar id (eg: 'sidebar-1').
48     * @param array|null     $settings The settings for the widget.
49     *
50     * @return array A normalized array representing this widget.
51     */
52    public static function format_widget( $position, $widget_id, $sidebar, $settings = null ) {
53        if ( ! $settings ) {
54            $all_settings = get_option( self::get_widget_option_name( $widget_id ) );
55            $instance     = self::get_widget_instance_key( $widget_id );
56            $settings     = $all_settings[ $instance ];
57        }
58        $widget = array();
59
60        $widget['id']       = $widget_id;
61        $widget['id_base']  = self::get_widget_id_base( $widget_id );
62        $widget['settings'] = $settings;
63        $widget['sidebar']  = $sidebar;
64        $widget['position'] = $position;
65
66        return $widget;
67    }
68
69    /**
70     * Return a widget's id_base from its id.
71     *
72     * @param string $widget_id The id of a widget. (eg: 'text-3').
73     *
74     * @return string The id_base of a widget (eg: 'text').
75     */
76    public static function get_widget_id_base( $widget_id ) {
77        // Grab what's before the hyphen.
78        return substr( $widget_id, 0, strrpos( $widget_id, '-' ) );
79    }
80
81    /**
82     * Determine a widget's option name (the WP option where the widget's settings
83     * are stored - generally `widget_` + the widget's id_base).
84     *
85     * @param string $widget_id The id of a widget. (eg: 'text-3').
86     *
87     * @return string The option name of the widget's settings. (eg: 'widget_text')
88     */
89    public static function get_widget_option_name( $widget_id ) {
90        return 'widget_' . self::get_widget_id_base( $widget_id );
91    }
92
93    /**
94     * Determine a widget instance key from its ID. (eg: 'text-3' becomes '3').
95     * Used to access the widget's settings.
96     *
97     * @param string $widget_id The id of a widget.
98     *
99     * @return integer The instance key of that widget.
100     */
101    public static function get_widget_instance_key( $widget_id ) {
102        // Grab all numbers from the end of the id.
103        preg_match( '/(\d+)$/', $widget_id, $matches );
104
105        return (int) $matches[0];
106    }
107
108    /**
109     * Return a widget by ID (formatted for output) or null if nothing is found.
110     *
111     * @param string $widget_id The id of a widget to look for.
112     *
113     * @return array|null The matching formatted widget (see format_widget).
114     */
115    public static function get_widget_by_id( $widget_id ) {
116        $found = null;
117        foreach ( self::get_all_widgets() as $widget ) {
118            if ( $widget['id'] === $widget_id ) {
119                $found = $widget;
120            }
121        }
122        return $found;
123    }
124
125    /**
126     * Return an array of all widgets (active and inactive) formatted for output.
127     *
128     * @return array An array of all widgets (see format_widget).
129     */
130    public static function get_all_widgets() {
131        $all_widgets      = array();
132        $sidebars_widgets = self::get_all_sidebars();
133
134        foreach ( $sidebars_widgets as $sidebar => $widgets ) {
135            if ( ! is_array( $widgets ) ) {
136                continue;
137            }
138            foreach ( $widgets as $key => $widget_id ) {
139                array_push( $all_widgets, self::format_widget( $key, $widget_id, $sidebar ) );
140            }
141        }
142
143        return $all_widgets;
144    }
145
146    /**
147     * Return an array of all active widgets formatted for output.
148     *
149     * @return array An array of all active widgets (see format_widget).
150     */
151    public static function get_active_widgets() {
152        $active_widgets = array();
153        $all_widgets    = self::get_all_widgets();
154        foreach ( $all_widgets as $widget ) {
155            if ( 'wp_inactive_widgets' === $widget['sidebar'] ) {
156                continue;
157            }
158            array_push( $active_widgets, $widget );
159        }
160        return $active_widgets;
161    }
162
163    /**
164     * Return an array of all widget IDs (active and inactive)
165     *
166     * @return array An array of all widget IDs.
167     */
168    public static function get_all_widget_ids() {
169        $all_widgets      = array();
170        $sidebars_widgets = self::get_all_sidebars();
171        foreach ( array_values( $sidebars_widgets ) as $widgets ) {
172            if ( ! is_array( $widgets ) ) {
173                continue;
174            }
175            foreach ( array_values( $widgets ) as $widget_id ) {
176                array_push( $all_widgets, $widget_id );
177            }
178        }
179        return $all_widgets;
180    }
181
182    /**
183     * Return an array of widgets with a specific id_base (eg: `text`).
184     *
185     * @param string $id_base The id_base of a widget type.
186     *
187     * @return array All the formatted widgets matching that widget type (see format_widget).
188     */
189    public static function get_widgets_with_id_base( $id_base ) {
190        $matching_widgets = array();
191        foreach ( self::get_all_widgets() as $widget ) {
192            if ( self::get_widget_id_base( $widget['id'] ) === $id_base ) {
193                array_push( $matching_widgets, $widget );
194            }
195        }
196        return $matching_widgets;
197    }
198
199    /**
200     * Return the array of widget IDs in a sidebar or null if that sidebar does
201     * not exist. Will return an empty array for an existing empty sidebar.
202     *
203     * @param string $sidebar The id of a sidebar.
204     *
205     * @return array|null The array of widget IDs in the sidebar.
206     */
207    public static function get_widgets_in_sidebar( $sidebar ) {
208        $sidebars = self::get_all_sidebars();
209
210        if ( ! $sidebars || ! is_array( $sidebars ) ) {
211            return null;
212        }
213        if ( ! $sidebars[ $sidebar ] && array_key_exists( $sidebar, $sidebars ) ) {
214            return array();
215        }
216        return $sidebars[ $sidebar ];
217    }
218
219    /**
220     * Return an associative array of all registered sidebars for this theme,
221     * active and inactive, including the hidden disabled widgets sidebar (keyed
222     * by `wp_inactive_widgets`). Each sidebar is keyed by the ID of the sidebar
223     * and its value is an array of widget IDs for that sidebar.
224     *
225     * @return array An associative array of all sidebars and their widget IDs.
226     */
227    public static function get_all_sidebars() {
228        $sidebars_widgets = self::get_sidebars_widgets();
229
230        if ( ! is_array( $sidebars_widgets ) ) {
231            return array();
232        }
233        return $sidebars_widgets;
234    }
235
236    /**
237     * Return an associative array of all active sidebars for this theme, Each
238     * sidebar is keyed by the ID of the sidebar and its value is an array of
239     * widget IDs for that sidebar.
240     *
241     * @return array An associative array of all active sidebars and their widget IDs.
242     */
243    public static function get_active_sidebars() {
244        $sidebars = array();
245        foreach ( self::get_all_sidebars() as $sidebar => $widgets ) {
246            if ( 'wp_inactive_widgets' === $sidebar || ! isset( $widgets ) || ! is_array( $widgets ) ) {
247                continue;
248            }
249            $sidebars[ $sidebar ] = $widgets;
250        }
251        return $sidebars;
252    }
253
254    /**
255     * Activates a widget in a sidebar. Does not validate that the sidebar exists,
256     * so please do that first. Also does not save the widget's settings. Please
257     * do that with `set_widget_settings`.
258     *
259     * If position is not set, it will be set to the next available position.
260     *
261     * @param string         $widget_id The newly-formed id of the widget to be added.
262     * @param string         $sidebar   The id of the sidebar where the widget will be added.
263     * @param string|integer $position  (Optional) The position within the sidebar where the widget will be added.
264     *
265     * @return bool
266     */
267    public static function add_widget_to_sidebar( $widget_id, $sidebar, $position ) {
268        return self::move_widget_to_sidebar( array( 'id' => $widget_id ), $sidebar, $position );
269    }
270
271    /**
272     * Removes a widget from a sidebar. Does not validate that the sidebar exists
273     * or remove any settings from the widget, so please do that separately.
274     *
275     * @param array $widget The widget to be removed.
276     */
277    public static function remove_widget_from_sidebar( $widget ) {
278        $sidebars_widgets = self::get_sidebars_widgets();
279        // Remove the widget from its old location and reflow the positions of the remaining widgets.
280        array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
281
282        update_option( 'sidebars_widgets', $sidebars_widgets );
283    }
284
285    /**
286     * Moves a widget to a sidebar. Does not validate that the sidebar exists,
287     * so please do that first. Also does not save the widget's settings. Please
288     * do that with `set_widget_settings`. The first argument should be a
289     * widget as returned by `format_widget` including `id`, `sidebar`, and
290     * `position`.
291     *
292     * If $position is not set, it will be set to the next available position.
293     *
294     * Can be used to add a new widget to a sidebar if
295     * $widget['sidebar'] === NULL
296     *
297     * Can be used to move a widget within a sidebar as well if
298     * $widget['sidebar'] === $sidebar.
299     *
300     * @param array          $widget   The widget to be moved (see format_widget).
301     * @param string         $sidebar  The sidebar where this widget will be moved.
302     * @param string|integer $position (Optional) The position where this widget will be moved in the sidebar.
303     *
304     * @return bool
305     */
306    public static function move_widget_to_sidebar( $widget, $sidebar, $position ) {
307        $sidebars_widgets = self::get_sidebars_widgets();
308
309        /*
310         * If a position is passed and the sidebar isn't empty,
311         * splice the widget into the sidebar,
312         * update the sidebar option, and return the result.
313         */
314        if ( isset( $widget['sidebar'] ) && isset( $widget['position'] ) ) {
315            array_splice( $sidebars_widgets[ $widget['sidebar'] ], $widget['position'], 1 );
316        }
317
318        // Sometimes an existing empty sidebar is NULL, so initialize it.
319        if ( array_key_exists( $sidebar, $sidebars_widgets ) && ! is_array( $sidebars_widgets[ $sidebar ] ) ) {
320            $sidebars_widgets[ $sidebar ] = array();
321        }
322
323        // If no position is passed, set one from items in sidebar.
324        if ( ! isset( $position ) ) {
325            $position      = 0;
326            $last_position = self::get_last_position_in_sidebar( $sidebar );
327            if ( isset( $last_position ) && is_numeric( $last_position ) ) {
328                $position = $last_position + 1;
329            }
330        }
331
332        // Add the widget to the sidebar and reflow the positions of the other widgets.
333        if ( empty( $sidebars_widgets[ $sidebar ] ) ) {
334            $sidebars_widgets[ $sidebar ][] = $widget['id'];
335        } else {
336            array_splice( $sidebars_widgets[ $sidebar ], (int) $position, 0, $widget['id'] );
337        }
338
339        set_theme_mod(
340            'sidebars_widgets',
341            array(
342                'time' => time(),
343                'data' => $sidebars_widgets,
344            )
345        );
346        return update_option( 'sidebars_widgets', $sidebars_widgets );
347    }
348
349    /**
350     * Return an integer containing the largest position number in a sidebar or
351     * null if there are no widgets in that sidebar.
352     *
353     * @param string $sidebar The id of a sidebar.
354     *
355     * @return integer|null The last index position of a widget in that sidebar.
356     */
357    public static function get_last_position_in_sidebar( $sidebar ) {
358        $widgets = self::get_widgets_in_sidebar( $sidebar );
359        if ( ! $widgets ) {
360            return null;
361        }
362        $last_position = 0;
363        foreach ( $widgets as $widget_id ) {
364            $widget = self::get_widget_by_id( $widget_id );
365            if ( (int) $widget['position'] > $last_position ) {
366                $last_position = (int) $widget['position'];
367            }
368        }
369        return $last_position;
370    }
371
372    /**
373     * Saves settings for a widget. Does not add that widget to a sidebar. Please
374     * do that with `move_widget_to_sidebar` first. Will merge the settings of
375     * any existing widget with the same `$widget_id`.
376     *
377     * @param string $widget_id The id of a widget.
378     * @param array  $settings An associative array of settings to merge with any existing settings on this widget.
379     *
380     * @return boolean|WP_Error True if update was successful.
381     */
382    public static function set_widget_settings( $widget_id, $settings ) {
383        $widget_option_name = self::get_widget_option_name( $widget_id );
384        $widget_settings    = get_option( $widget_option_name );
385        $instance_key       = self::get_widget_instance_key( $widget_id );
386        $old_settings       = $widget_settings[ $instance_key ];
387        $settings           = self::sanitize_widget_settings( $widget_id, $settings, $old_settings );
388
389        if ( ! $settings ) {
390            return new WP_Error( 'invalid_data', 'Update failed.', 500 );
391        }
392        if ( is_array( $old_settings ) ) {
393            // array_filter prevents empty arguments from replacing existing ones.
394            $settings = wp_parse_args( array_filter( $settings ), $old_settings );
395        }
396
397        $widget_settings[ $instance_key ] = $settings;
398
399        return update_option( $widget_option_name, $widget_settings );
400    }
401
402    /**
403     * Sanitize an associative array for saving.
404     *
405     * @param string $widget_id The id of a widget.
406     * @param array  $settings A widget settings array.
407     * @param array  $old_settings The existing widget settings array.
408     *
409     * @return array|false The settings array sanitized by `WP_Widget::update` or false if sanitization failed.
410     */
411    private static function sanitize_widget_settings( $widget_id, $settings, $old_settings ) {
412        $widget = self::get_registered_widget_object( self::get_widget_id_base( $widget_id ) );
413
414        if ( ! $widget ) {
415            return false;
416        }
417        $new_settings = $widget->update( $settings, $old_settings );
418        if ( ! is_array( $new_settings ) ) {
419            return false;
420        }
421        return $new_settings;
422    }
423
424    /**
425     * Deletes settings for a widget. Does not remove that widget to a sidebar. Please
426     * do that with `remove_widget_from_sidebar` first.
427     *
428     * @param array $widget The widget which will have its settings removed (see format_widget).
429     */
430    public static function remove_widget_settings( $widget ) {
431        $widget_option_name = self::get_widget_option_name( $widget['id'] );
432        $widget_settings    = get_option( $widget_option_name );
433        unset( $widget_settings[ self::get_widget_instance_key( $widget['id'] ) ] );
434        update_option( $widget_option_name, $widget_settings );
435    }
436
437    /**
438     * Update a widget's settings, sidebar, and position. Returns the (updated)
439     * formatted widget if successful or a WP_Error if it fails.
440     *
441     * @param string              $widget_id The id of a widget to update.
442     * @param string              $sidebar (Optional) A sidebar to which this widget will be moved.
443     * @param string|integer      $position (Optional) A new position to which this widget will be moved within its new or existing sidebar.
444     * @param array|object|string $settings Settings to merge with the existing settings of the widget (will be passed through `decode_settings`).
445     *
446     * @return array|WP_Error The newly added widget as an associative array with all the above properties.
447     */
448    public static function update_widget( $widget_id, $sidebar, $position, $settings ) {
449        $settings = self::decode_settings( $settings );
450        if ( isset( $settings ) && ! is_array( $settings ) ) {
451            return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
452        }
453        // Default to an empty array if nothing is specified.
454        if ( ! is_array( $settings ) ) {
455            $settings = array();
456        }
457        $widget = self::get_widget_by_id( $widget_id );
458        if ( ! $widget ) {
459            return new WP_Error( 'not_found', 'No widget found.', 400 );
460        }
461        if ( ! $sidebar ) {
462            $sidebar = $widget['sidebar'];
463        }
464        if ( ! isset( $position ) ) {
465            $position = $widget['position'];
466        }
467        if ( ! is_numeric( $position ) ) {
468            return new WP_Error( 'invalid_data', 'Invalid position', 400 );
469        }
470        $widgets_in_sidebar = self::get_widgets_in_sidebar( $sidebar );
471        if ( ! isset( $widgets_in_sidebar ) ) {
472            return new WP_Error( 'invalid_data', 'No such sidebar exists', 400 );
473        }
474        self::move_widget_to_sidebar( $widget, $sidebar, $position );
475        $widget_save_status = self::set_widget_settings( $widget_id, $settings );
476        if ( is_wp_error( $widget_save_status ) ) {
477            return $widget_save_status;
478        }
479        return self::get_widget_by_id( $widget_id );
480    }
481
482    /**
483     * Deletes a widget entirely including all its settings. Returns a WP_Error if
484     * the widget could not be found. Otherwise returns an empty array.
485     *
486     * @param string $widget_id The id of a widget to delete. (eg: 'text-2').
487     *
488     * @return array|WP_Error An empty array if successful.
489     */
490    public static function delete_widget( $widget_id ) {
491        $widget = self::get_widget_by_id( $widget_id );
492        if ( ! $widget ) {
493            return new WP_Error( 'not_found', 'No widget found.', 400 );
494        }
495        self::remove_widget_from_sidebar( $widget );
496        self::remove_widget_settings( $widget );
497        return array();
498    }
499
500    /**
501     * Return an array of settings. The input can be either an object, a JSON
502     * string, or an array.
503     *
504     * @param array|string|object $settings The settings of a widget as passed into the API.
505     *
506     * @return array Decoded associative array of settings.
507     */
508    public static function decode_settings( $settings ) {
509        // Treat as string in case JSON was passed.
510        if ( is_object( $settings ) && property_exists( $settings, 'scalar' ) ) {
511            $settings = $settings->scalar;
512        }
513        if ( is_object( $settings ) ) {
514            $settings = (array) $settings;
515        }
516        // Attempt to decode JSON string.
517        if ( is_string( $settings ) ) {
518            $settings = (array) json_decode( $settings );
519        }
520        return $settings;
521    }
522
523    /**
524     * Activate a new widget.
525     *
526     * @param string              $id_base The id_base of the new widget (eg: 'text').
527     * @param string              $sidebar The id of the sidebar where this widget will go. Dependent on theme. (eg: 'sidebar-1').
528     * @param string|integer      $position (Optional) The position of the widget in the sidebar. Defaults to the last position.
529     * @param array|object|string $settings (Optional) An associative array of settings for this widget (will be passed through `decode_settings`). Varies by widget.
530     *
531     * @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'.
532     */
533    public static function activate_widget( $id_base, $sidebar, $position, $settings ) {
534        if ( ! isset( $id_base ) || ! self::validate_id_base( $id_base ) ) {
535            return new WP_Error( 'invalid_data', 'Invalid ID base', 400 );
536        }
537
538        if ( ! isset( $sidebar ) ) {
539            return new WP_Error( 'invalid_data', 'No sidebar provided', 400 );
540        }
541
542        if ( isset( $position ) && ! is_numeric( $position ) ) {
543            return new WP_Error( 'invalid_data', 'Invalid position', 400 );
544        }
545
546        $settings = self::decode_settings( $settings );
547        if ( isset( $settings ) && ! is_array( $settings ) ) {
548            return new WP_Error( 'invalid_data', 'Invalid settings', 400 );
549        }
550
551        // Default to an empty array if nothing is specified.
552        if ( ! is_array( $settings ) ) {
553            $settings = array();
554        }
555
556        $widget_counter = 1 + self::get_last_widget_instance_key_with_id_base( $id_base );
557        $widget_id      = $id_base . '-' . $widget_counter;
558        if ( 0 >= $widget_counter ) {
559            return new WP_Error( 'invalid_data', 'Error creating widget ID' . $widget_id, 500 );
560        }
561        if ( self::get_widget_by_id( $widget_id ) ) {
562            return new WP_Error( 'invalid_data', 'Widget ID already exists', 500 );
563        }
564
565        self::add_widget_to_sidebar( $widget_id, $sidebar, $position );
566        $widget_save_status = self::set_widget_settings( $widget_id, $settings );
567        if ( is_wp_error( $widget_save_status ) ) {
568            return $widget_save_status;
569        }
570
571        // Add a Tracks event for non-Headstart activity.
572        if ( ! defined( 'HEADSTART' ) ) {
573            $tracking = new Automattic\Jetpack\Tracking();
574            $tracking->tracks_record_event(
575                wp_get_current_user(),
576                'wpcom_widgets_activate_widget',
577                array(
578                    'widget'   => $id_base,
579                    'settings' => wp_json_encode( $settings ),
580                )
581            );
582        }
583
584        return self::get_widget_by_id( $widget_id );
585    }
586
587    /**
588     * Activate an array of new widgets. Like calling `activate_widget` multiple times.
589     *
590     * @param array $widgets An array of widget arrays. Each sub-array must be of the format required by `activate_widget`.
591     *
592     * @return array|WP_Error The newly added widgets in the form returned by `get_all_widgets`.
593     */
594    public static function activate_widgets( $widgets ) {
595        if ( ! is_array( $widgets ) ) {
596            return new WP_Error( 'invalid_data', 'Invalid widgets', 400 );
597        }
598
599        $added_widgets = array();
600
601        foreach ( $widgets as $widget ) {
602            $added_widgets[] = self::activate_widget( $widget['id_base'], $widget['sidebar'], $widget['position'], $widget['settings'] );
603        }
604
605        return $added_widgets;
606    }
607
608    /**
609     * Return the last instance key (integer) of an existing widget matching
610     * `$id_base`. So if you pass in `text`, and there is a widget with the id
611     * `text-2`, this function will return `2`.
612     *
613     * @param string $id_base The id_base of a type of widget. (eg: 'rss').
614     *
615     * @return integer The last instance key of that type of widget.
616     */
617    public static function get_last_widget_instance_key_with_id_base( $id_base ) {
618        $similar_widgets = self::get_widgets_with_id_base( $id_base );
619
620        if ( ! empty( $similar_widgets ) ) {
621            // If the last widget with the same name is `text-3`, we want `text-4`.
622            usort( $similar_widgets, __CLASS__ . '::sort_widgets' );
623
624            $last_widget = array_pop( $similar_widgets );
625            $last_val    = (int) self::get_widget_instance_key( $last_widget['id'] );
626
627            return $last_val;
628        }
629
630        return 0;
631    }
632
633    /**
634     * Method used to sort widgets
635     *
636     * @since 5.4
637     *
638     * @param array $a A normalized array representing a widget.
639     * @param array $b A normalized array representing a widget.
640     *
641     * @return int
642     */
643    public static function sort_widgets( $a, $b ) {
644        $a_val = (int) self::get_widget_instance_key( $a['id'] );
645        $b_val = (int) self::get_widget_instance_key( $b['id'] );
646        return $a_val <=> $b_val;
647    }
648
649    /**
650     * Retrieve a given widget object instance by ID base (eg. 'text' or 'archives').
651     *
652     * @param string $id_base The id_base of a type of widget.
653     *
654     * @return WP_Widget|false The found widget object or false if the id_base was not found.
655     */
656    public static function get_registered_widget_object( $id_base ) {
657        if ( ! $id_base ) {
658            return false;
659        }
660
661        // Get all of the registered widgets.
662        global $wp_widget_factory;
663        if ( ! isset( $wp_widget_factory ) ) {
664            return false;
665        }
666
667        $registered_widgets = $wp_widget_factory->widgets;
668        if ( empty( $registered_widgets ) ) {
669            return false;
670        }
671
672        foreach ( array_values( $registered_widgets ) as $registered_widget_object ) {
673            if ( $registered_widget_object->id_base === $id_base ) {
674                return $registered_widget_object;
675            }
676        }
677        return false;
678    }
679
680    /**
681     * Validate a given widget ID base (eg. 'text' or 'archives').
682     *
683     * @param string $id_base The id_base of a type of widget.
684     *
685     * @return boolean True if the widget is of a known type.
686     */
687    public static function validate_id_base( $id_base ) {
688        return ( false !== self::get_registered_widget_object( $id_base ) );
689    }
690
691    /**
692     * Insert a new widget in a given sidebar.
693     *
694     * @param string $widget_id ID of the widget.
695     * @param array  $widget_options Content of the widget.
696     * @param string $sidebar ID of the sidebar to which the widget will be added.
697     *
698     * @return WP_Error|true True when data has been saved correctly, error otherwise.
699     */
700    public static function insert_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
701        // Retrieve sidebars, widgets and their instances.
702        $sidebars_widgets = get_option( 'sidebars_widgets', array() );
703        $widget_instances = get_option( 'widget_' . $widget_id, array() );
704
705        // Retrieve the key of the next widget instance.
706        $numeric_keys = array_filter( array_keys( $widget_instances ), 'is_int' );
707        $next_key     = $numeric_keys ? max( $numeric_keys ) + 1 : 2;
708
709        // Add this widget to the sidebar.
710        if ( ! isset( $sidebars_widgets[ $sidebar ] ) ) {
711            $sidebars_widgets[ $sidebar ] = array();
712        }
713        $sidebars_widgets[ $sidebar ][] = $widget_id . '-' . $next_key;
714
715        // Add the new widget instance.
716        $widget_instances[ $next_key ] = $widget_options;
717
718        // Store updated sidebars, widgets and their instances.
719        if (
720            ! ( update_option( 'sidebars_widgets', $sidebars_widgets ) )
721            || ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) )
722        ) {
723            return new WP_Error( 'widget_update_failed', 'Failed to update widget or sidebar.', 400 );
724        }
725
726        return true;
727    }
728
729    /**
730     * Update the content of an existing widget in a given sidebar.
731     *
732     * @param string $widget_id ID of the widget.
733     * @param array  $widget_options New content for the update.
734     * @param string $sidebar ID of the sidebar to which the widget will be added.
735     *
736     * @return WP_Error|true True when data has been updated correctly, error otherwise.
737     */
738    public static function update_widget_in_sidebar( $widget_id, $widget_options, $sidebar ) {
739        // Retrieve sidebars, widgets and their instances.
740        $sidebars_widgets = get_option( 'sidebars_widgets', array() );
741        $widget_instances = get_option( 'widget_' . $widget_id, array() );
742
743        // Retrieve index of first widget instance in that sidebar.
744        $widget_key = false;
745        foreach ( $sidebars_widgets[ $sidebar ] as $widget ) {
746            if ( str_contains( $widget, $widget_id ) ) {
747                $widget_key = absint( str_replace( $widget_id . '-', '', $widget ) );
748                break;
749            }
750        }
751
752        // There is no widget instance.
753        if ( ! $widget_key ) {
754            return new WP_Error( 'invalid_data', 'No such widget.', 400 );
755        }
756
757        // Update the widget instance and option if the data has changed.
758        if ( $widget_instances[ $widget_key ]['title'] !== $widget_options['title']
759            || $widget_instances[ $widget_key ]['address'] !== $widget_options['address']
760        ) {
761
762            $widget_instances[ $widget_key ] = array_merge( $widget_instances[ $widget_key ], $widget_options );
763
764            // Store updated widget instances and return Error when not successful.
765            if ( ! ( update_option( 'widget_' . $widget_id, $widget_instances ) ) ) {
766                return new WP_Error( 'widget_update_failed', 'Failed to update widget.', 400 );
767            }
768        }
769        return true;
770    }
771
772    /**
773     * Retrieve the first active sidebar.
774     *
775     * @return string|WP_Error First active sidebar, error if none exists.
776     */
777    public static function get_first_sidebar() {
778        $active_sidebars = get_option( 'sidebars_widgets', array() );
779        unset( $active_sidebars['wp_inactive_widgets'], $active_sidebars['array_version'] );
780
781        if ( empty( $active_sidebars ) ) {
782            return false;
783        }
784        $active_sidebars_keys = array_keys( $active_sidebars );
785        return array_shift( $active_sidebars_keys );
786    }
787}