PageRenderTime 61ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-includes/class-wp-customize-widgets.php

https://bitbucket.org/theshipswakecreative/psw
PHP | 1556 lines | 724 code | 192 blank | 640 comment | 89 complexity | b7229a21d6fff9ab7be002dfb6694c2c MD5 | raw file
Possible License(s): LGPL-3.0, Apache-2.0
  1. <?php
  2. /**
  3. * Customize Widgets Class
  4. *
  5. * Implements widget management in the Customizer.
  6. *
  7. * @package WordPress
  8. * @subpackage Customize
  9. * @since 3.9.0
  10. */
  11. final class WP_Customize_Widgets {
  12. /**
  13. * WP_Customize_Manager instance.
  14. *
  15. * @since 3.9.0
  16. * @access public
  17. * @var WP_Customize_Manager
  18. */
  19. public $manager;
  20. /**
  21. * All id_bases for widgets defined in core.
  22. *
  23. * @since 3.9.0
  24. * @access protected
  25. * @var array
  26. */
  27. protected $core_widget_id_bases = array(
  28. 'archives', 'calendar', 'categories', 'links', 'meta',
  29. 'nav_menu', 'pages', 'recent-comments', 'recent-posts',
  30. 'rss', 'search', 'tag_cloud', 'text',
  31. );
  32. /**
  33. * @since 3.9.0
  34. * @access protected
  35. * @var
  36. */
  37. protected $_customized;
  38. /**
  39. * @since 3.9.0
  40. * @access protected
  41. * @var array
  42. */
  43. protected $_prepreview_added_filters = array();
  44. /**
  45. * @since 3.9.0
  46. * @access protected
  47. * @var array
  48. */
  49. protected $rendered_sidebars = array();
  50. /**
  51. * @since 3.9.0
  52. * @access protected
  53. * @var array
  54. */
  55. protected $rendered_widgets = array();
  56. /**
  57. * @since 3.9.0
  58. * @access protected
  59. * @var array
  60. */
  61. protected $old_sidebars_widgets = array();
  62. /**
  63. * Initial loader.
  64. *
  65. * @since 3.9.0
  66. * @access public
  67. *
  68. * @param WP_Customize_Manager $manager Customize manager bootstrap instance.
  69. */
  70. public function __construct( $manager ) {
  71. $this->manager = $manager;
  72. add_action( 'after_setup_theme', array( $this, 'setup_widget_addition_previews' ) );
  73. add_action( 'wp_loaded', array( $this, 'override_sidebars_widgets_for_theme_switch' ) );
  74. add_action( 'customize_controls_init', array( $this, 'customize_controls_init' ) );
  75. add_action( 'customize_register', array( $this, 'schedule_customize_register' ), 1 );
  76. add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
  77. add_action( 'customize_controls_print_styles', array( $this, 'print_styles' ) );
  78. add_action( 'customize_controls_print_scripts', array( $this, 'print_scripts' ) );
  79. add_action( 'customize_controls_print_footer_scripts', array( $this, 'print_footer_scripts' ) );
  80. add_action( 'customize_controls_print_footer_scripts', array( $this, 'output_widget_control_templates' ) );
  81. add_action( 'customize_preview_init', array( $this, 'customize_preview_init' ) );
  82. add_action( 'dynamic_sidebar', array( $this, 'tally_rendered_widgets' ) );
  83. add_filter( 'is_active_sidebar', array( $this, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
  84. add_filter( 'dynamic_sidebar_has_widgets', array( $this, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
  85. }
  86. /**
  87. * Get an unslashed post value or return a default.
  88. *
  89. * @since 3.9.0
  90. *
  91. * @access protected
  92. *
  93. * @param string $name Post value.
  94. * @param mixed $default Default post value.
  95. * @return mixed Unslashed post value or default value.
  96. */
  97. protected function get_post_value( $name, $default = null ) {
  98. if ( ! isset( $_POST[ $name ] ) ) {
  99. return $default;
  100. }
  101. return wp_unslash( $_POST[$name] );
  102. }
  103. /**
  104. * Set up widget addition previews.
  105. *
  106. * Since the widgets get registered on 'widgets_init' before the customizer
  107. * settings are set up on 'customize_register', we have to filter the options
  108. * similarly to how the setting previewer will filter the options later.
  109. *
  110. * @since 3.9.0
  111. *
  112. * @access public
  113. */
  114. public function setup_widget_addition_previews() {
  115. $is_customize_preview = false;
  116. if ( ! empty( $this->manager ) && ! is_admin() && 'on' === $this->get_post_value( 'wp_customize' ) ) {
  117. $is_customize_preview = check_ajax_referer( 'preview-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
  118. }
  119. $is_ajax_widget_update = false;
  120. if ( $this->manager->doing_ajax() && 'update-widget' === $this->get_post_value( 'action' ) ) {
  121. $is_ajax_widget_update = check_ajax_referer( 'update-widget', 'nonce', false );
  122. }
  123. $is_ajax_customize_save = false;
  124. if ( $this->manager->doing_ajax() && 'customize_save' === $this->get_post_value( 'action' ) ) {
  125. $is_ajax_customize_save = check_ajax_referer( 'save-customize_' . $this->manager->get_stylesheet(), 'nonce', false );
  126. }
  127. $is_valid_request = ( $is_ajax_widget_update || $is_customize_preview || $is_ajax_customize_save );
  128. if ( ! $is_valid_request ) {
  129. return;
  130. }
  131. // Input from customizer preview.
  132. if ( isset( $_POST['customized'] ) ) {
  133. $this->_customized = json_decode( $this->get_post_value( 'customized' ), true );
  134. } else { // Input from ajax widget update request.
  135. $this->_customized = array();
  136. $id_base = $this->get_post_value( 'id_base' );
  137. $widget_number = $this->get_post_value( 'widget_number', false );
  138. $option_name = 'widget_' . $id_base;
  139. $this->_customized[ $option_name ] = array();
  140. if ( preg_match( '/^[0-9]+$/', $widget_number ) ) {
  141. $option_name .= '[' . $widget_number . ']';
  142. $this->_customized[ $option_name ][ $widget_number ] = array();
  143. }
  144. }
  145. $function = array( $this, 'prepreview_added_sidebars_widgets' );
  146. $hook = 'option_sidebars_widgets';
  147. add_filter( $hook, $function );
  148. $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
  149. $hook = 'default_option_sidebars_widgets';
  150. add_filter( $hook, $function );
  151. $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
  152. $function = array( $this, 'prepreview_added_widget_instance' );
  153. foreach ( $this->_customized as $setting_id => $value ) {
  154. if ( preg_match( '/^(widget_.+?)(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
  155. $option = $matches[1];
  156. $hook = sprintf( 'option_%s', $option );
  157. if ( ! has_filter( $hook, $function ) ) {
  158. add_filter( $hook, $function );
  159. $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
  160. }
  161. $hook = sprintf( 'default_option_%s', $option );
  162. if ( ! has_filter( $hook, $function ) ) {
  163. add_filter( $hook, $function );
  164. $this->_prepreview_added_filters[] = compact( 'hook', 'function' );
  165. }
  166. /*
  167. * Make sure the option is registered so that the update_option()
  168. * won't fail due to the filters providing a default value, which
  169. * causes the update_option() to get confused.
  170. */
  171. add_option( $option, array() );
  172. }
  173. }
  174. }
  175. /**
  176. * Ensure that newly-added widgets will appear in the widgets_sidebars.
  177. *
  178. * This is necessary because the customizer's setting preview filters
  179. * are added after the widgets_init action, which is too late for the
  180. * widgets to be set up properly.
  181. *
  182. * @since 3.9.0
  183. * @access public
  184. *
  185. * @param array $sidebars_widgets Associative array of sidebars and their widgets.
  186. * @return array Filtered array of sidebars and their widgets.
  187. */
  188. public function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
  189. foreach ( $this->_customized as $setting_id => $value ) {
  190. if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
  191. $sidebar_id = $matches[1];
  192. $sidebars_widgets[ $sidebar_id ] = $value;
  193. }
  194. }
  195. return $sidebars_widgets;
  196. }
  197. /**
  198. * Ensure newly-added widgets have empty instances so they
  199. * will be recognized.
  200. *
  201. * This is necessary because the customizer's setting preview
  202. * filters are added after the widgets_init action, which is
  203. * too late for the widgets to be set up properly.
  204. *
  205. * @since 3.9.0
  206. * @access public
  207. *
  208. * @param array|bool|mixed $value Widget instance(s), false if open was empty.
  209. * @return array|mixed Widget instance(s) with additions.
  210. */
  211. public function prepreview_added_widget_instance( $value = false ) {
  212. if ( ! preg_match( '/^(?:default_)?option_(widget_(.+))/', current_filter(), $matches ) ) {
  213. return $value;
  214. }
  215. $id_base = $matches[2];
  216. foreach ( $this->_customized as $setting_id => $setting ) {
  217. $parsed_setting_id = $this->parse_widget_setting_id( $setting_id );
  218. if ( is_wp_error( $parsed_setting_id ) || $id_base !== $parsed_setting_id['id_base'] ) {
  219. continue;
  220. }
  221. $widget_number = $parsed_setting_id['number'];
  222. if ( is_null( $widget_number ) ) {
  223. // Single widget.
  224. if ( false === $value ) {
  225. $value = array();
  226. }
  227. } else {
  228. // Multi widget.
  229. if ( empty( $value ) ) {
  230. $value = array( '_multiwidget' => 1 );
  231. }
  232. if ( ! isset( $value[ $widget_number ] ) ) {
  233. $value[ $widget_number ] = array();
  234. }
  235. }
  236. }
  237. return $value;
  238. }
  239. /**
  240. * Remove pre-preview filters.
  241. *
  242. * Removes filters added in setup_widget_addition_previews()
  243. * to ensure widgets are populating the options during
  244. * 'widgets_init'.
  245. *
  246. * @since 3.9.0
  247. * @access public
  248. */
  249. public function remove_prepreview_filters() {
  250. foreach ( $this->_prepreview_added_filters as $prepreview_added_filter ) {
  251. remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
  252. }
  253. $this->_prepreview_added_filters = array();
  254. }
  255. /**
  256. * Override sidebars_widgets for theme switch.
  257. *
  258. * When switching a theme via the customizer, supply any previously-configured
  259. * sidebars_widgets from the target theme as the initial sidebars_widgets
  260. * setting. Also store the old theme's existing settings so that they can
  261. * be passed along for storing in the sidebars_widgets theme_mod when the
  262. * theme gets switched.
  263. *
  264. * @since 3.9.0
  265. * @access public
  266. */
  267. public function override_sidebars_widgets_for_theme_switch() {
  268. global $sidebars_widgets;
  269. if ( $this->manager->doing_ajax() || $this->manager->is_theme_active() ) {
  270. return;
  271. }
  272. $this->old_sidebars_widgets = wp_get_sidebars_widgets();
  273. add_filter( 'customize_value_old_sidebars_widgets_data', array( $this, 'filter_customize_value_old_sidebars_widgets_data' ) );
  274. // retrieve_widgets() looks at the global $sidebars_widgets
  275. $sidebars_widgets = $this->old_sidebars_widgets;
  276. $sidebars_widgets = retrieve_widgets( 'customize' );
  277. add_filter( 'option_sidebars_widgets', array( $this, 'filter_option_sidebars_widgets_for_theme_switch' ), 1 );
  278. }
  279. /**
  280. * Filter old_sidebars_widgets_data customizer setting.
  281. *
  282. * When switching themes, filter the Customizer setting
  283. * old_sidebars_widgets_data to supply initial $sidebars_widgets before they
  284. * were overridden by retrieve_widgets(). The value for
  285. * old_sidebars_widgets_data gets set in the old theme's sidebars_widgets
  286. * theme_mod.
  287. *
  288. * @see WP_Customize_Widgets::handle_theme_switch()
  289. * @since 3.9.0
  290. * @access public
  291. *
  292. * @param array $sidebars_widgets
  293. */
  294. public function filter_customize_value_old_sidebars_widgets_data( $old_sidebars_widgets ) {
  295. return $this->old_sidebars_widgets;
  296. }
  297. /**
  298. * Filter sidebars_widgets option for theme switch.
  299. *
  300. * When switching themes, the retrieve_widgets() function is run when the
  301. * Customizer initializes, and then the new sidebars_widgets here get
  302. * supplied as the default value for the sidebars_widgets option.
  303. *
  304. * @see WP_Customize_Widgets::handle_theme_switch()
  305. * @since 3.9.0
  306. * @access public
  307. *
  308. * @param array $sidebars_widgets
  309. */
  310. public function filter_option_sidebars_widgets_for_theme_switch( $sidebars_widgets ) {
  311. $sidebars_widgets = $GLOBALS['sidebars_widgets'];
  312. $sidebars_widgets['array_version'] = 3;
  313. return $sidebars_widgets;
  314. }
  315. /**
  316. * Make sure all widgets get loaded into the Customizer.
  317. *
  318. * Note: these actions are also fired in wp_ajax_update_widget().
  319. *
  320. * @since 3.9.0
  321. * @access public
  322. */
  323. public function customize_controls_init() {
  324. /** This action is documented in wp-admin/includes/ajax-actions.php */
  325. do_action( 'load-widgets.php' );
  326. /** This action is documented in wp-admin/includes/ajax-actions.php */
  327. do_action( 'widgets.php' );
  328. /** This action is documented in wp-admin/widgets.php */
  329. do_action( 'sidebar_admin_setup' );
  330. }
  331. /**
  332. * Ensure widgets are available for all types of previews.
  333. *
  334. * When in preview, hook to 'customize_register' for settings
  335. * after WordPress is loaded so that all filters have been
  336. * initialized (e.g. Widget Visibility).
  337. *
  338. * @since 3.9.0
  339. * @access public
  340. */
  341. public function schedule_customize_register() {
  342. if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
  343. $this->customize_register();
  344. } else {
  345. add_action( 'wp', array( $this, 'customize_register' ) );
  346. }
  347. }
  348. /**
  349. * Register customizer settings and controls for all sidebars and widgets.
  350. *
  351. * @since 3.9.0
  352. * @access public
  353. */
  354. public function customize_register() {
  355. global $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_sidebars;
  356. $sidebars_widgets = array_merge(
  357. array( 'wp_inactive_widgets' => array() ),
  358. array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
  359. wp_get_sidebars_widgets()
  360. );
  361. $new_setting_ids = array();
  362. /*
  363. * Register a setting for all widgets, including those which are active,
  364. * inactive, and orphaned since a widget may get suppressed from a sidebar
  365. * via a plugin (like Widget Visibility).
  366. */
  367. foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
  368. $setting_id = $this->get_setting_id( $widget_id );
  369. $setting_args = $this->get_setting_args( $setting_id );
  370. $setting_args['sanitize_callback'] = array( $this, 'sanitize_widget_instance' );
  371. $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_widget_js_instance' );
  372. $this->manager->add_setting( $setting_id, $setting_args );
  373. $new_setting_ids[] = $setting_id;
  374. }
  375. /*
  376. * Add a setting which will be supplied for the theme's sidebars_widgets
  377. * theme_mod when the the theme is switched.
  378. */
  379. if ( ! $this->manager->is_theme_active() ) {
  380. $setting_id = 'old_sidebars_widgets_data';
  381. $setting_args = $this->get_setting_args( $setting_id, array(
  382. 'type' => 'global_variable',
  383. ) );
  384. $this->manager->add_setting( $setting_id, $setting_args );
  385. }
  386. $this->manager->add_panel( 'widgets', array(
  387. 'title' => __( 'Widgets' ),
  388. 'description' => __( 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).' ),
  389. 'priority' => 110,
  390. ) );
  391. foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
  392. if ( empty( $sidebar_widget_ids ) ) {
  393. $sidebar_widget_ids = array();
  394. }
  395. $is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
  396. $is_inactive_widgets = ( 'wp_inactive_widgets' === $sidebar_id );
  397. $is_active_sidebar = ( $is_registered_sidebar && ! $is_inactive_widgets );
  398. // Add setting for managing the sidebar's widgets.
  399. if ( $is_registered_sidebar || $is_inactive_widgets ) {
  400. $setting_id = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
  401. $setting_args = $this->get_setting_args( $setting_id );
  402. $setting_args['sanitize_callback'] = array( $this, 'sanitize_sidebar_widgets' );
  403. $setting_args['sanitize_js_callback'] = array( $this, 'sanitize_sidebar_widgets_js_instance' );
  404. $this->manager->add_setting( $setting_id, $setting_args );
  405. $new_setting_ids[] = $setting_id;
  406. // Add section to contain controls.
  407. $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
  408. if ( $is_active_sidebar ) {
  409. $section_args = array(
  410. 'title' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['name'],
  411. 'description' => $GLOBALS['wp_registered_sidebars'][ $sidebar_id ]['description'],
  412. 'priority' => array_search( $sidebar_id, array_keys( $wp_registered_sidebars ) ),
  413. 'panel' => 'widgets',
  414. );
  415. /**
  416. * Filter Customizer widget section arguments for a given sidebar.
  417. *
  418. * @since 3.9.0
  419. *
  420. * @param array $section_args Array of Customizer widget section arguments.
  421. * @param string $section_id Customizer section ID.
  422. * @param int|string $sidebar_id Sidebar ID.
  423. */
  424. $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
  425. $this->manager->add_section( $section_id, $section_args );
  426. $control = new WP_Widget_Area_Customize_Control( $this->manager, $setting_id, array(
  427. 'section' => $section_id,
  428. 'sidebar_id' => $sidebar_id,
  429. 'priority' => count( $sidebar_widget_ids ), // place 'Add Widget' and 'Reorder' buttons at end.
  430. ) );
  431. $new_setting_ids[] = $setting_id;
  432. $this->manager->add_control( $control );
  433. }
  434. }
  435. // Add a control for each active widget (located in a sidebar).
  436. foreach ( $sidebar_widget_ids as $i => $widget_id ) {
  437. // Skip widgets that may have gone away due to a plugin being deactivated.
  438. if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
  439. continue;
  440. }
  441. $registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
  442. $setting_id = $this->get_setting_id( $widget_id );
  443. $id_base = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
  444. $control = new WP_Widget_Form_Customize_Control( $this->manager, $setting_id, array(
  445. 'label' => $registered_widget['name'],
  446. 'section' => $section_id,
  447. 'sidebar_id' => $sidebar_id,
  448. 'widget_id' => $widget_id,
  449. 'widget_id_base' => $id_base,
  450. 'priority' => $i,
  451. 'width' => $wp_registered_widget_controls[$widget_id]['width'],
  452. 'height' => $wp_registered_widget_controls[$widget_id]['height'],
  453. 'is_wide' => $this->is_wide_widget( $widget_id ),
  454. ) );
  455. $this->manager->add_control( $control );
  456. }
  457. }
  458. /*
  459. * We have to register these settings later than customize_preview_init
  460. * so that other filters have had a chance to run.
  461. */
  462. if ( did_action( 'customize_preview_init' ) ) {
  463. foreach ( $new_setting_ids as $new_setting_id ) {
  464. $this->manager->get_setting( $new_setting_id )->preview();
  465. }
  466. }
  467. $this->remove_prepreview_filters();
  468. }
  469. /**
  470. * Covert a widget_id into its corresponding customizer setting ID (option name).
  471. *
  472. * @since 3.9.0
  473. * @access public
  474. *
  475. * @param string $widget_id Widget ID.
  476. * @return string Maybe-parsed widget ID.
  477. */
  478. public function get_setting_id( $widget_id ) {
  479. $parsed_widget_id = $this->parse_widget_id( $widget_id );
  480. $setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
  481. if ( ! is_null( $parsed_widget_id['number'] ) ) {
  482. $setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
  483. }
  484. return $setting_id;
  485. }
  486. /**
  487. * Determine whether the widget is considered "wide".
  488. *
  489. * Core widgets which may have controls wider than 250, but can
  490. * still be shown in the narrow customizer panel. The RSS and Text
  491. * widgets in Core, for example, have widths of 400 and yet they
  492. * still render fine in the customizer panel. This method will
  493. * return all Core widgets as being not wide, but this can be
  494. * overridden with the is_wide_widget_in_customizer filter.
  495. *
  496. * @since 3.9.0
  497. * @access public
  498. *
  499. * @param string $widget_id Widget ID.
  500. * @return bool Whether or not the widget is a "wide" widget.
  501. */
  502. public function is_wide_widget( $widget_id ) {
  503. global $wp_registered_widget_controls;
  504. $parsed_widget_id = $this->parse_widget_id( $widget_id );
  505. $width = $wp_registered_widget_controls[$widget_id]['width'];
  506. $is_core = in_array( $parsed_widget_id['id_base'], $this->core_widget_id_bases );
  507. $is_wide = ( $width > 250 && ! $is_core );
  508. /**
  509. * Filter whether the given widget is considered "wide".
  510. *
  511. * @since 3.9.0
  512. *
  513. * @param bool $is_wide Whether the widget is wide, Default false.
  514. * @param string $widget_id Widget ID.
  515. */
  516. return apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
  517. }
  518. /**
  519. * Covert a widget ID into its id_base and number components.
  520. *
  521. * @since 3.9.0
  522. * @access public
  523. *
  524. * @param string $widget_id Widget ID.
  525. * @return array Array containing a widget's id_base and number components.
  526. */
  527. public function parse_widget_id( $widget_id ) {
  528. $parsed = array(
  529. 'number' => null,
  530. 'id_base' => null,
  531. );
  532. if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
  533. $parsed['id_base'] = $matches[1];
  534. $parsed['number'] = intval( $matches[2] );
  535. } else {
  536. // likely an old single widget
  537. $parsed['id_base'] = $widget_id;
  538. }
  539. return $parsed;
  540. }
  541. /**
  542. * Convert a widget setting ID (option path) to its id_base and number components.
  543. *
  544. * @since 3.9.0
  545. * @access public
  546. *
  547. * @param string $setting_id Widget setting ID.
  548. * @return WP_Error|array Array containing a widget's id_base and number components,
  549. * or a WP_Error object.
  550. */
  551. public function parse_widget_setting_id( $setting_id ) {
  552. if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
  553. return new WP_Error( 'widget_setting_invalid_id' );
  554. }
  555. $id_base = $matches[2];
  556. $number = isset( $matches[3] ) ? intval( $matches[3] ) : null;
  557. return compact( 'id_base', 'number' );
  558. }
  559. /**
  560. * Call admin_print_styles-widgets.php and admin_print_styles hooks to
  561. * allow custom styles from plugins.
  562. *
  563. * @since 3.9.0
  564. * @access public
  565. */
  566. public function print_styles() {
  567. /** This action is documented in wp-admin/admin-header.php */
  568. do_action( 'admin_print_styles-widgets.php' );
  569. /** This action is documented in wp-admin/admin-header.php */
  570. do_action( 'admin_print_styles' );
  571. }
  572. /**
  573. * Call admin_print_scripts-widgets.php and admin_print_scripts hooks to
  574. * allow custom scripts from plugins.
  575. *
  576. * @since 3.9.0
  577. * @access public
  578. */
  579. public function print_scripts() {
  580. /** This action is documented in wp-admin/admin-header.php */
  581. do_action( 'admin_print_scripts-widgets.php' );
  582. /** This action is documented in wp-admin/admin-header.php */
  583. do_action( 'admin_print_scripts' );
  584. }
  585. /**
  586. * Enqueue scripts and styles for customizer panel and export data to JavaScript.
  587. *
  588. * @since 3.9.0
  589. * @access public
  590. */
  591. public function enqueue_scripts() {
  592. wp_enqueue_style( 'customize-widgets' );
  593. wp_enqueue_script( 'customize-widgets' );
  594. /** This action is documented in wp-admin/admin-header.php */
  595. do_action( 'admin_enqueue_scripts', 'widgets.php' );
  596. /*
  597. * Export available widgets with control_tpl removed from model
  598. * since plugins need templates to be in the DOM.
  599. */
  600. $available_widgets = array();
  601. foreach ( $this->get_available_widgets() as $available_widget ) {
  602. unset( $available_widget['control_tpl'] );
  603. $available_widgets[] = $available_widget;
  604. }
  605. $widget_reorder_nav_tpl = sprintf(
  606. '<div class="widget-reorder-nav"><span class="move-widget" tabindex="0">%1$s</span><span class="move-widget-down" tabindex="0">%2$s</span><span class="move-widget-up" tabindex="0">%3$s</span></div>',
  607. __( 'Move to another area&hellip;' ),
  608. __( 'Move down' ),
  609. __( 'Move up' )
  610. );
  611. $move_widget_area_tpl = str_replace(
  612. array( '{description}', '{btn}' ),
  613. array(
  614. __( 'Select an area to move this widget into:' ),
  615. _x( 'Move', 'Move widget' ),
  616. ),
  617. '<div class="move-widget-area">
  618. <p class="description">{description}</p>
  619. <ul class="widget-area-select">
  620. <% _.each( sidebars, function ( sidebar ){ %>
  621. <li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
  622. <% }); %>
  623. </ul>
  624. <div class="move-widget-actions">
  625. <button class="move-widget-btn button-secondary" type="button">{btn}</button>
  626. </div>
  627. </div>'
  628. );
  629. global $wp_scripts;
  630. $settings = array(
  631. 'nonce' => wp_create_nonce( 'update-widget' ),
  632. 'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
  633. 'registeredWidgets' => $GLOBALS['wp_registered_widgets'],
  634. 'availableWidgets' => $available_widgets, // @todo Merge this with registered_widgets
  635. 'l10n' => array(
  636. 'saveBtnLabel' => __( 'Apply' ),
  637. 'saveBtnTooltip' => __( 'Save and preview changes before publishing them.' ),
  638. 'removeBtnLabel' => __( 'Remove' ),
  639. 'removeBtnTooltip' => __( 'Trash widget by moving it to the inactive widgets sidebar.' ),
  640. 'error' => __( 'An error has occurred. Please reload the page and try again.' ),
  641. ),
  642. 'tpl' => array(
  643. 'widgetReorderNav' => $widget_reorder_nav_tpl,
  644. 'moveWidgetArea' => $move_widget_area_tpl,
  645. ),
  646. );
  647. foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
  648. unset( $registered_widget['callback'] ); // may not be JSON-serializeable
  649. }
  650. $wp_scripts->add_data(
  651. 'customize-widgets',
  652. 'data',
  653. sprintf( 'var _wpCustomizeWidgetsSettings = %s;', json_encode( $settings ) )
  654. );
  655. }
  656. /**
  657. * Render the widget form control templates into the DOM.
  658. *
  659. * @since 3.9.0
  660. * @access public
  661. */
  662. public function output_widget_control_templates() {
  663. ?>
  664. <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
  665. <div id="available-widgets">
  666. <div id="available-widgets-filter">
  667. <label class="screen-reader-text" for="widgets-search"><?php _e( 'Search Widgets' ); ?></label>
  668. <input type="search" id="widgets-search" placeholder="<?php esc_attr_e( 'Search widgets&hellip;' ) ?>" />
  669. </div>
  670. <?php foreach ( $this->get_available_widgets() as $available_widget ): ?>
  671. <div id="widget-tpl-<?php echo esc_attr( $available_widget['id'] ) ?>" data-widget-id="<?php echo esc_attr( $available_widget['id'] ) ?>" class="widget-tpl <?php echo esc_attr( $available_widget['id'] ) ?>" tabindex="0">
  672. <?php echo $available_widget['control_tpl']; ?>
  673. </div>
  674. <?php endforeach; ?>
  675. </div><!-- #available-widgets -->
  676. </div><!-- #widgets-left -->
  677. <?php
  678. }
  679. /**
  680. * Call admin_print_footer_scripts and admin_print_scripts hooks to
  681. * allow custom scripts from plugins.
  682. *
  683. * @since 3.9.0
  684. * @access public
  685. */
  686. public function print_footer_scripts() {
  687. /** This action is documented in wp-admin/admin-footer.php */
  688. do_action( 'admin_print_footer_scripts' );
  689. /** This action is documented in wp-admin/admin-footer.php */
  690. do_action( 'admin_footer-widgets.php' );
  691. }
  692. /**
  693. * Get common arguments to supply when constructing a Customizer setting.
  694. *
  695. * @since 3.9.0
  696. * @access public
  697. *
  698. * @param string $id Widget setting ID.
  699. * @param array $overrides Array of setting overrides.
  700. * @return array Possibly modified setting arguments.
  701. */
  702. public function get_setting_args( $id, $overrides = array() ) {
  703. $args = array(
  704. 'type' => 'option',
  705. 'capability' => 'edit_theme_options',
  706. 'transport' => 'refresh',
  707. 'default' => array(),
  708. );
  709. $args = array_merge( $args, $overrides );
  710. /**
  711. * Filter the common arguments supplied when constructing a Customizer setting.
  712. *
  713. * @since 3.9.0
  714. *
  715. * @see WP_Customize_Setting
  716. *
  717. * @param array $args Array of Customizer setting arguments.
  718. * @param string $id Widget setting ID.
  719. */
  720. return apply_filters( 'widget_customizer_setting_args', $args, $id );
  721. }
  722. /**
  723. * Make sure that sidebar widget arrays only ever contain widget IDS.
  724. *
  725. * Used as the 'sanitize_callback' for each $sidebars_widgets setting.
  726. *
  727. * @since 3.9.0
  728. * @access public
  729. *
  730. * @param array $widget_ids Array of widget IDs.
  731. * @return array Array of sanitized widget IDs.
  732. */
  733. public function sanitize_sidebar_widgets( $widget_ids ) {
  734. global $wp_registered_widgets;
  735. $widget_ids = array_map( 'strval', (array) $widget_ids );
  736. $sanitized_widget_ids = array();
  737. foreach ( $widget_ids as $widget_id ) {
  738. if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
  739. $sanitized_widget_ids[] = $widget_id;
  740. }
  741. }
  742. return $sanitized_widget_ids;
  743. }
  744. /**
  745. * Build up an index of all available widgets for use in Backbone models.
  746. *
  747. * @since 3.9.0
  748. * @access public
  749. *
  750. * @see wp_list_widgets()
  751. *
  752. * @return array List of available widgets.
  753. */
  754. public function get_available_widgets() {
  755. static $available_widgets = array();
  756. if ( ! empty( $available_widgets ) ) {
  757. return $available_widgets;
  758. }
  759. global $wp_registered_widgets, $wp_registered_widget_controls;
  760. require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
  761. $sort = $wp_registered_widgets;
  762. usort( $sort, array( $this, '_sort_name_callback' ) );
  763. $done = array();
  764. foreach ( $sort as $widget ) {
  765. if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
  766. continue;
  767. }
  768. $sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
  769. $done[] = $widget['callback'];
  770. if ( ! isset( $widget['params'][0] ) ) {
  771. $widget['params'][0] = array();
  772. }
  773. $available_widget = $widget;
  774. unset( $available_widget['callback'] ); // not serializable to JSON
  775. $args = array(
  776. 'widget_id' => $widget['id'],
  777. 'widget_name' => $widget['name'],
  778. '_display' => 'template',
  779. );
  780. $is_disabled = false;
  781. $is_multi_widget = ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) && isset( $widget['params'][0]['number'] ) );
  782. if ( $is_multi_widget ) {
  783. $id_base = $wp_registered_widget_controls[$widget['id']]['id_base'];
  784. $args['_temp_id'] = "$id_base-__i__";
  785. $args['_multi_num'] = next_widget_id_number( $id_base );
  786. $args['_add'] = 'multi';
  787. } else {
  788. $args['_add'] = 'single';
  789. if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
  790. $is_disabled = true;
  791. }
  792. $id_base = $widget['id'];
  793. }
  794. $list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
  795. $control_tpl = $this->get_widget_control( $list_widget_controls_args );
  796. // The properties here are mapped to the Backbone Widget model.
  797. $available_widget = array_merge( $available_widget, array(
  798. 'temp_id' => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
  799. 'is_multi' => $is_multi_widget,
  800. 'control_tpl' => $control_tpl,
  801. 'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
  802. 'is_disabled' => $is_disabled,
  803. 'id_base' => $id_base,
  804. 'transport' => 'refresh',
  805. 'width' => $wp_registered_widget_controls[$widget['id']]['width'],
  806. 'height' => $wp_registered_widget_controls[$widget['id']]['height'],
  807. 'is_wide' => $this->is_wide_widget( $widget['id'] ),
  808. ) );
  809. $available_widgets[] = $available_widget;
  810. }
  811. return $available_widgets;
  812. }
  813. /**
  814. * Naturally order available widgets by name.
  815. *
  816. * @since 3.9.0
  817. * @static
  818. * @access protected
  819. *
  820. * @param array $widget_a The first widget to compare.
  821. * @param array $widget_b The second widget to compare.
  822. * @return int Reorder position for the current widget comparison.
  823. */
  824. protected function _sort_name_callback( $widget_a, $widget_b ) {
  825. return strnatcasecmp( $widget_a['name'], $widget_b['name'] );
  826. }
  827. /**
  828. * Get the widget control markup.
  829. *
  830. * @since 3.9.0
  831. * @access public
  832. *
  833. * @param array $args Widget control arguments.
  834. * @return string Widget control form HTML markup.
  835. */
  836. public function get_widget_control( $args ) {
  837. ob_start();
  838. call_user_func_array( 'wp_widget_control', $args );
  839. $replacements = array(
  840. '<form action="" method="post">' => '<div class="form">',
  841. '</form>' => '</div><!-- .form -->',
  842. );
  843. $control_tpl = ob_get_clean();
  844. $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
  845. return $control_tpl;
  846. }
  847. /**
  848. * Add hooks for the customizer preview.
  849. *
  850. * @since 3.9.0
  851. * @access public
  852. */
  853. public function customize_preview_init() {
  854. add_filter( 'sidebars_widgets', array( $this, 'preview_sidebars_widgets' ), 1 );
  855. add_action( 'wp_enqueue_scripts', array( $this, 'customize_preview_enqueue' ) );
  856. add_action( 'wp_print_styles', array( $this, 'print_preview_css' ), 1 );
  857. add_action( 'wp_footer', array( $this, 'export_preview_data' ), 20 );
  858. }
  859. /**
  860. * When previewing, make sure the proper previewing widgets are used.
  861. *
  862. * Because wp_get_sidebars_widgets() gets called early at init
  863. * (via wp_convert_widget_settings()) and can set global variable
  864. * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' )
  865. * before the customizer preview filter is added, we have to reset
  866. * it after the filter has been added.
  867. *
  868. * @since 3.9.0
  869. * @access public
  870. *
  871. * @param array $sidebars_widgets List of widgets for the current sidebar.
  872. */
  873. public function preview_sidebars_widgets( $sidebars_widgets ) {
  874. $sidebars_widgets = get_option( 'sidebars_widgets' );
  875. unset( $sidebars_widgets['array_version'] );
  876. return $sidebars_widgets;
  877. }
  878. /**
  879. * Enqueue scripts for the Customizer preview.
  880. *
  881. * @since 3.9.0
  882. * @access public
  883. */
  884. public function customize_preview_enqueue() {
  885. wp_enqueue_script( 'customize-preview-widgets' );
  886. }
  887. /**
  888. * Insert default style for highlighted widget at early point so theme
  889. * stylesheet can override.
  890. *
  891. * @since 3.9.0
  892. * @access public
  893. *
  894. * @action wp_print_styles
  895. */
  896. public function print_preview_css() {
  897. ?>
  898. <style>
  899. .widget-customizer-highlighted-widget {
  900. outline: none;
  901. -webkit-box-shadow: 0 0 2px rgba(30,140,190,0.8);
  902. box-shadow: 0 0 2px rgba(30,140,190,0.8);
  903. position: relative;
  904. z-index: 1;
  905. }
  906. </style>
  907. <?php
  908. }
  909. /**
  910. * At the very end of the page, at the very end of the wp_footer,
  911. * communicate the sidebars that appeared on the page.
  912. *
  913. * @since 3.9.0
  914. * @access public
  915. */
  916. public function export_preview_data() {
  917. // Prepare customizer settings to pass to Javascript.
  918. $settings = array(
  919. 'renderedSidebars' => array_fill_keys( array_unique( $this->rendered_sidebars ), true ),
  920. 'renderedWidgets' => array_fill_keys( array_keys( $this->rendered_widgets ), true ),
  921. 'registeredSidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
  922. 'registeredWidgets' => $GLOBALS['wp_registered_widgets'],
  923. 'l10n' => array(
  924. 'widgetTooltip' => __( 'Shift-click to edit this widget.' ),
  925. ),
  926. );
  927. foreach ( $settings['registeredWidgets'] as &$registered_widget ) {
  928. unset( $registered_widget['callback'] ); // may not be JSON-serializeable
  929. }
  930. ?>
  931. <script type="text/javascript">
  932. var _wpWidgetCustomizerPreviewSettings = <?php echo json_encode( $settings ); ?>;
  933. </script>
  934. <?php
  935. }
  936. /**
  937. * Keep track of the widgets that were rendered.
  938. *
  939. * @since 3.9.0
  940. * @access public
  941. *
  942. * @param array $widget Rendered widget to tally.
  943. */
  944. public function tally_rendered_widgets( $widget ) {
  945. $this->rendered_widgets[ $widget['id'] ] = true;
  946. }
  947. /**
  948. * Determine if a widget is rendered on the page.
  949. *
  950. * @since 4.0.0
  951. * @access public
  952. *
  953. * @param string $widget_id Widget ID to check.
  954. * @return bool Whether the widget is rendered.
  955. */
  956. public function is_widget_rendered( $widget_id ) {
  957. return in_array( $widget_id, $this->rendered_widgets );
  958. }
  959. /**
  960. * Determine if a sidebar is rendered on the page.
  961. *
  962. * @since 4.0.0
  963. * @access public
  964. *
  965. * @param string $sidebar_id Sidebar ID to check.
  966. * @return bool Whether the sidebar is rendered.
  967. */
  968. public function is_sidebar_rendered( $sidebar_id ) {
  969. return in_array( $sidebar_id, $this->rendered_sidebars );
  970. }
  971. /**
  972. * Tally the sidebars rendered via is_active_sidebar().
  973. *
  974. * Keep track of the times that is_active_sidebar() is called
  975. * in the template, and assume that this means that the sidebar
  976. * would be rendered on the template if there were widgets
  977. * populating it.
  978. *
  979. * @since 3.9.0
  980. * @access public
  981. *
  982. * @param bool $is_active Whether the sidebar is active.
  983. * @param string $sidebar_id Sidebar ID.
  984. */
  985. public function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
  986. if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
  987. $this->rendered_sidebars[] = $sidebar_id;
  988. }
  989. /*
  990. * We may need to force this to true, and also force-true the value
  991. * for 'dynamic_sidebar_has_widgets' if we want to ensure that there
  992. * is an area to drop widgets into, if the sidebar is empty.
  993. */
  994. return $is_active;
  995. }
  996. /**
  997. * Tally the sidebars rendered via dynamic_sidebar().
  998. *
  999. * Keep track of the times that dynamic_sidebar() is called in the template,
  1000. * and assume this means the sidebar would be rendered on the template if
  1001. * there were widgets populating it.
  1002. *
  1003. * @since 3.9.0
  1004. * @access public
  1005. *
  1006. * @param bool $has_widgets Whether the current sidebar has widgets.
  1007. * @param string $sidebar_id Sidebar ID.
  1008. */
  1009. public function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
  1010. if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
  1011. $this->rendered_sidebars[] = $sidebar_id;
  1012. }
  1013. /*
  1014. * We may need to force this to true, and also force-true the value
  1015. * for 'is_active_sidebar' if we want to ensure there is an area to
  1016. * drop widgets into, if the sidebar is empty.
  1017. */
  1018. return $has_widgets;
  1019. }
  1020. /**
  1021. * Get MAC for a serialized widget instance string.
  1022. *
  1023. * Allows values posted back from JS to be rejected if any tampering of the
  1024. * data has occurred.
  1025. *
  1026. * @since 3.9.0
  1027. * @access protected
  1028. *
  1029. * @param string $serialized_instance Widget instance.
  1030. * @return string MAC for serialized widget instance.
  1031. */
  1032. protected function get_instance_hash_key( $serialized_instance ) {
  1033. return wp_hash( $serialized_instance );
  1034. }
  1035. /**
  1036. * Sanitize a widget instance.
  1037. *
  1038. * Unserialize the JS-instance for storing in the options. It's important
  1039. * that this filter only get applied to an instance once.
  1040. *
  1041. * @since 3.9.0
  1042. * @access public
  1043. *
  1044. * @param array $value Widget instance to sanitize.
  1045. * @return array Sanitized widget instance.
  1046. */
  1047. public function sanitize_widget_instance( $value ) {
  1048. if ( $value === array() ) {
  1049. return $value;
  1050. }
  1051. if ( empty( $value['is_widget_customizer_js_value'] )
  1052. || empty( $value['instance_hash_key'] )
  1053. || empty( $value['encoded_serialized_instance'] ) )
  1054. {
  1055. return null;
  1056. }
  1057. $decoded = base64_decode( $value['encoded_serialized_instance'], true );
  1058. if ( false === $decoded ) {
  1059. return null;
  1060. }
  1061. if ( $this->get_instance_hash_key( $decoded ) !== $value['instance_hash_key'] ) {
  1062. return null;
  1063. }
  1064. $instance = unserialize( $decoded );
  1065. if ( false === $instance ) {
  1066. return null;
  1067. }
  1068. return $instance;
  1069. }
  1070. /**
  1071. * Convert widget instance into JSON-representable format.
  1072. *
  1073. * @since 3.9.0
  1074. * @access public
  1075. *
  1076. * @param array $value Widget instance to convert to JSON.
  1077. * @return array JSON-converted widget instance.
  1078. */
  1079. public function sanitize_widget_js_instance( $value ) {
  1080. if ( empty( $value['is_widget_customizer_js_value'] ) ) {
  1081. $serialized = serialize( $value );
  1082. $value = array(
  1083. 'encoded_serialized_instance' => base64_encode( $serialized ),
  1084. 'title' => empty( $value['title'] ) ? '' : $value['title'],
  1085. 'is_widget_customizer_js_value' => true,
  1086. 'instance_hash_key' => $this->get_instance_hash_key( $serialized ),
  1087. );
  1088. }
  1089. return $value;
  1090. }
  1091. /**
  1092. * Strip out widget IDs for widgets which are no longer registered.
  1093. *
  1094. * One example where this might happen is when a plugin orphans a widget
  1095. * in a sidebar upon deactivation.
  1096. *
  1097. * @since 3.9.0
  1098. * @access public
  1099. *
  1100. * @param array $widget_ids List of widget IDs.
  1101. * @return array Parsed list of widget IDs.
  1102. */
  1103. public function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
  1104. global $wp_registered_widgets;
  1105. $widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
  1106. return $widget_ids;
  1107. }
  1108. /**
  1109. * Find and invoke the widget update and control callbacks.
  1110. *
  1111. * Requires that $_POST be populated with the instance data.
  1112. *
  1113. * @since 3.9.0
  1114. * @access public
  1115. *
  1116. * @param string $widget_id Widget ID.
  1117. * @return WP_Error|array Array containing the updated widget information.
  1118. * A WP_Error object, otherwise.
  1119. */
  1120. public function call_widget_update( $widget_id ) {
  1121. global $wp_registered_widget_updates, $wp_registered_widget_controls;
  1122. $this->start_capturing_option_updates();
  1123. $parsed_id = $this->parse_widget_id( $widget_id );
  1124. $option_name = 'widget_' . $parsed_id['id_base'];
  1125. /*
  1126. * If a previously-sanitized instance is provided, populate the input vars
  1127. * with its values so that the widget update callback will read this instance
  1128. */
  1129. $added_input_vars = array();
  1130. if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
  1131. $sanitized_widget_setting = json_decode( $this->get_post_value( 'sanitized_widget_setting' ), true );
  1132. if ( false === $sanitized_widget_setting ) {
  1133. $this->stop_capturing_option_updates();
  1134. return new WP_Error( 'widget_setting_malformed' );
  1135. }
  1136. $instance = $this->sanitize_widget_instance( $sanitized_widget_setting );
  1137. if ( is_null( $instance ) ) {
  1138. $this->stop_capturing_option_updates();
  1139. return new WP_Error( 'widget_setting_unsanitized' );
  1140. }
  1141. if ( ! is_null( $parsed_id['number'] ) ) {
  1142. $value = array();
  1143. $value[$parsed_id['number']] = $instance;
  1144. $key = 'widget-' . $parsed_id['id_base'];
  1145. $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
  1146. $added_input_vars[] = $key;
  1147. } else {
  1148. foreach ( $instance as $key => $value ) {
  1149. $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
  1150. $added_input_vars[] = $key;
  1151. }
  1152. }
  1153. }
  1154. // Invoke the widget update callback.
  1155. foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
  1156. if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
  1157. ob_start();
  1158. call_user_func_array( $control['callback'], $control['params'] );
  1159. ob_end_clean();
  1160. break;
  1161. }
  1162. }
  1163. // Clean up any input vars that were manually added
  1164. foreach ( $added_input_vars as $key ) {
  1165. unset( $_POST[$key] );
  1166. unset( $_REQUEST[$key] );
  1167. }
  1168. // Make sure the expected option was updated.
  1169. if ( 0 !== $this->count_captured_options() ) {
  1170. if ( $this->count_captured_options() > 1 ) {
  1171. $this->stop_capturing_option_updates();
  1172. return new WP_Error( 'widget_setting_too_many_options' );
  1173. }
  1174. $updated_option_name = key( $this->get_captured_options() );
  1175. if ( $updated_option_name !== $option_name ) {
  1176. $this->stop_capturing_option_updates();
  1177. return new WP_Error( 'widget_setting_unexpected_option' );
  1178. }
  1179. }
  1180. // Obtain the widget control with the updated instance in place.
  1181. ob_start();
  1182. $form = $wp_registered_widget_controls[$widget_id];
  1183. if ( $form ) {
  1184. call_user_func_array( $form['callback'], $form['params'] );
  1185. }
  1186. $form = ob_get_clean();
  1187. // Obtain the widget instance.
  1188. $option = get_option( $option_name );
  1189. if ( null !== $parsed_id['number'] ) {
  1190. $instance = $option[$parsed_id['number']];
  1191. } else {
  1192. $instance = $option;
  1193. }
  1194. $this->stop_capturing_option_updates();
  1195. return compact( 'instance', 'form' );
  1196. }
  1197. /**
  1198. * Update widget settings asynchronously.
  1199. *
  1200. * Allows the Customizer to update a widget using its form, but return the new
  1201. * instance info via Ajax instead of saving it to the options table.
  1202. *
  1203. * Most code here copied from wp_ajax_save_widget()
  1204. *
  1205. * @since 3.9.0
  1206. * @access public
  1207. *
  1208. * @see wp_ajax_save_widget()
  1209. *
  1210. */
  1211. public function wp_ajax_update_widget() {
  1212. if ( ! is_user_logged_in() ) {
  1213. wp_die( 0 );
  1214. }
  1215. check_ajax_referer( 'update-widget', 'nonce' );
  1216. if ( ! current_user_can( 'edit_theme_options' ) ) {
  1217. wp_die( -1 );
  1218. }
  1219. if ( ! isset( $_POST['widget-id'] ) ) {
  1220. wp_send_json_error();
  1221. }
  1222. /** This action is documented in wp-admin/includes/ajax-actions.php */
  1223. do_action( 'load-widgets.php' );
  1224. /** This action is documented in wp-admin/includes/ajax-actions.php */
  1225. do_action( 'widgets.php' );
  1226. /** This action is documented in wp-admin/widgets.php */
  1227. do_action( 'sidebar_admin_setup' );
  1228. $widget_id = $this->get_post_value( 'widget-id' );
  1229. $parsed_id = $this->parse_widget_id( $widget_id );
  1230. $id_base = $parsed_id['id_base'];
  1231. if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
  1232. wp_send_json_error();
  1233. }
  1234. $updated_widget = $this->call_widget_update( $widget_id ); // => {instance,form}
  1235. if ( is_wp_error( $updated_widget ) ) {
  1236. wp_send_json_error();
  1237. }
  1238. $form = $updated_widget['form'];
  1239. $instance = $this->sanitize_widget_js_instance( $updated_widget['instance'] );
  1240. wp_send_json_success( compact( 'form', 'instance' ) );
  1241. }
  1242. /***************************************************************************
  1243. * Option Update Capturing
  1244. ***************************************************************************/
  1245. /**
  1246. * List of captured widget option updates.
  1247. *
  1248. * @since 3.9.0
  1249. * @access protected
  1250. * @var array $_captured_options Values updated while option capture is happening.
  1251. */
  1252. protected $_captured_options = array();
  1253. /**
  1254. * Whether option capture is currently happening.
  1255. *
  1256. * @since 3.9.0
  1257. * @access protected
  1258. * @var bool $_is_current Whether option capture is currently happening or not.
  1259. */
  1260. protected $_is_capturing_option_updates = false;
  1261. /**
  1262. * Determine whether the captured option update should be ignored.
  1263. *
  1264. * @since 3.9.0
  1265. * @access protected
  1266. *
  1267. * @param string $option_name Option name.
  1268. * @return boolean Whether the option capture is ignored.
  1269. */
  1270. protected function is_option_capture_ignored( $option_name ) {
  1271. return ( 0 === strpos( $option_name, '_transient_' ) );
  1272. }
  1273. /**
  1274. * Retrieve captured widget option updates.
  1275. *
  1276. * @since 3.9.0
  1277. * @access protected
  1278. *
  1279. * @return array Array of captured options.
  1280. */
  1281. protected function get_captured_options() {
  1282. return $this->_captured_options;
  1283. }
  1284. /**
  1285. * Get the number of captured widget option updates.
  1286. *
  1287. * @since 3.9.0
  1288. * @access protected
  1289. *
  1290. * @return int Number of updated options.
  1291. */
  1292. protected function count_captured_options() {
  1293. return count( $this->_captured_options );
  1294. }
  1295. /**
  1296. * Start keeping track of changes to widget options, caching new values.
  1297. *
  1298. * @since 3.9.0
  1299. * @access protected
  1300. */
  1301. protected function start_capturing_option_updates() {
  1302. if ( $this->_is_capturing_option_updates ) {
  1303. return;
  1304. }
  1305. $this->_is_capturing_option_updates = true;
  1306. add_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
  1307. }
  1308. /**
  1309. * Pre-filter captured option values before updating.
  1310. *
  1311. * @since 3.9.0
  1312. * @access public
  1313. *
  1314. * @param mixed $new_value
  1315. * @param string $option_name
  1316. * @param mixed $old_value
  1317. * @return mixed
  1318. */
  1319. public function capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
  1320. if ( $this->is_option_capture_ignored( $option_name ) ) {
  1321. return;
  1322. }
  1323. if ( ! isset( $this->_captured_options[$option_name] ) ) {
  1324. add_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
  1325. }
  1326. $this->_captured_options[$option_name] = $new_value;
  1327. return $old_value;
  1328. }
  1329. /**
  1330. * Pre-filter captured option values before retrieving.
  1331. *
  1332. * @since 3.9.0
  1333. * @access public
  1334. *
  1335. * @param mixed $value Option
  1336. * @return mixed
  1337. */
  1338. public function capture_filter_pre_get_option( $value ) {
  1339. $option_name = preg_replace( '/^pre_option_/', '', current_filter() );
  1340. if ( isset( $this->_captured_options[$option_name] ) ) {
  1341. $value = $this->_captured_options[$option_name];
  1342. /** This filter is documented in wp-includes/option.php */
  1343. $value = apply_filters( 'option_' . $option_name, $value );
  1344. }
  1345. return $value;
  1346. }
  1347. /**
  1348. * Undo any changes to the options since options capture began.
  1349. *
  1350. * @since 3.9.0
  1351. * @access protected
  1352. */
  1353. protected function stop_capturing_option_updates() {
  1354. if ( ! $this->_is_capturing_option_updates ) {
  1355. return;
  1356. }
  1357. remove_filter( 'pre_update_option', array( $this, 'capture_filter_pre_update_option' ), 10, 3 );
  1358. foreach ( array_keys( $this->_captured_options ) as $option_name ) {
  1359. remove_filter( "pre_option_{$option_name}", array( $this, 'capture_filter_pre_get_option' ) );
  1360. }
  1361. $this->_captured_options = array();
  1362. $this->_is_capturing_option_updates = false;
  1363. }
  1364. }