widgets.php 26 KB

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