is_super_plugin() ){ $this->sitewide_options = true; } //Set some plugin-specific options if ( empty($this->option_name) ){ $this->option_name = 'ws_menu_editor'; } $this->defaults = array( 'hide_advanced_settings' => true, 'menu_format_version' => 0, 'display_survey_notice' => true, 'first_install_time' => null, ); $this->serialize_with_json = false; //(Don't) store the options in JSON format $this->settings_link = 'options-general.php?page=menu_editor'; $this->magic_hooks = true; $this->magic_hook_priority = 99999; //Build some template arrays $this->templates['basic_defaults'] = array( 'page_title' => '', 'menu_title' => '', 'access_level' => 'read', 'file' => '', 'css_class' => '', 'hookname' => '', 'icon_url' => '', 'position' => 0, 'separator' => false, 'custom' => false, 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default) ); //Template for a basic top-level menu $this->templates['blank_menu'] = array( 'page_title' => null, 'menu_title' => null, 'access_level' => null, 'file' => null, 'css_class' => null, 'hookname' => null, 'icon_url' => null, 'position' => null, 'separator' => null, 'custom' => null, 'open_in' => null, 'defaults' => $this->templates['basic_defaults'], 'items' => array(), ); //Template for menu items $this->templates['blank_item'] = array( 'menu_title' => null, 'access_level' => null, 'file' => null, 'page_title' => null, 'position' => null, 'custom' => null, 'open_in' => null, 'defaults' => $this->templates['basic_defaults'], ); //AJAXify screen options add_action( 'wp_ajax_ws_ame_save_screen_options', array(&$this,'ajax_save_screen_options') ); //AJAXify hints add_action('wp_ajax_ws_ame_hide_hint', array($this, 'ajax_hide_hint')); //Make sure we have access to the original, un-mangled request data. //This is necessary because WordPress will stupidly apply "magic quotes" //to the request vars even if this PHP misfeature is disabled. add_action('plugins_loaded', array($this, 'capture_request_vars')); add_action('admin_enqueue_scripts', array($this, 'enqueue_menu_fix_script')); //User survey add_action('admin_notices', array($this, 'display_survey_notice')); } function init_finish() { parent::init_finish(); if ( !isset($this->options['first_install_time']) ) { $this->options['first_install_time'] = time(); $this->save_options(); } } /** * Activation hook * * @return void */ function activate(){ //If we have no stored settings for this version of the plugin, try importing them //from other versions (i.e. the free or the Pro version). if ( !$this->load_options() ){ $this->import_settings(); } parent::activate(); } /** * Import settings from a different version of the plugin. * * @return bool True if settings were imported successfully, False otherwise */ function import_settings(){ $possible_names = array('ws_menu_editor', 'ws_menu_editor_pro'); foreach($possible_names as $option_name){ if ( $this->load_options($option_name) ){ return true; } } return false; } /** * Add the JS required by the editor to the page header * * @return void */ function enqueue_scripts(){ //jQuery JSON plugin wp_enqueue_script('jquery-json', plugins_url('js/jquery.json-1.3.js', $this->plugin_file), array('jquery'), '1.3'); //jQuery sort plugin wp_enqueue_script('jquery-sort', plugins_url('js/jquery.sort.js', $this->plugin_file), array('jquery')); //jQuery UI Droppable wp_enqueue_script('jquery-ui-droppable'); //We use WordPress media uploader to let the user upload custom menu icons (WP 3.5+). if ( function_exists('wp_enqueue_media') ) { wp_enqueue_media(); } //Editor's scipts wp_enqueue_script( 'menu-editor', plugins_url('js/menu-editor.js', $this->plugin_file), array('jquery', 'jquery-ui-sortable', 'jquery-ui-dialog', 'jquery-form'), '20130221' ); //The editor will need access to some of the plugin data and WP data. wp_localize_script( 'menu-editor', 'wsEditorData', array( 'adminAjaxUrl' => admin_url('admin-ajax.php'), 'showHints' => $this->get_hint_visibility(), ) ); } /** * Compatibility workaround for Participants Database 1.4.5.2. * * Participants Database loads its settings JavaScript on every page in the "Settings" menu, * not just its own. It doesn't bother to also load the script's dependencies, though, so * the script crashes *and* it breaks the menu editor by way of collateral damage. * * Fix by forcibly removing the offending script from the queue. */ public function dequeue_pd_scripts() { if ( is_plugin_active('participants-database/participants-database.php') ) { wp_dequeue_script('settings_script'); } } /** * Add the editor's CSS file to the page header * * @return void */ function enqueue_styles(){ wp_enqueue_style('menu-editor-style', plugins_url('css/menu-editor.css', $this->plugin_file), array(), '20130805'); } /** * Create a configuration page and load the custom menu * * @return void */ function hook_admin_menu(){ global $menu, $submenu; //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1 $reset_requested = isset($this->get['reset_admin_menu']) && $this->get['reset_admin_menu']; if ( $reset_requested && $this->current_user_can_edit_menu() ){ $this->options['custom_menu'] = null; $this->save_options(); } //The menu editor is only visible to users with the manage_options privilege. //Or, if the plugin is installed in mu-plugins, only to the site administrator(s). if ( $this->current_user_can_edit_menu() ){ $page = add_options_page( apply_filters('admin_menu_editor-self_page_title', 'Menu Editor'), apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'), 'manage_options', 'menu_editor', array(&$this, 'page_menu_editor') ); //Output our JS & CSS on that page only add_action("admin_print_scripts-$page", array(&$this, 'enqueue_scripts')); add_action("admin_print_styles-$page", array(&$this, 'enqueue_styles')); //Compatibility fix for Participants Database. add_action("admin_print_scripts-$page", array($this, 'dequeue_pd_scripts')); //Make a placeholder for our screen options (hacky) add_meta_box("ws-ame-screen-options", "You should never see this", array(&$this, 'noop'), $page); } //WP 3.0 in multisite mode has two separators with the same filename. This plugin //expects all top-level menus to have unique filenames/URLs. $first_separator1 = -1; $last_separator1 = -1; foreach($menu as $index => $item){ if ( $item[2] == 'separator1' ){ $last_separator1 = $index; if ( $first_separator1 == -1 ){ $first_separator1 = $index; } } } if ( $first_separator1 != $last_separator1 ){ $menu[$first_separator1][2] = 'separator0'; } //Store the "original" menus for later use in the editor $this->default_wp_menu = $menu; $this->default_wp_submenu = $submenu; //Is there a custom menu to use? if ( !empty($this->options['custom_menu']) ){ //Check if we need to upgrade the menu structure if ( empty($this->options['menu_format_version']) || ($this->options['menu_format_version'] < $this->menu_format_version) ){ $this->options['custom_menu'] = $this->upgrade_menu_structure($this->options['custom_menu']); $this->options['menu_format_version'] = $this->menu_format_version; $this->save_options(); } //Merge in data from the default menu $tree = $this->menu_merge($this->options['custom_menu'], $menu, $submenu); //Save for later - the editor page will need it $this->custom_menu = $tree; //Apply the custom menu list($menu, $submenu, $this->title_lookups) = $this->tree2wp($tree); //Re-filter the menu (silly WP should do that itself, oh well) $this->filter_menu(); $this->filtered_wp_menu = $menu; $this->filtered_wp_submenu = $submenu; } } /** * Activate the 'menu_order' filter. * * @return bool */ function hook_custom_menu_order(){ return true; } /** * Override the order of the top-level menu entries. * * @param array $menu_order * @return array */ function hook_menu_order($menu_order){ if (empty($this->custom_menu)){ return $menu_order; } $custom_menu_order = array(); foreach($this->filtered_wp_menu as $topmenu){ $filename = $topmenu[2]; if ( in_array($filename, $menu_order) ){ $custom_menu_order[] = $filename; } } return $custom_menu_order; } /** * Determine if the current user may use the menu editor. * * @return bool */ function current_user_can_edit_menu(){ if ( $this->is_super_plugin() ){ return is_super_admin(); } else { return current_user_can('manage_options'); } } /** * Intercept a handy action to fix the page title for moved plugin pages. * * @return void */ function hook_admin_xml_ns(){ global $title; if ( empty($title) ){ $title = $this->get_real_page_title(); } } /** * Fix the page title for move plugin pages. * The 'admin_title' filter is only available in WP 3.1+ * * @param string $admin_title The current admin title. * @param string $title The current page title. * @return string New admin title. */ function hook_admin_title($admin_title, $title){ if ( empty($title) ){ $admin_title = $this->get_real_page_title() . $admin_title; } return $admin_title; } /** * Get the correct page title for a plugin page that's been moved to a different menu. * * @return string */ function get_real_page_title(){ global $title; global $pagenow; global $plugin_page; if ( empty($title) && !empty($plugin_page) && !empty($pagenow) ){ $file = sprintf('%s?page=%s', $pagenow, $plugin_page); if ( isset($this->title_lookups[$file]) ){ $title = esc_html( strip_tags( $this->title_lookups[$file] ) ); } } return $title; } /** * Loop over the Dashboard submenus and remove pages for which the current user does not have privs. * * @return void */ function filter_menu(){ global $menu, $submenu, $_wp_submenu_nopriv, $_wp_menu_nopriv; foreach ( array( 'submenu' ) as $sub_loop ) { foreach ($$sub_loop as $parent => $sub) { foreach ($sub as $index => $data) { if ( ! current_user_can($data[1]) ) { unset(${$sub_loop}[$parent][$index]); $_wp_submenu_nopriv[$parent][$data[2]] = true; } } if ( empty(${$sub_loop}[$parent]) ) unset(${$sub_loop}[$parent]); } } } /** * Encode a menu tree as JSON * * @param array $tree * @return string */ function getMenuAsJS($tree){ return $this->json_encode($tree); } /** * Convert a WP menu structure to an associative array * * @param array $item An element of the $menu array * @param integer $pos The position (index) of the menu item * @return array */ function menu2assoc($item, $pos=0){ $item = array( 'menu_title' => $item[0], 'access_level' => $item[1], 'file' => $item[2], 'page_title' => $item[3], 'css_class' => $item[4], 'hookname' => (isset($item[5])?$item[5]:''), //ID 'icon_url' => (isset($item[6])?$item[6]:''), 'position' => $pos, ); $item['separator'] = strpos($item['css_class'], 'wp-menu-separator') !== false; //Flag plugin pages $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], '') != null); return array_merge($this->templates['basic_defaults'], $item); } /** * Convert a WP submenu structure to an associative array * * @param array $item An element of the $submenu array * @param integer $pos The position (index) of that element * @param string $parent Parent file that this menu item belongs to. * @return array */ function submenu2assoc($item, $pos = 0, $parent = ''){ $item = array( 'menu_title' => $item[0], 'access_level' => $item[1], 'file' => $item[2], 'page_title' => (isset($item[3])?$item[3]:''), 'position' => $pos, ); //Save the default parent menu $item['parent'] = $parent; //Flag plugin pages $item['is_plugin_page'] = (get_plugin_page_hook($item['file'], $parent) != null); return array_merge($this->templates['basic_defaults'], $item); } /** * Populate lookup arrays with default values from $menu and $submenu. Used later to merge * a custom menu with the native WordPress menu structure somewhat gracefully. * * @param array $menu * @param array $submenu * @return array An array with two elements containing menu and submenu defaults. */ function build_lookups($menu, $submenu){ //Process the top menu $menu_defaults = array(); foreach($menu as $pos => $item){ $item = $this->menu2assoc($item, $pos); $menu_defaults[$item['file']] = $item; //index by filename } //Process the submenu $submenu_defaults = array(); foreach($submenu as $parent => $items){ foreach($items as $pos => $item){ $item = $this->submenu2assoc($item, $pos, $parent); //File itself is not guaranteed to be unique, so we use a surrogate ID to identify submenus. $uid = $this->unique_submenu_id($item['file'], $parent); $submenu_defaults[$uid] = $item; } } return array($menu_defaults, $submenu_defaults); } /** * Merge $menu and $submenu into the $tree. Adds/replaces defaults, inserts new items * and marks missing items as such. * * @param array $tree A menu in plugin's internal form * @param array $menu WordPress menu structure * @param array $submenu WordPress submenu structure * @return array Updated menu tree */ function menu_merge($tree, $menu, $submenu){ list($menu_defaults, $submenu_defaults) = $this->build_lookups($menu, $submenu); //Iterate over all menus and submenus and look up default values foreach ($tree as &$topmenu){ $topfile = $this->get_menu_field($topmenu, 'file'); //Is this menu present in the default WP menu? if (isset($menu_defaults[$topfile])){ //Yes, load defaults from that item $topmenu['defaults'] = $menu_defaults[$topfile]; //Note that the original item was used $menu_defaults[$topfile]['used'] = true; } else { //Record the menu as missing, unless it's a menu separator if ( empty($topmenu['separator']) ){ $topmenu['missing'] = true; //[Nasty] Fill the 'defaults' array for menu's that don't have it. //This should never be required - saving a custom menu should set the defaults //for all menus it contains automatically. if ( empty($topmenu['defaults']) ){ $tmp = $topmenu; $topmenu['defaults'] = $tmp; } } } if (isset($topmenu['items']) && is_array($topmenu['items'])) { //Iterate over submenu items foreach ($topmenu['items'] as $file => &$item){ $uid = $this->unique_submenu_id($item, $topfile); //Is this item present in the default WP menu? if (isset($submenu_defaults[$uid])){ //Yes, load defaults from that item $item['defaults'] = $submenu_defaults[$uid]; $submenu_defaults[$uid]['used'] = true; } else { //Record as missing $item['missing'] = true; if ( empty($item['defaults']) ){ $tmp = $item; $item['defaults'] = $tmp; } } } } } //If we don't unset these they will fuck up the next two loops where the same names are used. unset($topmenu); unset($item); //Note : Now we have some items marked as missing, and some items in lookup arrays //that are not marked as used. The missing items are handled elsewhere (e.g. tree2wp()), //but lets merge in the unused items now. //Find and merge unused toplevel menus foreach ($menu_defaults as $topfile => $topmenu){ //Skip used menus and separators if ( !empty($topmenu['used']) || !empty($topmenu['separator'])) { continue; }; //Found an unused item. Build the tree entry. $entry = $this->templates['blank_menu']; $entry['defaults'] = $topmenu; $entry['items'] = array(); //prepare a place for menu items, if any. //Note that this item is unused $entry['unused'] = true; //Add the new entry to the menu tree $tree[$topfile] = $entry; } unset($topmenu); //Find and merge submenu items foreach($submenu_defaults as $uid => $item){ if ( !empty($item['used']) ) continue; //Found an unused item. Build an entry and attach it under the default toplevel menu. $entry = $this->templates['blank_item']; $entry['defaults'] = $item; //Note that this item is unused $entry['unused'] = true; //Check if the toplevel menu exists if (isset($tree[$item['parent']])) { //Okay, insert the item. $tree[$item['parent']]['items'][$item['file']] = $entry; } else { //Ooops? This should never happen. Some kind of inconsistency? } } //Resort the tree to ensure the found items are in the right spots $tree = $this->sort_menu_tree($tree); return $tree; } /** * Generate an ID that uniquely identifies a given submenu item. * * @param string|array $file Menu item in question * @param string $parent Parent menu. Optional. If $file is an array, the function will try to get the parent value from $file['defaults'] instead. * @return string Unique ID */ function unique_submenu_id($file, $parent = ''){ if ( is_array($file) ){ if ( isset($file['defaults']) && isset($file['defaults']['parent']) ){ $parent = $file['defaults']['parent']; } $file = $this->get_menu_field($file, 'file'); } if ( !empty($parent) ){ return $parent . '::' . $file; } else { return $file; } } /** * Convert the WP menu structure to the internal representation. All properties set as defaults. * * @param array $menu * @param array $submenu * @return array Menu in the internal tree format. */ function wp2tree($menu, $submenu){ $tree = array(); $separator_count = 0; foreach ($menu as $pos => $item){ $tree_item = $this->templates['blank_menu']; $tree_item['defaults'] = $this->menu2assoc($item, $pos); $tree_item['separator'] = empty($item[2]) || empty($item[0]) || (strpos($item[4], 'wp-menu-separator') !== false); if ( empty($tree_item['defaults']['file']) ){ $tree_item['defaults']['file'] = 'separator_'.$separator_count; $separator_count++; } //Attach submenu items $parent = $tree_item['defaults']['file']; if ( isset($submenu[$parent]) ){ foreach($submenu[$parent] as $subitem_pos => $subitem){ $tree_item['items'][$subitem[2]] = array_merge( $this->templates['blank_item'], array('defaults' => $this->submenu2assoc($subitem, $subitem_pos, $parent)) ); } } $tree[$parent] = $tree_item; } $tree = $this->sort_menu_tree($tree); return $tree; } /** * Set all undefined menu fields to the default value * * @param array $item Menu item in the plugin's internal form * @return array */ function apply_defaults($item){ foreach($item as $key => $value){ //Is the field set? if ($value === null){ //Use default, if available if (isset($item['defaults']) && isset($item['defaults'][$key])){ $item[$key] = $item['defaults'][$key]; } } } return $item; } /** * Apply custom menu filters to an item of the custom menu. * * Calls two types of filters : * 'custom_admin_$item_type' with the entire $item passed as the argument. * 'custom_admin_$item_type-$field' with the value of a single field of $item as the argument. * * Used when converting the current custom menu to a WP-format menu. * * @param array $item Associative array representing one menu item (either top-level or submenu). * @param string $item_type 'menu' or 'submenu' * @param mixed $extra Optional extra data to pass to hooks. * @return array Filtered menu item. */ function apply_menu_filters($item, $item_type = '', $extra = null){ if ( empty($item_type) ){ //Only top-level menus have an icon $item_type = isset($item['icon_url'])?'menu':'submenu'; } $item = apply_filters("custom_admin_{$item_type}", $item, $extra); foreach($item as $field => $value){ $item[$field] = apply_filters("custom_admin_{$item_type}-$field", $value, $extra); } return $item; } /** * Get the value of a menu/submenu field. * Will return the corresponding value from the 'defaults' entry of $item if the * specified field is not set in the item itself. * * @param array $item * @param string $field_name * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null. * @return mixed Field value. */ function get_menu_field($item, $field_name, $default = null){ if ( isset($item[$field_name]) && ($item[$field_name] !== null) ){ return $item[$field_name]; } else { if ( isset($item['defaults']) && isset($item['defaults'][$field_name]) ){ return $item['defaults'][$field_name]; } else { return $default; } } } /** * Custom comparison function that compares menu items based on their position in the menu. * * @param array $a * @param array $b * @return int */ function compare_position($a, $b){ if ($a['position']!==null) { $p1 = $a['position']; } else { if ( isset($a['defaults']['position']) ){ $p1 = $a['defaults']['position']; } else { $p1 = 0; } } if ($b['position']!==null) { $p2 = $b['position']; } else { if ( isset($b['defaults']['position']) ){ $p2 = $b['defaults']['position']; } else { $p2 = 0; } } return $p1 - $p2; } /** * Sort the menus and menu items of a given menu according to their positions * * @param array $tree A menu structure in the internal format * @return array Sorted menu in the internal format */ function sort_menu_tree($tree){ //Resort the tree to ensure the found items are in the right spots uasort($tree, array(&$this, 'compare_position')); //Resort all submenus as well foreach ($tree as &$topmenu){ if (!empty($topmenu['items'])){ uasort($topmenu['items'], array(&$this, 'compare_position')); } } return $tree; } /** * Convert internal menu representation to the form used by WP. * * Note : While this function doesn't cause any side effects of its own, * it executes several filters that may modify global state. Specifically, * IFrame-handling callbacks in 'extras.php' may insert items into the * global $menu and $submenu arrays. * * @param array $tree * @return array $menu and $submenu */ function tree2wp($tree){ $menu = array(); $submenu = array(); $title_lookup = array(); //Sort the menu by position uasort($tree, array(&$this, 'compare_position')); //Prepare the top menu $first_nonseparator_found = false; foreach ($tree as $topmenu){ //Skip missing menus, unless they're user-created and thus might point to a non-standard file $custom = $this->get_menu_field($topmenu, 'custom', false); if ( !empty($topmenu['missing']) && !$custom ) { continue; }; //Skip leading menu separators. Fixes a superfluous separator showing up //in WP 3.0 (multisite mode) when there's a custom menu and the current user //can't access its first item ("Super Admin"). if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) continue; $first_nonseparator_found = true; //Menus that have both a custom icon URL and a "menu-icon-*" class will get two overlapping icons. //Fix this by automatically removing the class. The user can set a custom class attr. to override. if ( ameMenuItem::is_default($topmenu, 'css_class') && !ameMenuItem::is_default($topmenu, 'icon_url') && !in_array($topmenu['icon_url'], array('', 'none', 'div')) //Skip "no custom icon" icons. ) { $new_classes = preg_replace('@\bmenu-icon-[^\s]+\b@', '', $topmenu['defaults']['css_class']); if ( $new_classes !== $topmenu['defaults']['css_class'] ) { $topmenu['css_class'] = $new_classes; } } //Apply defaults & filters $topmenu = $this->apply_defaults($topmenu); $topmenu = $this->apply_menu_filters($topmenu, 'menu'); //Skip hidden entries if (!empty($topmenu['hidden'])) continue; //Build the menu structure that WP expects $menu[] = array( $topmenu['menu_title'], $topmenu['access_level'], $topmenu['file'], $topmenu['page_title'], $topmenu['css_class'], $topmenu['hookname'], //ID $topmenu['icon_url'] ); //Prepare the submenu of this menu if( !empty($topmenu['items']) ){ $items = $topmenu['items']; //Sort by position uasort($items, array(&$this, 'compare_position')); foreach ($items as $item) { //Skip missing items, unless they're user-created $custom = $this->get_menu_field($item, 'custom', false); if ( !empty($item['missing']) && !$custom ) continue; //Special case : plugin pages that have been moved to a different menu. //If the file field hasn't already been modified, we'll need to adjust it //to point to the old parent. This is required because WP identifies //plugin pages using *both* the plugin file and the parent file. if ( $this->get_menu_field($item, 'is_plugin_page', false) && ($item['file'] === null) ){ $default_parent = ''; if ( isset($item['defaults']) && isset($item['defaults']['parent'])){ $default_parent = $item['defaults']['parent']; } if ( $topmenu['file'] != $default_parent ){ $item['file'] = $default_parent . '?page=' . $item['defaults']['file']; } } $item = $this->apply_defaults($item); $item = $this->apply_menu_filters($item, 'submenu', $topmenu['file']); //Skip hidden items if (!empty($item['hidden'])) { continue; } $submenu[$topmenu['file']][] = array( $item['menu_title'], $item['access_level'], $item['file'], $item['page_title'], ); //Make a note of the page's correct title so we can fix it later //if necessary. $title_lookup[$item['file']] = $item['menu_title']; } } } return array($menu, $submenu, $title_lookup); } /** * Upgrade a menu tree to the currently used structure * Does nothing if the menu is already up to date. * * @param array $tree * @return array */ function upgrade_menu_structure($tree){ //Append new fields, if any foreach($tree as &$menu){ $menu = array_merge($this->templates['blank_menu'], $menu); $menu['defaults'] = array_merge($this->templates['basic_defaults'], $menu['defaults']); foreach($menu['items'] as $item_file => $item){ $item = array_merge($this->templates['blank_item'], $item); $item['defaults'] = array_merge($this->templates['basic_defaults'], $item['defaults']); $menu['items'][$item_file] = $item; } } return $tree; } /** * Output the menu editor page * * @return void */ function page_menu_editor(){ global $menu, $submenu; global $wp_roles; if ( !$this->current_user_can_edit_menu() ){ die("Access denied"); } $action = isset($this->post['action']) ? $this->post['action'] : (isset($this->get['action']) ? $this->get['action'] : ''); do_action('admin_menu_editor_header', $action); //Handle form submissions if (isset($this->post['data'])){ check_admin_referer('menu-editor-form'); //Try to decode a menu tree encoded as JSON $data = $this->json_decode($this->post['data'], true); if (!$data || (count($data) < 2) ){ $fixed = stripslashes($this->post['data']); $data = $this->json_decode( $fixed, true ); } $url = remove_query_arg('noheader'); if ($data){ //Ensure the user doesn't change the required capability to something they themselves don't have. if ( isset($data['options-general.php']['items']['menu_editor']) ){ $item = $data['options-general.php']['items']['menu_editor']; if ( !empty($item['access_level']) && !current_user_can($item['access_level']) ){ $item['access_level'] = null; $data['options-general.php']['items']['menu_editor'] = $item; } } //Save the custom menu $this->options['custom_menu'] = $data; $this->save_options(); //Redirect back to the editor and display the success message wp_redirect( add_query_arg('message', 1, $url) ); } else { //Or redirect & display the error message wp_redirect( add_query_arg('message', 2, $url) ); } die(); } //Kindly remind the user to give me money if ( !apply_filters('admin_menu_editor_is_pro', false) ){ $this->print_upgrade_notice(); } ?>
Settings saved.
Failed to decode input! The menu wasn\'t modified.