PageRenderTime 93ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/admin-menu-editor/includes/menu-editor-core.php

https://gitlab.com/blueprintmrk/bladencountyrecords
PHP | 1190 lines | 714 code | 159 blank | 317 comment | 115 complexity | e5fcbfcc9311eed6bb734c335f5effb6 MD5 | raw file
  1. <?php
  2. //Can't have two different versions of the plugin active at the same time. It would be incredibly buggy.
  3. if (class_exists('WPMenuEditor')){
  4. trigger_error(
  5. 'Another version of Admin Menu Editor is already active. Please deactivate it before activating this one.',
  6. E_USER_ERROR
  7. );
  8. }
  9. //Load the "framework"
  10. require 'shadow_plugin_framework.php';
  11. if ( !class_exists('WPMenuEditor') ) :
  12. class WPMenuEditor extends MenuEd_ShadowPluginFramework {
  13. protected $default_wp_menu = null; //Holds the default WP menu for later use in the editor
  14. protected $default_wp_submenu = null; //Holds the default WP menu for later use
  15. protected $title_lookups = array(); //A list of page titles indexed by $item['file']. Used to
  16. //fix the titles of moved plugin pages.
  17. private $custom_menu = null; //The current custom menu with defaults merged in
  18. public $menu_format_version = 4;
  19. private $templates = null; //Template arrays for various menu structures. See the constructor for details.
  20. function init(){
  21. //Determine if the plugin is active network-wide (i.e. either installed in
  22. //the /mu-plugins/ directory or activated "network wide" by the super admin.
  23. if ( $this->is_super_plugin() ){
  24. $this->sitewide_options = true;
  25. }
  26. //Set some plugin-specific options
  27. if ( empty($this->option_name) ){
  28. $this->option_name = 'ws_menu_editor';
  29. }
  30. $this->defaults = array(
  31. 'hide_advanced_settings' => true,
  32. 'menu_format_version' => 0,
  33. );
  34. $this->serialize_with_json = false; //(Don't) store the options in JSON format
  35. $this->settings_link = 'options-general.php?page=menu_editor';
  36. $this->magic_hooks = true;
  37. $this->magic_hook_priority = 99999;
  38. //Build some template arrays
  39. $this->templates['basic_defaults'] = array(
  40. 'page_title' => '',
  41. 'menu_title' => '',
  42. 'access_level' => 'read',
  43. 'file' => '',
  44. 'css_class' => '',
  45. 'hookname' => '',
  46. 'icon_url' => '',
  47. 'position' => 0,
  48. 'separator' => false,
  49. 'custom' => false,
  50. 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default)
  51. );
  52. //Template for a basic top-level menu
  53. $this->templates['blank_menu'] = array(
  54. 'page_title' => null,
  55. 'menu_title' => null,
  56. 'access_level' => null,
  57. 'file' => null,
  58. 'css_class' => null,
  59. 'hookname' => null,
  60. 'icon_url' => null,
  61. 'position' => null,
  62. 'separator' => null,
  63. 'custom' => null,
  64. 'open_in' => null,
  65. 'defaults' => $this->templates['basic_defaults'],
  66. 'items' => array(),
  67. );
  68. //Template for menu items
  69. $this->templates['blank_item'] = array(
  70. 'menu_title' => null,
  71. 'access_level' => null,
  72. 'file' => null,
  73. 'page_title' => null,
  74. 'position' => null,
  75. 'custom' => null,
  76. 'open_in' => null,
  77. 'defaults' => $this->templates['basic_defaults'],
  78. );
  79. //AJAXify screen options
  80. add_action( 'wp_ajax_ws_ame_save_screen_options', array(&$this,'ajax_save_screen_options') );
  81. }
  82. /**
  83. * Activation hook
  84. *
  85. * @return void
  86. */
  87. function activate(){
  88. //If we have no stored settings for this version of the plugin, try importing them
  89. //from other versions (i.e. the free or the Pro version).
  90. if ( !$this->load_options() ){
  91. $this->import_settings();
  92. }
  93. parent::activate();
  94. }
  95. /**
  96. * Import settings from a different version of the plugin.
  97. *
  98. * @return bool True if settings were imported successfully, False otherwise
  99. */
  100. function import_settings(){
  101. $possible_names = array('ws_menu_editor', 'ws_menu_editor_pro');
  102. foreach($possible_names as $option_name){
  103. if ( $this->load_options($option_name) ){
  104. return true;
  105. }
  106. }
  107. return false;
  108. }
  109. /**
  110. * Add the JS required by the editor to the page header
  111. *
  112. * @return void
  113. */
  114. function enqueue_scripts(){
  115. //jQuery JSON plugin
  116. wp_enqueue_script('jquery-json', $this->plugin_dir_url.'/js/jquery.json-1.3.js', array('jquery'), '1.3');
  117. //jQuery sort plugin
  118. wp_enqueue_script('jquery-sort', $this->plugin_dir_url.'/js/jquery.sort.js', array('jquery'));
  119. //Editor's scipts
  120. wp_enqueue_script(
  121. 'menu-editor',
  122. $this->plugin_dir_url.'/js/menu-editor.js',
  123. array('jquery', 'jquery-ui-sortable', 'jquery-ui-dialog', 'jquery-form'),
  124. '1.0'
  125. );
  126. }
  127. /**
  128. * Add the editor's CSS file to the page header
  129. *
  130. * @return void
  131. */
  132. function enqueue_styles(){
  133. wp_enqueue_style('menu-editor-style', $this->plugin_dir_url . '/css/menu-editor.css', array(), '1.0');
  134. }
  135. /**
  136. * Create a configuration page and load the custom menu
  137. *
  138. * @return void
  139. */
  140. function hook_admin_menu(){
  141. global $menu, $submenu;
  142. //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1
  143. $reset_requested = isset($_GET['reset_admin_menu']) && $_GET['reset_admin_menu'];
  144. if ( $reset_requested && $this->current_user_can_edit_menu() ){
  145. $this->options['custom_menu'] = null;
  146. $this->save_options();
  147. }
  148. //The menu editor is only visible to users with the manage_options privilege.
  149. //Or, if the plugin is installed in mu-plugins, only to the site administrator(s).
  150. if ( $this->current_user_can_edit_menu() ){
  151. $page = add_options_page(
  152. apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'),
  153. apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'),
  154. 'manage_options',
  155. 'menu_editor',
  156. array(&$this, 'page_menu_editor')
  157. );
  158. //Output our JS & CSS on that page only
  159. add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts'));
  160. add_action("admin_print_styles-$page", array(&$this, 'enqueue_styles'));
  161. //Make a placeholder for our screen options (hacky)
  162. add_meta_box("ws-ame-screen-options", "You should never see this", array(&$this, 'noop'), $page);
  163. }
  164. //WP 3.0 in multisite mode has two separators with the same filename. This plugin
  165. //expects all top-level menus to have unique filenames/URLs.
  166. $first_separator1 = -1;
  167. $last_separator1 = -1;
  168. foreach($menu as $index => $item){
  169. if ( $item[2] == 'separator1' ){
  170. $last_separator1 = $index;
  171. if ( $first_separator1 == -1 ){
  172. $first_separator1 = $index;
  173. }
  174. }
  175. }
  176. if ( $first_separator1 != $last_separator1 ){
  177. $menu[$first_separator1][2] = 'separator0';
  178. }
  179. //Store the "original" menus for later use in the editor
  180. $this->default_wp_menu = $menu;
  181. $this->default_wp_submenu = $submenu;
  182. //Is there a custom menu to use?
  183. if ( !empty($this->options['custom_menu']) ){
  184. //Check if we need to upgrade the menu structure
  185. if ( empty($this->options['menu_format_version']) || ($this->options['menu_format_version'] < $this->menu_format_version) ){
  186. $this->options['custom_menu'] = $this->upgrade_menu_structure($this->options['custom_menu']);
  187. $this->options['menu_format_version'] = $this->menu_format_version;
  188. $this->save_options();
  189. }
  190. //Merge in data from the default menu
  191. $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu);
  192. //Apply the custom menu
  193. list($menu, $submenu, $this->title_lookups) = $this->tree2wp($tree);
  194. //Save for later - the editor page will need it
  195. $this->custom_menu = $tree;
  196. //Re-filter the menu (silly WP should do that itself, oh well)
  197. $this->filter_menu();
  198. }
  199. }
  200. /**
  201. * Determine if the current user may use the menu editor.
  202. *
  203. * @return bool
  204. */
  205. function current_user_can_edit_menu(){
  206. if ( $this->is_super_plugin() ){
  207. return is_super_admin();
  208. } else {
  209. return current_user_can('manage_options');
  210. }
  211. }
  212. /**
  213. * Intercept a handy action to fix the page title for moved plugin pages.
  214. *
  215. * @return void
  216. */
  217. function hook_admin_xml_ns(){
  218. global $title;
  219. global $pagenow;
  220. global $plugin_page;
  221. if ( empty($title) && !empty($plugin_page) && !empty($pagenow) ){
  222. $file = sprintf('%s?page=%s', $pagenow, $plugin_page);
  223. if ( isset($this->title_lookups[$file]) ){
  224. $title = esc_html( strip_tags( $this->title_lookups[$file] ) );
  225. }
  226. }
  227. }
  228. /**
  229. * Loop over the Dashboard submenus and remove pages for which the current user does not have privs.
  230. *
  231. * @return void
  232. */
  233. function filter_menu(){
  234. global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv;
  235. foreach ( array( 'submenu' ) as $sub_loop ) {
  236. foreach ($$sub_loop as $parent => $sub) {
  237. foreach ($sub as $index => $data) {
  238. if ( ! current_user_can($data[1]) ) {
  239. unset(${$sub_loop}[$parent][$index]);
  240. $_wp_submenu_nopriv[$parent][$data[2]] = true;
  241. }
  242. }
  243. if ( empty(${$sub_loop}[$parent]) )
  244. unset(${$sub_loop}[$parent]);
  245. }
  246. }
  247. }
  248. /**
  249. * Encode a menu tree as JSON
  250. *
  251. * @param array $tree
  252. * @return string
  253. */
  254. function getMenuAsJS($tree){
  255. return $this->json_encode($tree);
  256. }
  257. /**
  258. * Convert a WP menu structure to an associative array
  259. *
  260. * @param array $item An element of the $menu array
  261. * @param integer $pos The position (index) of the menu item
  262. * @return array
  263. */
  264. function menu2assoc($item, $pos=0){
  265. $item = array(
  266. 'menu_title' => $item[0],
  267. 'access_level' => $item[1],
  268. 'file' => $item[2],
  269. 'page_title' => $item[3],
  270. 'css_class' => $item[4],
  271. 'hookname' => (isset($item[5])?$item[5]:''), //ID
  272. 'icon_url' => (isset($item[6])?$item[6]:''),
  273. 'position' => $pos,
  274. );
  275. $item['separator'] = strpos($item['css_class'], 'wp-menu-separator') !== false;
  276. //Flag plugin pages
  277. $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], '') != null);
  278. return array_merge($this->templates['basic_defaults'], $item);
  279. }
  280. /**
  281. * Convert a WP submenu structure to an associative array
  282. *
  283. * @param array $item An element of the $submenu array
  284. * @param integer $pos The position (index) of that element
  285. * @param string $parent Parent file that this menu item belongs to.
  286. * @return array
  287. */
  288. function submenu2assoc($item, $pos = 0, $parent = ''){
  289. $item = array(
  290. 'menu_title' => $item[0],
  291. 'access_level' => $item[1],
  292. 'file' => $item[2],
  293. 'page_title' => (isset($item[3])?$item[3]:''),
  294. 'position' => $pos,
  295. );
  296. //Save the default parent menu
  297. $item['parent'] = $parent;
  298. //Flag plugin pages
  299. $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null);
  300. return array_merge($this->templates['basic_defaults'], $item);
  301. }
  302. /**
  303. * Populate lookup arrays with default values from $menu and $submenu. Used later to merge
  304. * a custom menu with the native WordPress menu structure somewhat gracefully.
  305. *
  306. * @param array $menu
  307. * @param array $submenu
  308. * @return array An array with two elements containing menu and submenu defaults.
  309. */
  310. function build_lookups($menu, $submenu){
  311. //Process the top menu
  312. $menu_defaults = array();
  313. foreach($menu as $pos => $item){
  314. $item = $this->menu2assoc($item, $pos);
  315. $menu_defaults[$item['file']] = $item; //index by filename
  316. }
  317. //Process the submenu
  318. $submenu_defaults = array();
  319. foreach($submenu as $parent => $items){
  320. foreach($items as $pos => $item){
  321. $item = $this->submenu2assoc($item, $pos, $parent);
  322. //File itself is not guaranteed to be unique, so we use a surrogate ID to identify submenus.
  323. $uid = $this->unique_submenu_id($item['file'], $parent);
  324. $submenu_defaults[$uid] = $item;
  325. }
  326. }
  327. return array($menu_defaults, $submenu_defaults);
  328. }
  329. /**
  330. * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items
  331. * and marks missing items as such.
  332. *
  333. * @param array $tree A menu in plugin's internal form
  334. * @param array $menu WordPress menu structure
  335. * @param array $submenu WordPress submenu structure
  336. * @return array Updated menu tree
  337. */
  338. function menu_merge($tree, $menu, $submenu){
  339. list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu);
  340. //Iterate over all menus and submenus and look up default values
  341. foreach ($tree as &$topmenu){
  342. $topfile = $this->get_menu_field($topmenu, 'file');
  343. //Is this menu present in the default WP menu?
  344. if (isset($menu_defaults[$topfile])){
  345. //Yes, load defaults from that item
  346. $topmenu['defaults'] = $menu_defaults[$topfile];
  347. //Note that the original item was used
  348. $menu_defaults[$topfile]['used'] = true;
  349. } else {
  350. //Record the menu as missing, unless it's a menu separator
  351. if ( empty($topmenu['separator']) ){
  352. $topmenu['missing'] = true;
  353. //[Nasty] Fill the 'defaults' array for menu's that don't have it.
  354. //This should never be required - saving a custom menu should set the defaults
  355. //for all menus it contains automatically.
  356. if ( empty($topmenu['defaults']) ){
  357. $tmp = $topmenu;
  358. $topmenu['defaults'] = $tmp;
  359. }
  360. }
  361. }
  362. if (is_array($topmenu['items'])) {
  363. //Iterate over submenu items
  364. foreach ($topmenu['items'] as $file => &$item){
  365. $uid = $this->unique_submenu_id($item, $topfile);
  366. //Is this item present in the default WP menu?
  367. if (isset($submenu_defaults[$uid])){
  368. //Yes, load defaults from that item
  369. $item['defaults'] = $submenu_defaults[$uid];
  370. $submenu_defaults[$uid]['used'] = true;
  371. } else {
  372. //Record as missing
  373. $item['missing'] = true;
  374. if ( empty($item['defaults']) ){
  375. $tmp = $item;
  376. $item['defaults'] = $tmp;
  377. }
  378. }
  379. }
  380. }
  381. }
  382. //If we don't unset these they will fuck up the next two loops where the same names are used.
  383. unset($topmenu);
  384. unset($item);
  385. //Note : Now we have some items marked as missing, and some items in lookup arrays
  386. //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()),
  387. //but lets merge in the unused items now.
  388. //Find and merge unused toplevel menus
  389. foreach ($menu_defaults as $topfile => $topmenu){
  390. //Skip used menus and separators
  391. if ( !empty($topmenu['used']) || !empty($topmenu['separator'])) {
  392. continue;
  393. };
  394. //Found an unused item. Build the tree entry.
  395. $entry = $this->templates['blank_menu'];
  396. $entry['defaults'] = $topmenu;
  397. $entry['items'] = array(); //prepare a place for menu items, if any.
  398. //Note that this item is unused
  399. $entry['unused'] = true;
  400. //Add the new entry to the menu tree
  401. $tree[$topfile] = $entry;
  402. }
  403. unset($topmenu);
  404. //Find and merge submenu items
  405. foreach($submenu_defaults as $uid => $item){
  406. if ( !empty($item['used']) ) continue;
  407. //Found an unused item. Build an entry and attach it under the default toplevel menu.
  408. $entry = $this->templates['blank_item'];
  409. $entry['defaults'] = $item;
  410. //Note that this item is unused
  411. $entry['unused'] = true;
  412. //Check if the toplevel menu exists
  413. if (isset($tree[$item['parent']])) {
  414. //Okay, insert the item.
  415. $tree[$item['parent']]['items'][$item['file']] = $entry;
  416. } else {
  417. //Ooops? This should never happen. Some kind of inconsistency?
  418. }
  419. }
  420. //Resort the tree to ensure the found items are in the right spots
  421. $tree = $this->sort_menu_tree($tree);
  422. return $tree;
  423. }
  424. /**
  425. * Generate an ID that uniquely identifies a given submenu item.
  426. *
  427. * @param string|array $file Menu item in question
  428. * @param string $parent Parent menu. Optional. If $file is an array, the function will try to get the parent value from $file['defaults'] instead.
  429. * @return string Unique ID
  430. */
  431. function unique_submenu_id($file, $parent = ''){
  432. if ( is_array($file) ){
  433. if ( isset($file['defaults']) && isset($file['defaults']['parent']) ){
  434. $parent = $file['defaults']['parent'];
  435. }
  436. $file = $this->get_menu_field($file, 'file');
  437. }
  438. if ( !empty($parent) ){
  439. return $parent . '::' . $file;
  440. } else {
  441. return $file;
  442. }
  443. }
  444. /**
  445. * Convert the WP menu structure to the internal representation. All properties set as defaults.
  446. *
  447. * @param array $menu
  448. * @param array $submenu
  449. * @return array Menu in the internal tree format.
  450. */
  451. function wp2tree($menu, $submenu){
  452. $tree = array();
  453. $separator_count = 0;
  454. foreach ($menu as $pos => $item){
  455. $tree_item = $this->templates['blank_menu'];
  456. $tree_item['defaults'] = $this->menu2assoc($item, $pos);
  457. $tree_item['separator'] = empty($item[2]) || empty($item[0]) || (strpos($item[4], 'wp-menu-separator') !== false);
  458. if ( empty($tree_item['defaults']['file']) ){
  459. $tree_item['defaults']['file'] = 'separator_'.$separator_count;
  460. $separator_count++;
  461. }
  462. //Attach submenu items
  463. $parent = $tree_item['defaults']['file'];
  464. if ( isset($submenu[$parent]) ){
  465. foreach($submenu[$parent] as $pos => $subitem){
  466. $tree_item['items'][$subitem[2]] = array_merge(
  467. $this->templates['blank_item'],
  468. array('defaults' => $this->submenu2assoc($subitem, $pos, $parent))
  469. );
  470. }
  471. }
  472. $tree[$parent] = $tree_item;
  473. }
  474. $tree = $this->sort_menu_tree($tree);
  475. return $tree;
  476. }
  477. /**
  478. * Set all undefined menu fields to the default value
  479. *
  480. * @param array $item Menu item in the plugin's internal form
  481. * @return array
  482. */
  483. function apply_defaults($item){
  484. foreach($item as $key => $value){
  485. //Is the field set?
  486. if ($value === null){
  487. //Use default, if available
  488. if (isset($item['defaults']) && isset($item['defaults'][$key])){
  489. $item[$key] = $item['defaults'][$key];
  490. }
  491. }
  492. }
  493. return $item;
  494. }
  495. /**
  496. * Apply custom menu filters to an item of the custom menu.
  497. *
  498. * Calls two types of filters :
  499. * 'custom_admin_$item_type' with the entire $item passed as the argument.
  500. * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument.
  501. *
  502. * Used when converting the current custom menu to a WP-format menu.
  503. *
  504. * @param array $item Associative array representing one menu item (either top-level or submenu).
  505. * @param string $item_type 'menu' or 'submenu'
  506. * @param mixed $extra Optional extra data to pass to hooks.
  507. * @return array Filtered menu item.
  508. */
  509. function apply_menu_filters($item, $item_type = '', $extra = null){
  510. if ( empty($item_type) ){
  511. //Only top-level menus have an icon
  512. $item_type = isset($item['icon_url'])?'menu':'submenu';
  513. }
  514. $item = apply_filters("custom_admin_{$item_type}", $item, $extra);
  515. foreach($item as $field => $value){
  516. $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra);
  517. }
  518. return $item;
  519. }
  520. /**
  521. * Get the value of a menu/submenu field.
  522. * Will return the corresponding value from the 'defaults' entry of $item if the
  523. * specified field is not set in the item itself.
  524. *
  525. * @param array $item
  526. * @param string $field_name
  527. * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null.
  528. * @return mixed Field value.
  529. */
  530. function get_menu_field($item, $field_name, $default = null){
  531. if ( isset($item[$field_name]) && ($item[$field_name] !== null) ){
  532. return $item[$field_name];
  533. } else {
  534. if ( isset($item['defaults']) && isset($item['defaults'][$field_name]) ){
  535. return $item['defaults'][$field_name];
  536. } else {
  537. return $default;
  538. }
  539. }
  540. }
  541. /**
  542. * Custom comparison function that compares menu items based on their position in the menu.
  543. *
  544. * @param array $a
  545. * @param array $b
  546. * @return int
  547. */
  548. function compare_position($a, $b){
  549. if ($a['position']!==null) {
  550. $p1 = $a['position'];
  551. } else {
  552. if ( isset($a['defaults']['position']) ){
  553. $p1 = $a['defaults']['position'];
  554. } else {
  555. $p1 = 0;
  556. }
  557. }
  558. if ($b['position']!==null) {
  559. $p2 = $b['position'];
  560. } else {
  561. if ( isset($b['defaults']['position']) ){
  562. $p2 = $b['defaults']['position'];
  563. } else {
  564. $p2 = 0;
  565. }
  566. }
  567. return $p1 - $p2;
  568. }
  569. /**
  570. * Sort the menus and menu items of a given menu according to their positions
  571. *
  572. * @param array $tree A menu structure in the internal format
  573. * @return array Sorted menu in the internal format
  574. */
  575. function sort_menu_tree($tree){
  576. //Resort the tree to ensure the found items are in the right spots
  577. uasort($tree, array(&$this, 'compare_position'));
  578. //Resort all submenus as well
  579. foreach ($tree as &$topmenu){
  580. if (!empty($topmenu['items'])){
  581. uasort($topmenu['items'], array(&$this, 'compare_position'));
  582. }
  583. }
  584. return $tree;
  585. }
  586. /**
  587. * Convert internal menu representation to the form used by WP.
  588. *
  589. * Note : While this function doesn't cause any side effects of its own,
  590. * it executes several filters that may modify global state. Specifically,
  591. * IFrame-handling callbacks in 'extras.php' may insert items into the
  592. * global $menu and $submenu arrays.
  593. *
  594. * @param array $tree
  595. * @return array $menu and $submenu
  596. */
  597. function tree2wp($tree){
  598. $menu = array();
  599. $submenu = array();
  600. $title_lookup = array();
  601. //Sort the menu by position
  602. uasort($tree, array(&$this, 'compare_position'));
  603. //Prepare the top menu
  604. $first_nonseparator_found = false;
  605. foreach ($tree as $topmenu){
  606. //Skip missing menus, unless they're user-created and thus might point to a non-standard file
  607. $custom = $this->get_menu_field($topmenu, 'custom', false);
  608. if ( !empty($topmenu['missing']) && !$custom ) {
  609. continue;
  610. };
  611. //Skip leading menu separators. Fixes a superfluous separator showing up
  612. //in WP 3.0 (multisite mode) when there's a custom menu and the current user
  613. //can't access its first item ("Super Admin").
  614. if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) continue;
  615. $first_nonseparator_found = true;
  616. //Apply defaults & filters
  617. $topmenu = $this->apply_defaults($topmenu);
  618. $topmenu = $this->apply_menu_filters($topmenu, 'menu');
  619. //Skip hidden entries
  620. if (!empty($topmenu['hidden'])) continue;
  621. //Build the menu structure that WP expects
  622. $menu[] = array(
  623. $topmenu['menu_title'],
  624. $topmenu['access_level'],
  625. $topmenu['file'],
  626. $topmenu['page_title'],
  627. $topmenu['css_class'],
  628. $topmenu['hookname'], //ID
  629. $topmenu['icon_url']
  630. );
  631. //Prepare the submenu of this menu
  632. if( !empty($topmenu['items']) ){
  633. $items = $topmenu['items'];
  634. //Sort by position
  635. uasort($items, array(&$this, 'compare_position'));
  636. foreach ($items as $item) {
  637. //Skip missing items, unless they're user-created
  638. $custom = $this->get_menu_field($item, 'custom', false);
  639. if ( !empty($item['missing']) && !$custom ) continue;
  640. //Special case : plugin pages that have been moved to a different menu.
  641. //If the file field hasn't already been modified, we'll need to adjust it
  642. //to point to the old parent. This is required because WP identifies
  643. //plugin pages using *both* the plugin file and the parent file.
  644. if ( $this->get_menu_field($item, 'is_plugin_page', false) && ($item['file'] === null) ){
  645. $default_parent = '';
  646. if ( isset($item['defaults']) && isset($item['defaults']['parent'])){
  647. $default_parent = $item['defaults']['parent'];
  648. }
  649. if ( $topmenu['file'] != $default_parent ){
  650. $item['file'] = $default_parent . '?page=' . $item['defaults']['file'];
  651. }
  652. }
  653. $item = $this->apply_defaults($item);
  654. $item = $this->apply_menu_filters($item, 'submenu', $topmenu['file']);
  655. //Skip hidden items
  656. if (!empty($item['hidden'])) {
  657. continue;
  658. }
  659. $submenu[$topmenu['file']][] = array(
  660. $item['menu_title'],
  661. $item['access_level'],
  662. $item['file'],
  663. $item['page_title'],
  664. );
  665. //Make a note of the page's correct title so we can fix it later
  666. //if necessary.
  667. $title_lookup[$item['file']] = $item['menu_title'];
  668. }
  669. }
  670. }
  671. return array($menu, $submenu, $title_lookup);
  672. }
  673. /**
  674. * Prevent the "Menu Editor" menu item from becoming inaccessible.
  675. *
  676. * @param array $item
  677. * @return array
  678. */
  679. function hook_custom_admin_submenu($item){
  680. //To stop the user from accidentally shooting themselves in the foot, we make
  681. //any dangerous changes made to the "Menu Editor" menu have no effect.
  682. if ( !empty($item['file']) && ($item['file'] == 'menu_editor') ){
  683. //Reset the important fields back to the default values.
  684. $item['access_level'] = null;
  685. $item['page_title'] = null;
  686. $item['window_title'] = null;
  687. $item = $this->apply_defaults($item);
  688. $item['hidden'] = false; //Can't hide me!
  689. }
  690. return $item;
  691. }
  692. /**
  693. * Prevent the menu that contains the "Menu Editor" entry from being hidden.
  694. *
  695. * @param array $menu
  696. * @return array
  697. */
  698. function hook_custom_admin_menu($menu){
  699. if ( !empty($menu['items']) && array_key_exists('menu_editor', $menu['items']) ){
  700. $menu['hidden'] = false;
  701. }
  702. return $menu;
  703. }
  704. /**
  705. * Upgrade a menu tree to the currently used structure
  706. * Does nothing if the menu is already up to date.
  707. *
  708. * @param array $tree
  709. * @return array
  710. */
  711. function upgrade_menu_structure($tree){
  712. //Append new fields, if any
  713. foreach($tree as &$menu){
  714. $menu = array_merge($this->templates['blank_menu'], $menu);
  715. $menu['defaults'] = array_merge($this->templates['basic_defaults'], $menu['defaults']);
  716. foreach($menu['items'] as $item_file => $item){
  717. $item = array_merge($this->templates['blank_item'], $item);
  718. $item['defaults'] = array_merge($this->templates['basic_defaults'], $item['defaults']);
  719. $menu['items'][$item_file] = $item;
  720. }
  721. }
  722. return $tree;
  723. }
  724. /**
  725. * Output the menu editor page
  726. *
  727. * @return void
  728. */
  729. function page_menu_editor(){
  730. global $menu, $submenu;
  731. global $wp_roles;
  732. if ( !$this->current_user_can_edit_menu() ){
  733. die("Access denied");
  734. }
  735. $action = isset($_POST['action'])?$_POST['action']:(isset($_GET['action'])?$_GET['action']:'');
  736. do_action('admin_menu_editor_header', $action);
  737. //Handle form submissions
  738. if (isset($_POST['data'])){
  739. check_admin_referer('menu-editor-form');
  740. //Try to decode a menu tree encoded as JSON
  741. $data = $this->json_decode($_POST['data'], true);
  742. if (!$data || count(($data) < 2) ){
  743. $fixed = stripslashes($_POST['data']);
  744. $data = $this->json_decode( $fixed, true );
  745. }
  746. $url = remove_query_arg('noheader');
  747. if ($data){
  748. //Save the custom menu
  749. $this->options['custom_menu'] = $data;
  750. $this->save_options();
  751. //Redirect back to the editor and display the success message
  752. wp_redirect( add_query_arg('message', 1, $url) );
  753. } else {
  754. //Or redirect & display the error message
  755. wp_redirect( add_query_arg('message', 2, $url) );
  756. }
  757. die();
  758. }
  759. //Attach a "Feedback" link to the screen meta panel.
  760. $this->print_uservoice_widget();
  761. //Kindly remind the user to give me money
  762. if ( !apply_filters('admin_menu_editor_is_pro', false) ){
  763. $this->print_upgrade_notice();
  764. }
  765. ?>
  766. <div class="wrap">
  767. <h2>
  768. <?php echo apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'); ?>
  769. </h2>
  770. <?php
  771. if ( !empty($_GET['message']) ){
  772. if ( intval($_GET['message']) == 1 ){
  773. echo '<div id="message" class="updated fade"><p><strong>Settings saved.</strong></p></div>';
  774. } elseif ( intval($_GET['message']) == 2 ) {
  775. echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>';
  776. }
  777. }
  778. //Build a tree struct. for the default menu
  779. $default_menu = $this->wp2tree($this->default_wp_menu, $this->default_wp_submenu);
  780. //Is there a custom menu?
  781. if (!empty($this->custom_menu)){
  782. $custom_menu = $this->custom_menu;
  783. } else {
  784. //Start out with the default menu if there is no user-created one
  785. $custom_menu = $default_menu;
  786. }
  787. //Encode both menus as JSON
  788. $default_menu_js = $this->getMenuAsJS($default_menu);
  789. $custom_menu_js = $this->getMenuAsJS($custom_menu);
  790. $plugin_url = $this->plugin_dir_url;
  791. $images_url = $this->plugin_dir_url . '/images';
  792. //Create a list of all known capabilities and roles. Used for the dropdown list on the access field.
  793. $all_capabilities = $this->get_all_capabilities();
  794. $all_capabilities = array_keys($all_capabilities);
  795. natcasesort($all_capabilities);
  796. $all_roles = $this->get_all_roles();
  797. //Multi-site installs also get the virtual "Super Admin" role
  798. if ( is_multisite() ){
  799. $all_roles['super_admin'] = 'Super Admin';
  800. }
  801. asort($all_roles);
  802. ?>
  803. <div id='ws_menu_editor'>
  804. <div class='ws_main_container'>
  805. <div class='ws_toolbar'>
  806. <div class="ws_button_container">
  807. <a id='ws_cut_menu' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
  808. <a id='ws_copy_menu' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
  809. <a id='ws_paste_menu' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
  810. <div class="ws_separator">&nbsp;</div>
  811. <a id='ws_new_menu' class='ws_button' href='javascript:void(0)' title='New menu'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
  812. <a id='ws_hide_menu' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
  813. <a id='ws_delete_menu' class='ws_button' href='javascript:void(0)' title='Delete menu'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
  814. <div class="ws_separator">&nbsp;</div>
  815. <a id='ws_new_separator' class='ws_button' href='javascript:void(0)' title='New separator'><img src='<?php echo $images_url; ?>/separator_add.png' /></a>
  816. </div>
  817. </div>
  818. <div id='ws_menu_box' class="ws_box">
  819. </div>
  820. </div>
  821. <div class='ws_main_container'>
  822. <div class='ws_toolbar'>
  823. <div class="ws_button_container">
  824. <a id='ws_cut_item' class='ws_button' href='javascript:void(0)' title='Cut'><img src='<?php echo $images_url; ?>/cut.png' /></a>
  825. <a id='ws_copy_item' class='ws_button' href='javascript:void(0)' title='Copy'><img src='<?php echo $images_url; ?>/page_white_copy.png' /></a>
  826. <a id='ws_paste_item' class='ws_button' href='javascript:void(0)' title='Paste'><img src='<?php echo $images_url; ?>/page_white_paste.png' /></a>
  827. <div class="ws_separator">&nbsp;</div>
  828. <a id='ws_new_item' class='ws_button' href='javascript:void(0)' title='New menu item'><img src='<?php echo $images_url; ?>/page_white_add.png' /></a>
  829. <a id='ws_hide_item' class='ws_button' href='javascript:void(0)' title='Show/Hide'><img src='<?php echo $images_url; ?>/plugin_disabled.png' /></a>
  830. <a id='ws_delete_item' class='ws_button' href='javascript:void(0)' title='Delete menu item'><img src='<?php echo $images_url; ?>/page_white_delete.png' /></a>
  831. <div class="ws_separator">&nbsp;</div>
  832. <a id='ws_sort_ascending' class='ws_button' href='javascript:void(0)' title='Sort ascending'>
  833. <img src='<?php echo $images_url; ?>/sort_ascending.png' />
  834. </a>
  835. <a id='ws_sort_descending' class='ws_button' href='javascript:void(0)' title='Sort descending'>
  836. <img src='<?php echo $images_url; ?>/sort_descending.png' />
  837. </a>
  838. </div>
  839. </div>
  840. <div id='ws_submenu_box' class="ws_box">
  841. </div>
  842. </div>
  843. </div>
  844. <div class="ws_main_container" id="ws_editor_sidebar">
  845. <form method="post" action="<?php echo admin_url('options-general.php?page=menu_editor&noheader=1'); ?>" id='ws_main_form' name='ws_main_form'>
  846. <?php wp_nonce_field('menu-editor-form'); ?>
  847. <input type="hidden" name="data" id="ws_data" value="">
  848. <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" />
  849. </form>
  850. <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" />
  851. <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" />
  852. <?php
  853. do_action('admin_menu_editor_sidebar');
  854. ?>
  855. </div>
  856. </div>
  857. <?php
  858. //Createa a pop-up capability selector
  859. $capSelector = array('<select id="ws_cap_selector" size="10">');
  860. $capSelector[] = '<optgroup label="Roles">';
  861. foreach($all_roles as $role_id => $role_name){
  862. $capSelector[] = sprintf(
  863. '<option value="%s">%s</option>',
  864. esc_attr($role_id),
  865. $role_name
  866. );
  867. }
  868. $capSelector[] = '</optgroup>';
  869. $capSelector[] = '<optgroup label="Capabilities">';
  870. foreach($all_capabilities as $cap){
  871. $capSelector[] = sprintf(
  872. '<option value="%s">%s</option>',
  873. esc_attr($cap),
  874. $cap
  875. );
  876. }
  877. $capSelector[] = '</optgroup>';
  878. $capSelector[] = '</select>';
  879. echo implode("\n", $capSelector);
  880. ?>
  881. <span id="ws-ame-screen-meta-contents" style="display:none;">
  882. <label for="ws-hide-advanced-settings">
  883. <input type="checkbox" id="ws-hide-advanced-settings"<?php
  884. if ( $this->options['hide_advanced_settings'] ){
  885. echo ' checked="checked"';
  886. }
  887. ?> /> Hide advanced options
  888. </label>
  889. </span>
  890. <script type='text/javascript'>
  891. var defaultMenu = <?php echo $default_menu_js; ?>;
  892. var customMenu = <?php echo $custom_menu_js; ?>;
  893. var imagesUrl = "<?php echo esc_js($images_url); ?>";
  894. var adminAjaxUrl = "<?php echo esc_js(admin_url('admin-ajax.php')); ?>";
  895. var hideAdvancedSettings = <?php echo $this->options['hide_advanced_settings']?'true':'false'; ?>;
  896. var hideAdvancedSettingsNonce = '<?php echo esc_js(wp_create_nonce('ws_ame_save_screen_options')); ?>';
  897. var captionShowAdvanced = 'Show advanced options';
  898. var captionHideAdvanced = 'Hide advanced options';
  899. window.wsMenuEditorPro = false; //Will be overwritten if extras are loaded
  900. </script>
  901. <?php
  902. //Let the Pro version script output it's extra HTML & scripts.
  903. do_action('admin_menu_editor_footer');
  904. }
  905. /**
  906. * Retrieve a list of all known capabilities of all roles
  907. *
  908. * @return array Associative array with capability names as keys
  909. */
  910. function get_all_capabilities(){
  911. global $wp_roles;
  912. $capabilities = array();
  913. if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
  914. return $capabilities;
  915. }
  916. //Iterate over all known roles and collect their capabilities
  917. foreach($wp_roles->roles as $role_id => $role){
  918. if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here
  919. $capabilities = array_merge($capabilities, $role['capabilities']);
  920. }
  921. }
  922. //Add multisite-specific capabilities (not listed in any roles in WP 3.0)
  923. $multisite_caps = array(
  924. 'manage_sites' => 1,
  925. 'manage_network' => 1,
  926. 'manage_network_users' => 1,
  927. 'manage_network_themes' => 1,
  928. 'manage_network_options' => 1,
  929. 'manage_network_plugins' => 1,
  930. );
  931. $capabilities = array_merge($capabilities, $multisite_caps);
  932. return $capabilities;
  933. }
  934. /**
  935. * Retrieve a list of all known roles
  936. *
  937. * @return array Associative array with role IDs as keys and role display names as values
  938. */
  939. function get_all_roles(){
  940. global $wp_roles;
  941. $roles = array();
  942. if ( !isset($wp_roles) || !isset($wp_roles->roles) ){
  943. return $roles;
  944. }
  945. foreach($wp_roles->roles as $role_id => $role){
  946. $roles[$role_id] = $role['name'];
  947. }
  948. return $roles;
  949. }
  950. /**
  951. * Create a virtual 'super_admin' capability that only super admins have.
  952. * This function accomplishes that by by filtering 'user_has_cap' calls.
  953. *
  954. * @param array $allcaps All capabilities belonging to the current user, cap => true/false.
  955. * @param array $required_caps The required capabilities.
  956. * @param array $args The capability passed to current_user_can, the current user's ID, and other args.
  957. * @return array Filtered version of $allcaps
  958. */
  959. function hook_user_has_cap($allcaps, $required_caps, $args){
  960. if ( in_array('super_admin', $required_caps) ){
  961. $allcaps['super_admin'] = is_multisite() && is_super_admin($args[1]);
  962. }
  963. return $allcaps;
  964. }
  965. /**
  966. * Output the JavaScript that adds the "Feedback" widget to screen meta.
  967. *
  968. * @return void
  969. */
  970. function print_uservoice_widget(){
  971. ?>
  972. <script type="text/javascript">
  973. (function($){
  974. $('#screen-meta-links').append(
  975. '<div id="ws-ame-feedback-widget-wrap" class="hide-if-no-js screen-meta-toggle">' +
  976. '<a href="http://feedback.w-shadow.com/forums/58572-admin-menu-editor" id="ws-ame-feedback-widget" class="show-settings" target="_blank" title="Open the user feedback forum">Feedback</a>' +
  977. '</div>'
  978. );
  979. })(jQuery);
  980. </script>
  981. <?php
  982. }
  983. /**
  984. * Output the "Upgrade to Pro" message
  985. *
  986. * @return void
  987. */
  988. function print_upgrade_notice(){
  989. ?>
  990. <script type="text/javascript">
  991. (function($){
  992. $('#screen-meta-links').append(
  993. '<div id="ws-pro-version-notice" class="hide-if-no-js screen-meta-toggle">' +
  994. '<a href="http://wpplugins.com/plugin/146/admin-menu-editor-pro" id="ws-pro-version-notice-link" class="show-settings" target="_blank" title="View Pro version details">Upgrade to Pro</a>' +
  995. '</div>'
  996. );
  997. })(jQuery);
  998. </script>
  999. <?php
  1000. }
  1001. /**
  1002. * AJAX callback for saving screen options (whether to show or to hide advanced menu options).
  1003. *
  1004. * Handles the 'ws_ame_save_screen_options' action. The new option value
  1005. * is read from $_POST['hide_advanced_settings'].
  1006. *
  1007. * @return void
  1008. */
  1009. function ajax_save_screen_options(){
  1010. if (!current_user_can('manage_options') || !check_ajax_referer('ws_ame_save_screen_options', false, false)){
  1011. die( $this->json_encode( array(
  1012. 'error' => "You're not allowed to do that!"
  1013. )));
  1014. }
  1015. $this->options['hide_advanced_settings'] = !empty($_POST['hide_advanced_settings']);
  1016. $this->save_options();
  1017. die('1');
  1018. }
  1019. /**
  1020. * A callback for the stub meta box added to the plugin's page. Does nothing.
  1021. *
  1022. * @return void
  1023. */
  1024. function noop(){
  1025. //nihil
  1026. }
  1027. } //class
  1028. endif;
  1029. ?>