class.wpcom-json-api-menus-v1-1-endpoint.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. <?php
  2. abstract class WPCOM_JSON_API_Menus_Abstract_Endpoint extends WPCOM_JSON_API_Endpoint {
  3. protected function switch_to_blog_and_validate_user( $site ) {
  4. $site_id = $this->api->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  5. if ( is_wp_error( $site_id ) ) {
  6. return $site_id;
  7. }
  8. if ( ! current_user_can( 'edit_theme_options' ) ) {
  9. return new WP_Error( 'unauthorised', 'User cannot edit theme options on this site.', 403 );
  10. }
  11. if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
  12. $this->load_theme_functions();
  13. }
  14. return $site_id;
  15. }
  16. protected function get_locations() {
  17. $locations = array();
  18. $menus = get_registered_nav_menus();
  19. if ( !empty( $menus ) ) {
  20. foreach( $menus as $name => $description ) {
  21. $locations[] = array( 'name' => $name, 'description' => $description );
  22. }
  23. }
  24. $locations = array_merge( $locations, WPCOM_JSON_API_Menus_Widgets::get() );
  25. // Primary (first) location should have defaultState -> default,
  26. // all other locations (including widgets) should have defaultState -> empty.
  27. for ( $i = 0; $i < count( $locations ); $i++ ) {
  28. $locations[ $i ]['defaultState'] = $i ? 'empty' : 'default';
  29. }
  30. return $locations;
  31. }
  32. protected function simplify( $data ) {
  33. $simplifier = new WPCOM_JSON_API_Menus_Simplifier( $data );
  34. return $simplifier->translate();
  35. }
  36. protected function complexify( $data ) {
  37. $complexifier = new WPCOM_JSON_API_Menus_Complexify( $data );
  38. return $complexifier->translate();
  39. }
  40. }
  41. abstract class WPCOM_JSON_API_Menus_Translator {
  42. protected $filter = '';
  43. protected $filters = array();
  44. public function __construct( $menus ) {
  45. $this->is_single_menu = ! is_array( $menus );
  46. $this->menus = is_array( $menus ) ? $menus : array( $menus );
  47. }
  48. public function translate() {
  49. $result = $this->menus;
  50. foreach ( $this->filters as $f ) {
  51. $result = call_user_func( array( $this, $f ), $result );
  52. if ( is_wp_error($result ) ) {
  53. return $result;
  54. }
  55. }
  56. return $this->maybe_extract( $result );
  57. }
  58. protected function maybe_extract( $menus ) {
  59. return $this->is_single_menu ? $menus[0] : $menus;
  60. }
  61. public function whitelist_and_rename_with( $object, $dict ) {
  62. $keys = array_keys( $dict );
  63. $return = array();
  64. foreach ( (array) $object as $k => $v ) {
  65. if ( in_array( $k, $keys ) ) {
  66. if ( is_array( $dict[ $k ] ) ) {
  67. settype( $v, $dict[ $k ]['type'] );
  68. $return[ $dict[ $k ]['name'] ] = $v;
  69. } else {
  70. $new_k = $dict[ $k ];
  71. $return[ $new_k ] = $v;
  72. }
  73. }
  74. }
  75. return $return;
  76. }
  77. }
  78. class WPCOM_JSON_API_Menus_Simplifier extends WPCOM_JSON_API_Menus_Translator {
  79. protected $filter = 'wpcom_menu_api_translator_simplify';
  80. protected $filters = array(
  81. 'whitelist_and_rename_keys',
  82. 'add_locations',
  83. 'treeify',
  84. 'add_widget_locations',
  85. );
  86. protected $menu_whitelist = array(
  87. 'term_id' => array( 'name' => 'id', 'type' => 'int' ),
  88. 'name' => array( 'name' => 'name', 'type' => 'string' ),
  89. 'description' => array( 'name' => 'description', 'type' => 'string' ),
  90. 'items' => array( 'name' => 'items', 'type' => 'array' ),
  91. );
  92. protected $menu_item_whitelist = array(
  93. 'db_id' => array( 'name' => 'id', 'type' => 'int' ),
  94. 'object_id' => array( 'name' => 'content_id', 'type' => 'int' ),
  95. 'object' => array( 'name' => 'type', 'type' => 'string' ),
  96. 'type' => array( 'name' => 'type_family', 'type' => 'string' ),
  97. 'type_label' => array( 'name' => 'type_label', 'type' => 'string' ),
  98. 'title' => array( 'name' => 'name', 'type' => 'string' ),
  99. 'menu_order' => array( 'name' => 'order', 'type' => 'int' ),
  100. 'menu_item_parent' => array( 'name' => 'parent', 'type' => 'int' ),
  101. 'url' => array( 'name' => 'url', 'type' => 'string' ),
  102. 'target' => array( 'name' => 'link_target', 'type' => 'string' ),
  103. 'attr_title' => array( 'name' => 'link_title', 'type' => 'string' ),
  104. 'description' => array( 'name' => 'description', 'type' => 'string' ),
  105. 'classes' => array( 'name' => 'classes', 'type' => 'array' ),
  106. 'xfn' => array( 'name' => 'xfn', 'type' => 'string' ),
  107. );
  108. /**************************
  109. * Filters methods
  110. **************************/
  111. public function treeify( $menus ) {
  112. return array_map( array( $this, 'treeify_menu' ), $menus );
  113. }
  114. // turn the flat item list into a tree of items
  115. protected function treeify_menu( $menu ) {
  116. $indexed_nodes = array();
  117. $tree = array();
  118. foreach( $menu['items'] as &$item ) {
  119. $indexed_nodes[ $item['id'] ] = &$item;
  120. }
  121. foreach( $menu['items'] as &$item ) {
  122. if ( $item['parent'] && isset( $indexed_nodes[ $item['parent'] ] ) ) {
  123. $parent_node = &$indexed_nodes[ $item['parent'] ];
  124. if ( !isset( $parent_node['items'] ) ) {
  125. $parent_node['items'] = array();
  126. }
  127. $parent_node['items'][ $item['order'] ] = &$item;
  128. } else {
  129. $tree[ $item['order'] ] = &$item;
  130. }
  131. unset( $item['order'] );
  132. unset( $item['parent'] );
  133. }
  134. $menu['items'] = $tree;
  135. $this->remove_item_keys( $menu );
  136. return $menu;
  137. }
  138. // recursively ensure item lists are contiguous
  139. protected function remove_item_keys( &$item ) {
  140. if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
  141. return;
  142. }
  143. foreach( $item['items'] as &$it ) {
  144. $this->remove_item_keys( $it );
  145. }
  146. $item['items'] = array_values( $item['items'] );
  147. }
  148. protected function whitelist_and_rename_keys( $menus ) {
  149. $transformed_menus = array();
  150. foreach ( $menus as $menu ) {
  151. $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
  152. if ( isset( $menu['items'] ) ) {
  153. foreach ( $menu['items'] as &$item ) {
  154. $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
  155. }
  156. }
  157. $transformed_menus[] = $menu;
  158. }
  159. return $transformed_menus;
  160. }
  161. protected function add_locations( $menus ) {
  162. $menus_with_locations = array();
  163. foreach( $menus as $menu ) {
  164. $menu['locations'] = array_keys( get_nav_menu_locations(), $menu['id'] );
  165. $menus_with_locations[] = $menu;
  166. }
  167. return $menus_with_locations;
  168. }
  169. protected function add_widget_locations( $menus ) {
  170. $nav_menu_widgets = WPCOM_JSON_API_Menus_Widgets::get();
  171. if ( ! is_array( $nav_menu_widgets ) ) {
  172. return $menus;
  173. }
  174. foreach ( $menus as &$menu ) {
  175. $widget_locations = array();
  176. foreach ( $nav_menu_widgets as $key => $widget ) {
  177. if ( is_array( $widget ) && isset( $widget['nav_menu'] ) &&
  178. $widget['nav_menu'] === $menu['id'] ) {
  179. $widget_locations[] = 'nav_menu_widget-' . $key;
  180. }
  181. }
  182. $menu['locations'] = array_merge( $menu['locations'], $widget_locations );
  183. }
  184. return $menus;
  185. }
  186. }
  187. class WPCOM_JSON_API_Menus_Complexify extends WPCOM_JSON_API_Menus_Translator {
  188. protected $filter = 'wpcom_menu_api_translator_complexify';
  189. protected $filters = array(
  190. 'untreeify',
  191. 'set_locations',
  192. 'whitelist_and_rename_keys',
  193. );
  194. protected $menu_whitelist = array(
  195. 'id' => 'term_id',
  196. 'name' => 'menu-name',
  197. 'description' => 'description',
  198. 'items' => 'items',
  199. );
  200. protected $menu_item_whitelist = array(
  201. 'id' => 'menu-item-db-id',
  202. 'content_id' => 'menu-item-object-id',
  203. 'type' => 'menu-item-object',
  204. 'type_family' => 'menu-item-type',
  205. 'type_label' => 'menu-item-type-label',
  206. 'name' => 'menu-item-title',
  207. 'order' => 'menu-item-position',
  208. 'parent' => 'menu-item-parent-id',
  209. 'url' => 'menu-item-url',
  210. 'link_target' => 'menu-item-target',
  211. 'link_title' => 'menu-item-attr-title',
  212. 'status' => 'menu-item-status',
  213. 'tmp_id' => 'tmp_id',
  214. 'tmp_parent' => 'tmp_parent',
  215. 'description' => 'menu-item-description',
  216. 'classes' => 'menu-item-classes',
  217. 'xfn' => 'menu-item-xfn',
  218. );
  219. /**************************
  220. * Filters methods
  221. **************************/
  222. public function untreeify( $menus ) {
  223. return array_map( array( $this, 'untreeify_menu' ), $menus );
  224. }
  225. // convert the tree of menu items to a flat list suitable for
  226. // the nav_menu APIs
  227. protected function untreeify_menu( $menu ) {
  228. if ( empty( $menu['items'] ) ) {
  229. return $menu;
  230. }
  231. $items_list = array();
  232. $counter = 1;
  233. foreach ( $menu['items'] as &$item ) {
  234. $item[ 'parent' ] = 0;
  235. }
  236. $this->untreeify_items( $menu['items'], $items_list, $counter );
  237. $menu['items'] = $items_list;
  238. return $menu;
  239. }
  240. /**
  241. * Recurse the items tree adding each item to a flat list and restoring
  242. * `order` and `parent` fields.
  243. *
  244. * @param array $items item tree
  245. * @param array &$items_list output flat list of items
  246. * @param int &$counter for creating temporary IDs
  247. */
  248. protected function untreeify_items( $items, &$items_list, &$counter ) {
  249. foreach( $items as $index => $item ) {
  250. $item['order'] = $index + 1;
  251. if( ! isset( $item['id'] ) ) {
  252. $this->set_tmp_id( $item, $counter++ );
  253. }
  254. if ( isset( $item['items'] ) && is_array( $item['items'] ) ) {
  255. foreach ( $item['items'] as &$i ) {
  256. $i['parent'] = $item['id'];
  257. }
  258. $this->untreeify_items( $item[ 'items' ], $items_list, $counter );
  259. unset( $item['items'] );
  260. }
  261. $items_list[] = $item;
  262. }
  263. }
  264. /**
  265. * Populate `tmp_id` field for a new item, and `tmp_parent` field
  266. * for all its children, to maintain the hierarchy.
  267. * These fields will be used when creating
  268. * new items with wp_update_nav_menu_item().
  269. */
  270. private function set_tmp_id( &$item, $tmp_id ) {
  271. $item['tmp_id'] = $tmp_id;
  272. if ( ! isset( $item['items'] ) || ! is_array( $item['items'] ) ) {
  273. return;
  274. }
  275. foreach ( $item['items'] as &$child ) {
  276. $child['tmp_parent'] = $tmp_id;
  277. }
  278. }
  279. protected function whitelist_and_rename_keys( $menus ) {
  280. $transformed_menus = array();
  281. foreach ( $menus as $menu ) {
  282. $menu = $this->whitelist_and_rename_with( $menu, $this->menu_whitelist );
  283. if ( isset( $menu['items'] ) ) {
  284. $menu['items'] = array_map( array( $this, 'whitelist_and_rename_item_keys' ), $menu['items'] );
  285. }
  286. $transformed_menus[] = $menu;
  287. }
  288. return $transformed_menus;
  289. }
  290. protected function whitelist_and_rename_item_keys( $item ) {
  291. $item = $this->implode_array_fields( $item );
  292. $item = $this->whitelist_and_rename_with( $item, $this->menu_item_whitelist );
  293. return $item;
  294. }
  295. // all item fields are set as strings
  296. protected function implode_array_fields( $menu_item ) {
  297. return array_map( array( $this, 'implode_array_field' ), $menu_item );
  298. }
  299. protected function implode_array_field( $field ) {
  300. if ( is_array( $field ) ) {
  301. return implode( ' ', $field );
  302. }
  303. return $field;
  304. }
  305. protected function set_locations( $menus ) {
  306. foreach ( $menus as $menu ) {
  307. if ( isset( $menu['locations'] ) ) {
  308. if ( true !== $this->locations_are_valid( $menu['locations'] ) ) {
  309. return $this->locations_are_valid( $menu['locations'] );
  310. }
  311. }
  312. }
  313. return array_map( array( $this, 'set_location' ), $menus );
  314. }
  315. protected function set_location( $menu ) {
  316. $this->set_menu_at_locations( $menu['locations'], $menu['id'] );
  317. return $menu;
  318. }
  319. protected function set_menu_at_locations( $locations, $menu_id ) {
  320. $location_map = get_nav_menu_locations();
  321. $this->remove_menu_from_all_locations( $menu_id, $location_map );
  322. if ( is_array( $locations ) ) {
  323. foreach ( $locations as $location ) {
  324. $location_map[ $location ] = $menu_id;
  325. }
  326. }
  327. set_theme_mod( 'nav_menu_locations', $location_map );
  328. $this->set_widget_menu_at_locations( $locations, $menu_id );
  329. }
  330. protected function remove_menu_from_all_locations( $menu_id, &$location_map ) {
  331. foreach ( get_nav_menu_locations() as $existing_location => $existing_menu_id) {
  332. if ( $existing_menu_id == $menu_id ) {
  333. unset( $location_map[$existing_location] );
  334. }
  335. }
  336. }
  337. protected function set_widget_menu_at_locations( $locations, $menu_id ) {
  338. $nav_menu_widgets = get_option( 'widget_nav_menu' );
  339. if ( ! is_array( $nav_menu_widgets ) ) {
  340. return;
  341. }
  342. // Remove menus from all custom menu widget locations
  343. foreach ( $nav_menu_widgets as &$widget ) {
  344. if ( is_array( $widget ) && isset( $widget['nav_menu'] ) && $widget['nav_menu'] == $menu_id ) {
  345. $widget['nav_menu'] = 0;
  346. }
  347. }
  348. if ( is_array( $locations ) ) {
  349. foreach ( $locations as $location ) {
  350. if ( preg_match( '/^nav_menu_widget-(\d+)/', $location, $matches ) ) {
  351. if ( isset( $matches[1] ) ) {
  352. $nav_menu_widgets[$matches[1]]['nav_menu'] = $menu_id;
  353. }
  354. }
  355. }
  356. }
  357. update_option( 'widget_nav_menu', $nav_menu_widgets );
  358. }
  359. protected function locations_are_valid( $locations ) {
  360. if ( is_int( $locations ) ) {
  361. if ( $locations != 0) {
  362. return new WP_Error( 'locations-int', 'Locations int must be 0.', 400 );
  363. } else {
  364. return true;
  365. }
  366. } elseif ( is_array( $locations ) ) {
  367. foreach ( $locations as $location_name ) {
  368. if ( ! $this->location_name_exists( $location_name ) ) {
  369. return new WP_Error( 'locations-array',
  370. sprintf( "Location '%s' does not exist.", $location_name ), 404 );
  371. }
  372. }
  373. return true;
  374. }
  375. return new WP_Error( 'locations', 'Locations must be array or integer.', 400 );
  376. }
  377. protected function location_name_exists( $location_name ) {
  378. $widget_location_names = wp_list_pluck( WPCOM_JSON_API_Menus_Widgets::get(), 'name' );
  379. $existing_locations = get_nav_menu_locations();
  380. if ( ! is_array( get_registered_nav_menus() ) ) {
  381. return false;
  382. }
  383. return array_key_exists( $location_name, get_registered_nav_menus() ) ||
  384. array_key_exists( $location_name, $existing_locations ) ||
  385. in_array( $location_name, $widget_location_names );
  386. }
  387. }
  388. new WPCOM_JSON_API_Menus_New_Menu_Endpoint( array (
  389. 'method' => 'POST',
  390. 'description' => 'Create a new navigation menu.',
  391. 'group' => 'menus',
  392. 'stat' => 'menus:new-menu',
  393. 'path' => '/sites/%s/menus/new',
  394. 'path_labels' => array(
  395. '$site' => '(int|string) Site ID or domain',
  396. ),
  397. 'request_format' => array(
  398. 'name' => '(string) Name of menu',
  399. ),
  400. 'response_format' => array(
  401. 'id' => '(int) Newly created menu ID',
  402. ),
  403. 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/new',
  404. 'example_request_data' => array(
  405. 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
  406. 'body' => array(
  407. 'name' => 'Menu 1'
  408. )
  409. ),
  410. ) );
  411. class WPCOM_JSON_API_Menus_New_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
  412. function callback( $path = '', $site = 0 ) {
  413. $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  414. if ( is_wp_error( $site_id ) ) {
  415. return $site_id;
  416. }
  417. $data = $this->input();
  418. $id = wp_create_nav_menu( $data['name'] );
  419. if ( is_wp_error( $id ) ) {
  420. return $id;
  421. }
  422. return array( 'id' => $id );
  423. }
  424. }
  425. new WPCOM_JSON_API_Menus_Update_Menu_Endpoint( array (
  426. 'method' => 'POST',
  427. 'description' => 'Update a navigation menu.',
  428. 'group' => 'menus',
  429. 'stat' => 'menus:update-menu',
  430. 'path' => '/sites/%s/menus/%d',
  431. 'path_labels' => array(
  432. '$site' => '(int|string) Site ID or domain',
  433. '$menu_id' => '(int) Menu ID',
  434. ),
  435. 'request_format' => array(
  436. 'name' => '(string) Name of menu',
  437. 'items' => '(array) A list of menu item objects.
  438. <br/><br/>
  439. Item objects contain fields relating to that item, e.g. id, type, content_id,
  440. but they can also contain other items objects - this nesting represents parents
  441. and child items in the item tree.'
  442. ),
  443. 'response_format' => array(
  444. 'menu' => '(object) Updated menu object',
  445. ),
  446. 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
  447. 'example_request_data' => array(
  448. 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
  449. 'body' => array(
  450. 'name' => 'Test Menu'
  451. ),
  452. ),
  453. ) );
  454. class WPCOM_JSON_API_Menus_Update_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
  455. function callback( $path = '', $site = 0, $menu_id = 0 ) {
  456. $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  457. if ( is_wp_error( $site_id ) ) {
  458. return $site_id;
  459. }
  460. if ( $menu_id <= 0 ) {
  461. return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
  462. }
  463. $data = $this->input( true, false );
  464. $data['id'] = $menu_id;
  465. $data = $this->complexify( array( $data ) );
  466. if ( is_wp_error( $data ) ) {
  467. return $data;
  468. }
  469. $data = $data[0];
  470. // Avoid special-case handling of an unset 'items' field in empty menus
  471. $data['items'] = isset( $data['items'] ) ? $data['items'] : array();
  472. $data = $this->create_new_items( $data, $menu_id );
  473. $result = wp_update_nav_menu_object( $menu_id, array( 'menu-name' => $data['menu-name'] ) );
  474. if ( is_wp_error( $result ) ) {
  475. return $result;
  476. }
  477. $delete_status = $this->delete_items_not_present( $menu_id, $data['items'] );
  478. if( is_wp_error( $delete_status ) ) {
  479. return $delete_status;
  480. }
  481. foreach ( $data['items'] as $item ) {
  482. $item_id = isset( $item['menu-item-db-id'] ) ? $item['menu-item-db-id'] : 0;
  483. $result = wp_update_nav_menu_item( $menu_id, $item_id, $item );
  484. if ( is_wp_error( $result ) ) {
  485. return $result;
  486. }
  487. }
  488. $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
  489. if ( is_wp_error( $items ) ) {
  490. return $items;
  491. }
  492. $menu = wp_get_nav_menu_object( $menu_id );
  493. $menu->items = $items;
  494. return array( 'menu' => $this->simplify( $menu ) );
  495. }
  496. /**
  497. * New items can have a 'tmp_id', allowing them to
  498. * be used as parent items before they have been created.
  499. *
  500. * This function will create items that have a 'tmp_id' set, and
  501. * update any items with a 'tmp_parent' to use the
  502. * newly created item as a parent.
  503. */
  504. function create_new_items( $data, $menu_id ) {
  505. $tmp_to_actual_ids = array();
  506. foreach ( $data['items'] as &$item ) {
  507. if ( isset( $item['tmp_id'] ) ) {
  508. $actual_id = wp_update_nav_menu_item( $menu_id, 0, $item );
  509. $tmp_to_actual_ids[ $item['tmp_id'] ] = $actual_id;
  510. unset( $item['tmp_id'] );
  511. $item['menu-item-db-id'] = $actual_id;
  512. }
  513. }
  514. foreach ( $data['items'] as &$item ) {
  515. if ( isset( $item['tmp_parent'] ) ) {
  516. $item['menu-item-parent-id'] = $tmp_to_actual_ids[ $item['tmp_parent'] ];
  517. unset( $item['tmp_parent'] );
  518. }
  519. }
  520. return $data;
  521. }
  522. /**
  523. * remove any existing menu items not present in the supplied array.
  524. * returns wp_error if an item cannot be deleted.
  525. */
  526. function delete_items_not_present( $menu_id, $menu_items ) {
  527. $existing_items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
  528. if ( ! is_array( $existing_items ) ) {
  529. return true;
  530. }
  531. $existing_ids = wp_list_pluck( $existing_items, 'db_id' );
  532. $ids_to_keep = wp_list_pluck( $menu_items, 'menu-item-db-id' );
  533. $ids_to_remove = array_diff( $existing_ids, $ids_to_keep );
  534. foreach ( $ids_to_remove as $id ) {
  535. if ( false === wp_delete_post( $id, true ) ) {
  536. return new WP_Error( 'menu-item',
  537. sprintf( 'Failed to delete menu item with id: %d.', $id ), 400 );
  538. }
  539. }
  540. return true;
  541. }
  542. }
  543. new WPCOM_JSON_API_Menus_List_Menus_Endpoint( array (
  544. 'method'=> 'GET',
  545. 'description' => 'Get a list of all navigation menus.',
  546. 'group' => 'menus',
  547. 'stat' => 'menus:list-menu',
  548. 'path' => '/sites/%s/menus',
  549. 'path_labels' => array(
  550. '$site' => '(int|string) Site ID or domain',
  551. ),
  552. 'response_format' => array(
  553. 'menus' => '(array) A list of menu objects.<br/><br/>
  554. A menu object contains a name, items, locations, etc.
  555. Check the example response for the full structure.
  556. <br/><br/>
  557. Item objects contain fields relating to that item, e.g. id, type, content_id,
  558. but they can also contain other items objects - this nesting represents parents
  559. and child items in the item tree.',
  560. 'locations' => '(array) Locations where menus can be placed. List of objects, one per location.'
  561. ),
  562. 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus',
  563. 'example_request_data' => array(
  564. 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
  565. ),
  566. ) );
  567. class WPCOM_JSON_API_Menus_List_Menus_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
  568. function callback( $path = '', $site = 0 ) {
  569. $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  570. if ( is_wp_error( $site_id ) ) {
  571. return $site_id;
  572. }
  573. $menus = wp_get_nav_menus( array( 'orderby' => 'term_id' ) );
  574. if ( is_wp_error( $menus ) ) {
  575. return $menus;
  576. }
  577. foreach ( $menus as $m ) {
  578. $items = wp_get_nav_menu_items( $m->term_id, array( 'update_post_term_cache' => false ) );
  579. if ( is_wp_error( $items ) ) {
  580. return $items;
  581. }
  582. $m->items = $items;
  583. }
  584. $menus = $this->simplify( $menus );
  585. if ( is_wp_error( $this->get_locations() ) ) {
  586. return $this->get_locations();
  587. }
  588. return array( 'menus' => $menus, 'locations' => $this->get_locations() );
  589. }
  590. }
  591. new WPCOM_JSON_API_Menus_Get_Menu_Endpoint( array (
  592. 'method'=> 'GET',
  593. 'description' => 'Get a single navigation menu.',
  594. 'group' => 'menus',
  595. 'stat' => 'menus:get-menu',
  596. 'path' => '/sites/%s/menus/%d',
  597. 'path_labels' => array(
  598. '$site' => '(int|string) Site ID or domain',
  599. '$menu_id' => '(int) Menu ID',
  600. ),
  601. 'response_format' => array(
  602. 'menu' => '(object) A menu object.<br/><br/>
  603. A menu object contains a name, items, locations, etc.
  604. Check the example response for the full structure.
  605. <br/><br/>
  606. Item objects contain fields relating to that item, e.g. id, type, content_id,
  607. but they can also contain other items objects - this nesting represents parents
  608. and child items in the item tree.'
  609. ),
  610. 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/510604099',
  611. 'example_request_data' => array(
  612. 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
  613. ),
  614. ) );
  615. class WPCOM_JSON_API_Menus_Get_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
  616. function callback( $path = '', $site = 0, $menu_id = 0 ) {
  617. $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  618. if ( is_wp_error( $site_id ) ) {
  619. return $site_id;
  620. }
  621. if ( $menu_id <= 0 ) {
  622. return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
  623. }
  624. $menu = get_term( $menu_id, 'nav_menu' );
  625. if ( is_wp_error( $menu ) ) {
  626. return $menu;
  627. }
  628. $items = wp_get_nav_menu_items( $menu_id, array( 'update_post_term_cache' => false ) );
  629. if ( is_wp_error( $items ) ) {
  630. return $items;
  631. }
  632. $menu->items = $items;
  633. return array( 'menu' => $this->simplify( $menu ) );
  634. }
  635. }
  636. new WPCOM_JSON_API_Menus_Delete_Menu_Endpoint( array (
  637. 'method' => 'POST',
  638. 'description' => 'Delete a navigation menu',
  639. 'group' => 'menus',
  640. 'stat' => 'menus:delete-menu',
  641. 'path' => '/sites/%s/menus/%d/delete',
  642. 'path_labels' => array(
  643. '$site' => '(int|string) Site ID or domain',
  644. '$menu_id' => '(int) Menu ID',
  645. ),
  646. 'response_format' => array(
  647. 'deleted' => '(bool) Has the menu been deleted?',
  648. ),
  649. 'example_request' => 'https://public-api.wordpress.com/rest/v1.1/sites/82974409/menus/$menu_id/delete',
  650. 'example_request_data' => array(
  651. 'headers' => array( 'authorization' => 'Bearer YOUR_API_TOKEN' ),
  652. ),
  653. ) );
  654. class WPCOM_JSON_API_Menus_Delete_Menu_Endpoint extends WPCOM_JSON_API_Menus_Abstract_Endpoint {
  655. function callback( $path = '', $site = 0, $menu_id = 0 ) {
  656. $site_id = $this->switch_to_blog_and_validate_user( $this->api->get_blog_id( $site ) );
  657. if ( is_wp_error( $site_id ) ) {
  658. return $site_id;
  659. }
  660. if ( $menu_id <= 0 ) {
  661. return new WP_Error( 'menu-id', 'Menu ID must be greater than 0.', 400 );
  662. }
  663. $result = wp_delete_nav_menu( $menu_id );
  664. if ( ! is_wp_error( $result ) ) {
  665. $result = array( 'deleted' => $result );
  666. }
  667. return $result;
  668. }
  669. }
  670. class WPCOM_JSON_API_Menus_Widgets {
  671. static function get() {
  672. $locations = array();
  673. $nav_menu_widgets = get_option( 'widget_nav_menu' );
  674. if ( ! is_array( $nav_menu_widgets ) ) {
  675. return $locations;
  676. }
  677. foreach ( $nav_menu_widgets as $k => $v ) {
  678. if ( is_array( $v ) && isset( $v['title'] ) ) {
  679. $locations[$k] = array( 'name' => 'nav_menu_widget-' . $k, 'description' => $v['title'] );
  680. }
  681. }
  682. return $locations;
  683. }
  684. }