class-wp-rest-sidebars-controller.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. <?php
  2. /**
  3. * REST API: WP_REST_Sidebars_Controller class
  4. *
  5. * Original code from {@link https://github.com/martin-pettersson/wp-rest-api-sidebars Martin Pettersson (martin_pettersson@outlook.com)}.
  6. *
  7. * @package WordPress
  8. * @subpackage REST_API
  9. * @since 5.8.0
  10. */
  11. /**
  12. * Core class used to manage a site's sidebars.
  13. *
  14. * @since 5.8.0
  15. *
  16. * @see WP_REST_Controller
  17. */
  18. class WP_REST_Sidebars_Controller extends WP_REST_Controller {
  19. /**
  20. * Tracks whether {@see retrieve_widgets()} has been called in the current request.
  21. *
  22. * @since 5.9.0
  23. * @var bool
  24. */
  25. protected $widgets_retrieved = false;
  26. /**
  27. * Sidebars controller constructor.
  28. *
  29. * @since 5.8.0
  30. */
  31. public function __construct() {
  32. $this->namespace = 'wp/v2';
  33. $this->rest_base = 'sidebars';
  34. }
  35. /**
  36. * Registers the controllers routes.
  37. *
  38. * @since 5.8.0
  39. */
  40. public function register_routes() {
  41. register_rest_route(
  42. $this->namespace,
  43. '/' . $this->rest_base,
  44. array(
  45. array(
  46. 'methods' => WP_REST_Server::READABLE,
  47. 'callback' => array( $this, 'get_items' ),
  48. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  49. 'args' => array(
  50. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  51. ),
  52. ),
  53. 'schema' => array( $this, 'get_public_item_schema' ),
  54. )
  55. );
  56. register_rest_route(
  57. $this->namespace,
  58. '/' . $this->rest_base . '/(?P<id>[\w-]+)',
  59. array(
  60. array(
  61. 'methods' => WP_REST_Server::READABLE,
  62. 'callback' => array( $this, 'get_item' ),
  63. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  64. 'args' => array(
  65. 'id' => array(
  66. 'description' => __( 'The id of a registered sidebar' ),
  67. 'type' => 'string',
  68. ),
  69. 'context' => $this->get_context_param( array( 'default' => 'view' ) ),
  70. ),
  71. ),
  72. array(
  73. 'methods' => WP_REST_Server::EDITABLE,
  74. 'callback' => array( $this, 'update_item' ),
  75. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  76. 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
  77. ),
  78. 'schema' => array( $this, 'get_public_item_schema' ),
  79. )
  80. );
  81. }
  82. /**
  83. * Checks if a given request has access to get sidebars.
  84. *
  85. * @since 5.8.0
  86. *
  87. * @param WP_REST_Request $request Full details about the request.
  88. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  89. */
  90. public function get_items_permissions_check( $request ) {
  91. $this->retrieve_widgets();
  92. foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
  93. $sidebar = $this->get_sidebar( $id );
  94. if ( ! $sidebar ) {
  95. continue;
  96. }
  97. if ( $this->check_read_permission( $sidebar ) ) {
  98. return true;
  99. }
  100. }
  101. return $this->do_permissions_check();
  102. }
  103. /**
  104. * Retrieves the list of sidebars (active or inactive).
  105. *
  106. * @since 5.8.0
  107. *
  108. * @param WP_REST_Request $request Full details about the request.
  109. * @return WP_REST_Response Response object on success.
  110. */
  111. public function get_items( $request ) {
  112. $this->retrieve_widgets();
  113. $data = array();
  114. $permissions_check = $this->do_permissions_check();
  115. foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
  116. $sidebar = $this->get_sidebar( $id );
  117. if ( ! $sidebar ) {
  118. continue;
  119. }
  120. if ( is_wp_error( $permissions_check ) && ! $this->check_read_permission( $sidebar ) ) {
  121. continue;
  122. }
  123. $data[] = $this->prepare_response_for_collection(
  124. $this->prepare_item_for_response( $sidebar, $request )
  125. );
  126. }
  127. return rest_ensure_response( $data );
  128. }
  129. /**
  130. * Checks if a given request has access to get a single sidebar.
  131. *
  132. * @since 5.8.0
  133. *
  134. * @param WP_REST_Request $request Full details about the request.
  135. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  136. */
  137. public function get_item_permissions_check( $request ) {
  138. $this->retrieve_widgets();
  139. $sidebar = $this->get_sidebar( $request['id'] );
  140. if ( $sidebar && $this->check_read_permission( $sidebar ) ) {
  141. return true;
  142. }
  143. return $this->do_permissions_check();
  144. }
  145. /**
  146. * Checks if a sidebar can be read publicly.
  147. *
  148. * @since 5.9.0
  149. *
  150. * @param array $sidebar The registered sidebar configuration.
  151. * @return bool Whether the side can be read.
  152. */
  153. protected function check_read_permission( $sidebar ) {
  154. return ! empty( $sidebar['show_in_rest'] );
  155. }
  156. /**
  157. * Retrieves one sidebar from the collection.
  158. *
  159. * @since 5.8.0
  160. *
  161. * @param WP_REST_Request $request Full details about the request.
  162. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
  163. */
  164. public function get_item( $request ) {
  165. $this->retrieve_widgets();
  166. $sidebar = $this->get_sidebar( $request['id'] );
  167. if ( ! $sidebar ) {
  168. return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.' ), array( 'status' => 404 ) );
  169. }
  170. return $this->prepare_item_for_response( $sidebar, $request );
  171. }
  172. /**
  173. * Checks if a given request has access to update sidebars.
  174. *
  175. * @since 5.8.0
  176. *
  177. * @param WP_REST_Request $request Full details about the request.
  178. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  179. */
  180. public function update_item_permissions_check( $request ) {
  181. return $this->do_permissions_check();
  182. }
  183. /**
  184. * Updates a sidebar.
  185. *
  186. * @since 5.8.0
  187. *
  188. * @param WP_REST_Request $request Full details about the request.
  189. * @return WP_REST_Response Response object on success, or WP_Error object on failure.
  190. */
  191. public function update_item( $request ) {
  192. if ( isset( $request['widgets'] ) ) {
  193. $sidebars = wp_get_sidebars_widgets();
  194. foreach ( $sidebars as $sidebar_id => $widgets ) {
  195. foreach ( $widgets as $i => $widget_id ) {
  196. // This automatically removes the passed widget IDs from any other sidebars in use.
  197. if ( $sidebar_id !== $request['id'] && in_array( $widget_id, $request['widgets'], true ) ) {
  198. unset( $sidebars[ $sidebar_id ][ $i ] );
  199. }
  200. // This automatically removes omitted widget IDs to the inactive sidebar.
  201. if ( $sidebar_id === $request['id'] && ! in_array( $widget_id, $request['widgets'], true ) ) {
  202. $sidebars['wp_inactive_widgets'][] = $widget_id;
  203. }
  204. }
  205. }
  206. $sidebars[ $request['id'] ] = $request['widgets'];
  207. wp_set_sidebars_widgets( $sidebars );
  208. }
  209. $request['context'] = 'edit';
  210. $sidebar = $this->get_sidebar( $request['id'] );
  211. /**
  212. * Fires after a sidebar is updated via the REST API.
  213. *
  214. * @since 5.8.0
  215. *
  216. * @param array $sidebar The updated sidebar.
  217. * @param WP_REST_Request $request Request object.
  218. */
  219. do_action( 'rest_save_sidebar', $sidebar, $request );
  220. return $this->prepare_item_for_response( $sidebar, $request );
  221. }
  222. /**
  223. * Checks if the user has permissions to make the request.
  224. *
  225. * @since 5.8.0
  226. *
  227. * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
  228. */
  229. protected function do_permissions_check() {
  230. // Verify if the current user has edit_theme_options capability.
  231. // This capability is required to access the widgets screen.
  232. if ( ! current_user_can( 'edit_theme_options' ) ) {
  233. return new WP_Error(
  234. 'rest_cannot_manage_widgets',
  235. __( 'Sorry, you are not allowed to manage widgets on this site.' ),
  236. array( 'status' => rest_authorization_required_code() )
  237. );
  238. }
  239. return true;
  240. }
  241. /**
  242. * Retrieves the registered sidebar with the given id.
  243. *
  244. * @since 5.8.0
  245. *
  246. * @param string|int $id ID of the sidebar.
  247. * @return array|null The discovered sidebar, or null if it is not registered.
  248. */
  249. protected function get_sidebar( $id ) {
  250. return wp_get_sidebar( $id );
  251. }
  252. /**
  253. * Looks for "lost" widgets once per request.
  254. *
  255. * @since 5.9.0
  256. *
  257. * @see retrieve_widgets()
  258. */
  259. protected function retrieve_widgets() {
  260. if ( ! $this->widgets_retrieved ) {
  261. retrieve_widgets();
  262. $this->widgets_retrieved = true;
  263. }
  264. }
  265. /**
  266. * Prepares a single sidebar output for response.
  267. *
  268. * @since 5.8.0
  269. * @since 5.9.0 Renamed `$raw_sidebar` to `$item` to match parent class for PHP 8 named parameter support.
  270. *
  271. * @global array $wp_registered_sidebars The registered sidebars.
  272. * @global array $wp_registered_widgets The registered widgets.
  273. *
  274. * @param array $item Sidebar instance.
  275. * @param WP_REST_Request $request Full details about the request.
  276. * @return WP_REST_Response Prepared response object.
  277. */
  278. public function prepare_item_for_response( $item, $request ) {
  279. global $wp_registered_sidebars, $wp_registered_widgets;
  280. // Restores the more descriptive, specific name for use within this method.
  281. $raw_sidebar = $item;
  282. $id = $raw_sidebar['id'];
  283. $sidebar = array( 'id' => $id );
  284. if ( isset( $wp_registered_sidebars[ $id ] ) ) {
  285. $registered_sidebar = $wp_registered_sidebars[ $id ];
  286. $sidebar['status'] = 'active';
  287. $sidebar['name'] = isset( $registered_sidebar['name'] ) ? $registered_sidebar['name'] : '';
  288. $sidebar['description'] = isset( $registered_sidebar['description'] ) ? wp_sidebar_description( $id ) : '';
  289. $sidebar['class'] = isset( $registered_sidebar['class'] ) ? $registered_sidebar['class'] : '';
  290. $sidebar['before_widget'] = isset( $registered_sidebar['before_widget'] ) ? $registered_sidebar['before_widget'] : '';
  291. $sidebar['after_widget'] = isset( $registered_sidebar['after_widget'] ) ? $registered_sidebar['after_widget'] : '';
  292. $sidebar['before_title'] = isset( $registered_sidebar['before_title'] ) ? $registered_sidebar['before_title'] : '';
  293. $sidebar['after_title'] = isset( $registered_sidebar['after_title'] ) ? $registered_sidebar['after_title'] : '';
  294. } else {
  295. $sidebar['status'] = 'inactive';
  296. $sidebar['name'] = $raw_sidebar['name'];
  297. $sidebar['description'] = '';
  298. $sidebar['class'] = '';
  299. }
  300. $fields = $this->get_fields_for_response( $request );
  301. if ( rest_is_field_included( 'widgets', $fields ) ) {
  302. $sidebars = wp_get_sidebars_widgets();
  303. $widgets = array_filter(
  304. isset( $sidebars[ $sidebar['id'] ] ) ? $sidebars[ $sidebar['id'] ] : array(),
  305. static function ( $widget_id ) use ( $wp_registered_widgets ) {
  306. return isset( $wp_registered_widgets[ $widget_id ] );
  307. }
  308. );
  309. $sidebar['widgets'] = array_values( $widgets );
  310. }
  311. $schema = $this->get_item_schema();
  312. $data = array();
  313. foreach ( $schema['properties'] as $property_id => $property ) {
  314. if ( isset( $sidebar[ $property_id ] ) && true === rest_validate_value_from_schema( $sidebar[ $property_id ], $property ) ) {
  315. $data[ $property_id ] = $sidebar[ $property_id ];
  316. } elseif ( isset( $property['default'] ) ) {
  317. $data[ $property_id ] = $property['default'];
  318. }
  319. }
  320. $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
  321. $data = $this->add_additional_fields_to_object( $data, $request );
  322. $data = $this->filter_response_by_context( $data, $context );
  323. $response = rest_ensure_response( $data );
  324. if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
  325. $response->add_links( $this->prepare_links( $sidebar ) );
  326. }
  327. /**
  328. * Filters the REST API response for a sidebar.
  329. *
  330. * @since 5.8.0
  331. *
  332. * @param WP_REST_Response $response The response object.
  333. * @param array $raw_sidebar The raw sidebar data.
  334. * @param WP_REST_Request $request The request object.
  335. */
  336. return apply_filters( 'rest_prepare_sidebar', $response, $raw_sidebar, $request );
  337. }
  338. /**
  339. * Prepares links for the sidebar.
  340. *
  341. * @since 5.8.0
  342. *
  343. * @param array $sidebar Sidebar.
  344. * @return array Links for the given widget.
  345. */
  346. protected function prepare_links( $sidebar ) {
  347. return array(
  348. 'collection' => array(
  349. 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
  350. ),
  351. 'self' => array(
  352. 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $sidebar['id'] ) ),
  353. ),
  354. 'https://api.w.org/widget' => array(
  355. 'href' => add_query_arg( 'sidebar', $sidebar['id'], rest_url( '/wp/v2/widgets' ) ),
  356. 'embeddable' => true,
  357. ),
  358. );
  359. }
  360. /**
  361. * Retrieves the block type' schema, conforming to JSON Schema.
  362. *
  363. * @since 5.8.0
  364. *
  365. * @return array Item schema data.
  366. */
  367. public function get_item_schema() {
  368. if ( $this->schema ) {
  369. return $this->add_additional_fields_schema( $this->schema );
  370. }
  371. $schema = array(
  372. '$schema' => 'http://json-schema.org/draft-04/schema#',
  373. 'title' => 'sidebar',
  374. 'type' => 'object',
  375. 'properties' => array(
  376. 'id' => array(
  377. 'description' => __( 'ID of sidebar.' ),
  378. 'type' => 'string',
  379. 'context' => array( 'embed', 'view', 'edit' ),
  380. 'readonly' => true,
  381. ),
  382. 'name' => array(
  383. 'description' => __( 'Unique name identifying the sidebar.' ),
  384. 'type' => 'string',
  385. 'context' => array( 'embed', 'view', 'edit' ),
  386. 'readonly' => true,
  387. ),
  388. 'description' => array(
  389. 'description' => __( 'Description of sidebar.' ),
  390. 'type' => 'string',
  391. 'context' => array( 'embed', 'view', 'edit' ),
  392. 'readonly' => true,
  393. ),
  394. 'class' => array(
  395. 'description' => __( 'Extra CSS class to assign to the sidebar in the Widgets interface.' ),
  396. 'type' => 'string',
  397. 'context' => array( 'embed', 'view', 'edit' ),
  398. 'readonly' => true,
  399. ),
  400. 'before_widget' => array(
  401. 'description' => __( 'HTML content to prepend to each widget\'s HTML output when assigned to this sidebar. Default is an opening list item element.' ),
  402. 'type' => 'string',
  403. 'default' => '',
  404. 'context' => array( 'embed', 'view', 'edit' ),
  405. 'readonly' => true,
  406. ),
  407. 'after_widget' => array(
  408. 'description' => __( 'HTML content to append to each widget\'s HTML output when assigned to this sidebar. Default is a closing list item element.' ),
  409. 'type' => 'string',
  410. 'default' => '',
  411. 'context' => array( 'embed', 'view', 'edit' ),
  412. 'readonly' => true,
  413. ),
  414. 'before_title' => array(
  415. 'description' => __( 'HTML content to prepend to the sidebar title when displayed. Default is an opening h2 element.' ),
  416. 'type' => 'string',
  417. 'default' => '',
  418. 'context' => array( 'embed', 'view', 'edit' ),
  419. 'readonly' => true,
  420. ),
  421. 'after_title' => array(
  422. 'description' => __( 'HTML content to append to the sidebar title when displayed. Default is a closing h2 element.' ),
  423. 'type' => 'string',
  424. 'default' => '',
  425. 'context' => array( 'embed', 'view', 'edit' ),
  426. 'readonly' => true,
  427. ),
  428. 'status' => array(
  429. 'description' => __( 'Status of sidebar.' ),
  430. 'type' => 'string',
  431. 'enum' => array( 'active', 'inactive' ),
  432. 'context' => array( 'embed', 'view', 'edit' ),
  433. 'readonly' => true,
  434. ),
  435. 'widgets' => array(
  436. 'description' => __( 'Nested widgets.' ),
  437. 'type' => 'array',
  438. 'items' => array(
  439. 'type' => array( 'object', 'string' ),
  440. ),
  441. 'default' => array(),
  442. 'context' => array( 'embed', 'view', 'edit' ),
  443. ),
  444. ),
  445. );
  446. $this->schema = $schema;
  447. return $this->add_additional_fields_schema( $this->schema );
  448. }
  449. }