array("administrator", "foo")] */ private $cached_user_roles = array(); private $cached_virtual_user_caps = array(); private $virtual_caps_for_this_call = array(); public $disable_virtual_caps = false; public $virtual_cap_mode = 3; //self::ALL_VIRTUAL_CAPS /** * @var array An index of URLs relative to /wp-admin/. Any menus that match the index will be ignored. */ private $menu_url_blacklist = array(); /** * @var array Menu editor page tabs. */ private $tabs = array(); /** * @var string The slug of the current settings tab, if any. */ private $current_tab = ''; /** * @var ameModule[] List of modules that were loaded for the current request. */ private $loaded_modules = array(); private $are_modules_loaded = false; /** * @var array List of capabilities that are used in the default admin menu. Used to detect meta capabilities. */ private $caps_used_in_menu = array(); public $is_access_test = false; private $test_menu = null; /** * @var ameAccessTestRunner|null */ private $access_test_runner = null; /** * @var Exception|null */ private $last_menu_exception = null; function init(){ $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, 'show_extra_icons' => false, 'custom_menu' => null, 'custom_network_menu' => null, 'first_install_time' => null, 'display_survey_notice' => true, 'plugin_db_version' => 0, 'security_logging_enabled' => false, 'menu_config_scope' => ($this->is_super_plugin() || !is_multisite()) ? 'global' : 'site', //super_admin, specific_user, or a capability. 'plugin_access' => $this->is_super_plugin() ? 'super_admin' : 'manage_options', //The ID of the user who is allowed to use this plugin. Only used when plugin_access == specific_user. 'allowed_user_id' => null, //The user who can see this plugin on the "Plugins" page. By default all admins can see it. 'plugins_page_allowed_user_id' => null, 'show_deprecated_hide_button' => true, //Note: Un-deprecated as of 2015.10.01. 'dashboard_hiding_confirmation_enabled' => true, //When to show submenu icons. 'submenu_icons_enabled' => 'if_custom', //"never", "if_custom" or "always". //Enable/disable CSS workaround that helps override menu icons set by other plugins. 'force_custom_dashicons' => true, //Menu editor UI colour scheme. "Classic" is the old blue/yellow scheme, and "wp-grey" is more WP-like. 'ui_colour_scheme' => 'classic', //User logins that will show up in the actor list at the top of the editor. 'visible_users' => array(), //Enable/disable the admin notice that tells the user where the plugin settings menu is. 'show_plugin_menu_notice' => true, //Where to place menu items that are not part of the last saved menu configuration. //This usually applies to new items added by other plugins and, in Multisite, items that exist on //the current site but did not exist on the site where the user last edited the menu configuration. 'unused_item_position' => 'relative', //"relative" or "bottom". //Permissions for menu items that are not part of the save menu configuration. //The default is to leave the permissions unchanged. 'unused_item_permissions' => 'unchanged', //"unchanged" or "match_plugin_access". //Verbosity level of menu permission errors. 'error_verbosity' => self::VERBOSITY_NORMAL, //Enable/disable menu configuration compression. Enabling it makes the DB row much smaller, //but adds decompression overhead to very admin page. 'compress_custom_menu' => false, //Make custom menu and page titles translatable with WPML. They will appear in the "Strings" section. //This only applies to custom (i.e. changed) titles. 'wpml_support_enabled' => true, //Prevent bbPress from resetting its own roles. This should allow the user to edit bbPress roles //with any role editing plugin. Disabled by default due to risk of conflicts and the performance impact. 'bbpress_override_enabled' => false, //Which modules are active or inactive. Format: ['module-id' => true/false]. 'is_active_module' => array( 'highlight-new-menus' => false, ), ); $this->serialize_with_json = false; //(Don't) store the options in JSON format //WP 4.3+ uses H1 headings for admin pages. Older versions use H2 instead. self::$admin_heading_tag = version_compare($GLOBALS['wp_version'], '4.3', '<') ? 'h2' : 'h1'; $this->settings_link = (is_network_admin() ? 'settings.php' : 'options-general.php') . '?page=menu_editor'; $this->magic_hooks = true; //Run our hooks last (almost). Priority is less than PHP_INT_MAX mostly for defensive programming purposes. //Old PHP versions have known bugs related to large array keys, and WP might have undiscovered edge cases. $this->magic_hook_priority = PHP_INT_MAX - 10; /* * Menu blacklist. Any menu items that *exactly* match one of the URLs on this list will be ignored. * They won't show up in the editor or the admin menu, but they will remain accessible (caps permitting). * * This is a workaround for plugins that add a menu item and then remove it. Most plugins do this * to create "Welcome" or "What's New" pages that are accessible but don't appear in the admin menu. * * We can't automatically detect menus like that. Here's why: * 1) Most plugins remove them too late, e.g. in admin_head. By that point, output has already started. * We need the finalize the list of menu items and their permissions before that. * 2) It's hard to automatically determine *why* a menu item was removed. We can't distinguish between * cosmetic changes like the hidden "welcome" items and people removing menus to deny access. */ $this->menu_url_blacklist = array( //WP RSS Aggregator 4.7.7 'index.php?page=wprss-welcome' => true, //AffiliateWP 1.7.8 'index.php?page=affwp-getting-started' => true, 'index.php?page=affwp-what-is-new' => true, 'index.php?page=affwp-credits' => true, //BuddyPress 2.3.4 'index.php?page=bp-about' => true, 'index.php?page=bp-credits' => true, //DW Question Answer 1.3.8.1 'index.php?page=dwqa-about' => true, 'index.php?page=dwqa-changelog' => true, 'index.php?page=dwqa-credits' => true, //Ninja Forms 2.9.41 'index.php?page=nf-about' => true, 'index.php?page=nf-changelog' => true, 'index.php?page=nf-getting-started' => true, 'index.php?page=nf-credits' => true, //All in One SEO Pack 2.3.9.2 'index.php?page=aioseop-about' => true, //WP Courseware 4.1.2 //'wpcw' => true, //This is commented out due to a bug. The Courseware top level menu and its first submenu //both have the URL "wpcw", but the top level menu also has some visible, non-blacklisted items. AME would //still hide the entire menu because the template builder doesn't check if a menu has submenu items. 'admin.php?page=wpcw-course-classroom' => true, 'admin.php?page=wpcw-student' => true, 'admin.php?page=WPCW_showPage_ConvertPage' => true, 'admin.php?page=WPCW_showPage_CourseOrdering' => true, 'admin.php?page=WPCW_showPage_GradeBook' => true, 'admin.php?page=WPCW_showPage_ModifyCourse' => true, 'admin.php?page=WPCW_showPage_ModifyModule' => true, 'admin.php?page=WPCW_showPage_ModifyQuestion' => true, 'admin.php?page=WPCW_showPage_ModifyQuiz' => true, 'admin.php?page=WPCW_showPage_UserCourseAccess' => true, 'admin.php?page=WPCW_showPage_UserProgess' => true, 'admin.php?page=WPCW_showPage_UserProgess_quizAnswers' => true, //Extended Widget Options 'index.php?page=extended-widget-opts-getting-started' => true, //Snax 'options-general.php?page=snax-pages-settings' => true, 'options-general.php?page=snax-lists-settings' => true, 'options-general.php?page=snax-quizzes-settings' => true, 'options-general.php?page=snax-polls-settings' => true, 'options-general.php?page=snax-stories-settings' => true, 'options-general.php?page=snax-memes-settings' => true, 'options-general.php?page=snax-audios-settings' => true, 'options-general.php?page=snax-videos-settings' => true, 'options-general.php?page=snax-images-settings' => true, 'options-general.php?page=snax-galleries-settings' => true, 'options-general.php?page=snax-embeds-settings' => true, 'options-general.php?page=snax-voting-settings' => true, 'options-general.php?page=snax-limits-settings' => true, 'options-general.php?page=snax-auth-settings' => true, 'options-general.php?page=snax-moderation-settings' => true, 'options-general.php?page=snax-embedly-settings' => true, 'options-general.php?page=snax-demo-settings' => true, 'index.php?page=snax-about' => true, 'options-general.php?page=snax-collections-settings' => true, 'options-general.php?page=snax-links-settings' => true, 'options-general.php?page=snax-extproduct-settings' => true, 'options-general.php?page=snax-slog-settings' => true, 'options-general.php?page=snax-slog-networks-settings' => true, 'options-general.php?page=snax-slog-locations-settings' => true, 'options-general.php?page=snax-slog-log-settings' => true, 'options-general.php?page=snax-slog-gdpr-settings' => true, 'options-general.php?page=snax-shares-settings' => true, 'options-general.php?page=snax-shares-positions-settings' => true, //Media Ace 'options-general.php?page=mace-image-bulk-settings' => true, 'options-general.php?page=mace-lazy_load-settings' => true, 'options-general.php?page=mace-watermarks-settings' => true, 'options-general.php?page=mace-hotlink-settings' => true, 'options-general.php?page=mace-gif-settings' => true, 'options-general.php?page=mace-auto-featured-image-settings' => true, 'options-general.php?page=mace-expiry-settings' => true, 'options-general.php?page=mace-video-settings' => true, 'options-general.php?page=mace-gallery-settings' => true, 'options-general.php?page=mace-general-settings' => true, //"What's Your Reaction" 'options-general.php?page=wyr-fakes-settings' => true, //WP-Job-Manager 1.34.1 'index.php?page=job-manager-setup' => true, //Simple Calendar 3.1.33 'index.php?page=simple-calendar_about' => true, 'index.php?page=simple-calendar_credits' => true, 'index.php?page=simple-calendar_translators' => true, ); //AJAXify screen options add_action('wp_ajax_ws_ame_save_screen_options', array($this,'ajax_save_screen_options')); //AJAXify hints and warnings add_action('wp_ajax_ws_ame_hide_hint', array($this, 'ajax_hide_hint')); add_action( 'wp_ajax_ws_ame_disable_dashboard_hiding_confirmation', array($this, 'ajax_disable_dashboard_hiding_confirmation') ); //Retrieve a list of pages via AJAX. add_action('wp_ajax_ws_ame_get_pages', array($this, 'ajax_get_pages')); //Get details about a specific page via AJAX. add_action('wp_ajax_ws_ame_get_page_details', array($this, 'ajax_get_page_details')); //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. $this->capture_request_vars(); add_action('admin_enqueue_scripts', array($this, 'enqueue_menu_fix_script')); //Enqueue miscellaneous helper scripts and styles. add_action('admin_enqueue_scripts', array($this, 'enqueue_helper_scripts')); add_action('admin_print_styles', array($this, 'enqueue_helper_styles')); //Make sure our scripts load before other plugins' scripts. add_action('admin_print_scripts', array($this, 'move_editor_scripts_to_top')); //User survey add_action('admin_notices', array($this, 'display_survey_notice')); //Tell first-time users where they can find the plugin settings page. add_action('all_admin_notices', array($this, 'display_plugin_menu_notice')); //Reset plugin access if the only allowed user gets deleted or their ID changes. add_action('wp_login', array($this, 'maybe_reset_plugin_access'), 10, 2); //Grant virtual capabilities like "super_user" to users. add_filter('user_has_cap', array($this, 'grant_virtual_caps_to_user'), 9, 3); add_filter('user_has_cap', array($this, 'regrant_virtual_caps_to_user'), 200, 1); //Update caches when the current user changes. add_action('set_current_user', array($this, 'update_current_user_cache'), 1, 0); //Run before most plugins. //Clear or refresh per-user caches when the user's roles or capabilities change. add_action('updated_user_meta', array($this, 'on_user_metadata_changed'), 10, 3); add_action('deleted_user_meta', array($this, 'on_user_metadata_changed'), 10, 3); //There's also a "set_user_role" hook, but it's only called by WP_User::set_role and not WP_User::add_role. //It's also redundant - WP_User::set_role updates user meta, so the above hooks already cover it. //Multisite: Clear role and capability caches when switching to another site. add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0); //"Test Access" feature. if ( (defined('DOING_AJAX') && DOING_AJAX) || isset($this->get['ame-test-menu-access-as']) ) { require_once 'access-test-runner.php'; $this->access_test_runner = new ameAccessTestRunner($this, $this->get); } //Additional links below the plugin description. add_filter('plugin_row_meta', array($this, 'add_plugin_row_meta_links'), 10, 2); //Utility actions. Modules can use them in their templates. add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs')); add_action('admin_menu_editor-display_header', array($this, 'display_settings_page_header')); add_action('admin_menu_editor-display_footer', array($this, 'display_settings_page_footer')); } function init_finish() { parent::init_finish(); $should_save_options = false; //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(); $should_save_options = true; } $this->zlib_compression = $this->options['compress_custom_menu']; //Track first install time. if ( !isset($this->options['first_install_time']) ) { $this->options['first_install_time'] = time(); $should_save_options = true; } if ( $this->options['plugin_db_version'] < $this->plugin_db_version ) { /* Put any activation code here. */ $this->options['plugin_db_version'] = $this->plugin_db_version; $should_save_options = true; } if ( $should_save_options ) { //Skip saving options if the plugin hasn't been fully activated yet. if ( $this->is_plugin_active($this->plugin_basename) ) { $this->save_options(); } else { //Yes, this method can actually run before WP updates the list of active plugins. That means functions //like is_plugin_active_for_network() will return false. As as result, we can't determine whether //the plugin has been network-activated yet, so lets skip setting up the default config until //the next page load. } } //This is here and not in init() because it relies on $options being initialized. if ( $this->options['security_logging_enabled'] ) { add_action('admin_notices', array($this, 'display_security_log')); } //Compatibility fix for MailPoet 3. $this->apply_mailpoet_compat_fix(); //bbPress role override. if ( !empty($this->options['bbpress_override_enabled']) ) { require_once __DIR__ . '/bbpress-role-override.php'; new ameBBPressRoleOverride(); } if ( did_action('plugins_loaded') ) { $this->load_modules(); } else { add_action('plugins_loaded', array($this, 'load_modules'), 11); } } public function load_modules() { //Load any active modules that haven't been loaded yet. foreach($this->get_active_modules() as $id => $module) { if ( array_key_exists($id, $this->loaded_modules) ) { continue; } /** @noinspection PhpIncludeInspection */ include ($module['path']); if ( !empty($module['className']) ) { $instance = new $module['className']($this); $this->loaded_modules[$id] = $instance; } else { $this->loaded_modules[$id] = true; } } $this->are_modules_loaded = true; //Set up the tabs for the menu editor page. Many tabs are provided by modules. $firstTabs = array('editor' => 'Admin Menu'); if ( is_network_admin() ) { //TODO: This could be in extras.php $firstTabs = array('network-admin-menu' => 'Network Admin Menu'); } $this->tabs = apply_filters('admin_menu_editor-tabs', $firstTabs); //The "Settings" tab is always last. $this->tabs['settings'] = 'Settings'; } /** * @return ameModule[] */ public function get_loaded_modules() { return $this->loaded_modules; } /** * 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; } /** * Create a configuration page and load the custom menu * * @return void */ function hook_admin_menu(){ global $menu, $submenu; //Compatibility fix for Shopp 1.2.9. This plugin has an "admin_menu" hook (Flow::menu) that adds another //"admin_menu" hook (AdminFlow::taxonomies) when it runs. Basically, it indirectly modifies the global //$wp_filters['admin_menu'] array while WordPress is iterating it (nasty!). Due to how PHP arrays are //implemented and how do_action() works, this second hook is the very last one to run, even after hooks //with a lower priority. //The only way we can see the changes made by the second hook is to do the same thing. static $firstRunSkipped = false; if ( !$firstRunSkipped && class_exists('Flow') ) { add_action(current_filter(), array($this, 'hook_admin_menu'), $this->magic_hook_priority + 1); $firstRunSkipped = true; return; } //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->set_custom_menu(null); } //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() ){ $this->log_security_note('Current user can edit the admin menu.'); //Determine the current menu editor page tab. reset($this->tabs); $this->current_tab = isset($this->get['sub_section']) ? strval($this->get['sub_section']) : key($this->tabs); $tab_title = ''; if ($this->current_tab !== 'editor' && isset($this->tabs[$this->current_tab])) { $tab_title = ' - ' . $this->tabs[$this->current_tab]; } $parent_slug = is_network_admin() ? 'settings.php' : 'options-general.php'; $page = add_submenu_page( $parent_slug, apply_filters('admin_menu_editor-self_page_title', 'Menu Editor') . $tab_title, apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'), apply_filters('admin_menu_editor-capability', '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'), 1); add_action("admin_print_styles-$page", array($this, 'enqueue_styles')); //Make sure Lodash doesn't conflict with the copy of Underscore that's bundled with WordPress. add_filter('script_loader_tag', array($this, 'lodash_noconflict'), 10, 2); //Filter exists since WP 4.1. //Compatibility fix for All In One Event Calendar; see the callback for details. add_action("admin_print_scripts-$page", array($this, 'dequeue_ai1ec_scripts')); //Compatibility fix for Participants Database. add_action("admin_print_scripts-$page", array($this, 'dequeue_pd_scripts')); //Experimental compatibility fix for Ultimate TinyMCE add_action("admin_print_scripts-$page", array($this, 'remove_ultimate_tinymce_qtags')); //Make a placeholder for our screen options (hacky) $screen_hook_name = $page; if ( is_network_admin() ) { $screen_hook_name .= '-network'; } if ( $this->current_tab === 'editor' ) { add_meta_box("ws-ame-screen-options", "[AME placeholder]", '__return_false', $screen_hook_name); } } //Compatibility fix for the WooCommerce order count bubble. Must be run before storing or processing $submenu. $this->apply_woocommerce_order_count_fix(); //Store the "original" menus for later use in the editor $this->default_wp_menu = $menu; $this->default_wp_submenu = $submenu; //Compatibility fix for bbPress. $this->apply_bbpress_compat_fix(); //Compatibility fix for WooCommerce (woo). $this->apply_woocommerce_compat_fix(); //Compatibility fix for WordPress Mu Domain Mapping. $this->apply_wpmu_domain_mapping_fix(); //Compatibility fix for Divi Training. $this->apply_divi_training_fix(); //As of WP 3.5, the "Links" menu is hidden by default. if ( !current_user_can('manage_links') ) { $this->remove_link_manager_menus(); } //Generate item templates from the default menu. $templateBuilder = new ameMenuTemplateBuilder(); $this->item_templates = $templateBuilder->build( $this->default_wp_menu, $this->default_wp_submenu, $this->menu_url_blacklist ); //Store the default order for later. It will be used when (re)inserting unused items into the menu. $this->relative_template_order = $templateBuilder->getRelativeTemplateOrder(); //Add extra templates that are not part of the normal menu. $this->item_templates = $this->add_special_templates($this->item_templates); //TODO: It would be nice to add the "Delete Site" item on multisite when on the main site. //Is there a custom menu to use? $custom_menu = $this->load_custom_menu(); if ( $custom_menu !== null ){ //Merge in data from the default menu $custom_menu['tree'] = $this->menu_merge($custom_menu['tree']); //Save the merged menu for later - the editor page will need it $this->merged_custom_menu = $custom_menu; //Convert our custom menu to the $menu + $submenu structure used by WP. //Note: This method sets up multiple internal fields and may cause side-effects. $this->user_cap_cache_enabled = true; $this->build_custom_wp_menu($this->merged_custom_menu['tree']); $this->user_cap_cache_enabled = false; if ( $this->is_access_test ) { $this->access_test_runner['wasCustomMenuApplied'] = true; $this->access_test_runner->setCurrentMenuItem($this->get_current_menu_item()); } if ( !$this->user_can_access_current_page() ) { $this->log_security_note('DENY access.'); if ( $this->is_access_test ) { $this->access_test_runner['userCanAccessCurrentPage'] = false; } $message = 'You do not have sufficient permissions to access this admin page.'; if ( ($this->options['error_verbosity'] >= self::VERBOSITY_NORMAL) ) { $current_item = $this->get_current_menu_item(); if ( isset($current_item, $current_item['access_decision_reason']) ) { $message .= sprintf( '
Reason: %s
', htmlentities($current_item['access_decision_reason']) ); } } if ($this->options['security_logging_enabled'] || ($this->options['error_verbosity'] >= self::VERBOSITY_VERBOSE) ) { $message .= 'Admin Menu Editor security log
'; $message .= $this->get_formatted_security_log(); } do_action('admin_page_access_denied'); wp_die($message); } else { $this->log_security_note('ALLOW access.'); if ( $this->is_access_test ) { $this->access_test_runner['userCanAccessCurrentPage'] = ($this->access_test_runner['currentMenuItem'] !== null); } } //Replace the admin menu just before it is displayed and restore it afterwards. //The fact that replace_wp_menu() is attached to the 'parent_file' hook is incidental; //there just wasn't any other, more suitable hook available. add_filter('parent_file', array($this, 'replace_wp_menu')); add_action('adminmenu', array($this, 'restore_wp_menu')); //A compatibility hack for Ozh's Admin Drop Down Menu. Make sure it also sees the modified menu. $ozh_adminmenu_priority = has_action('in_admin_header', 'wp_ozh_adminmenu'); if ( $ozh_adminmenu_priority !== false ) { add_action('in_admin_header', array($this, 'replace_wp_menu'), $ozh_adminmenu_priority - 1); add_action('in_admin_header', array($this, 'restore_wp_menu'), $ozh_adminmenu_priority + 1); } } else { do_action('admin_menu_editor-menu_replacement_skipped'); } } /** * Replace the current WP menu with our custom one. * * @param string $parent_file Ignored. Required because this method is a hook for the 'parent_file' filter. * @return string Returns the $parent_file argument. */ public function replace_wp_menu($parent_file = '') { global $menu, $submenu; $this->old_wp_menu = $menu; $this->old_wp_submenu = $submenu; $menu = $this->custom_wp_menu; $submenu = $this->custom_wp_submenu; $this->user_cap_cache_enabled = true; $this->filter_global_menu(); $this->user_cap_cache_enabled = false; do_action('admin_menu_editor-menu_replaced'); return $parent_file; } /** * Restore the default WordPress menu that was replaced using replace_wp_menu(). * * @return void */ public function restore_wp_menu() { global $menu, $submenu; $menu = $this->old_wp_menu; $submenu = $this->old_wp_submenu; } /** * Filter a menu so that it can be handed to _wp_menu_output(). This method basically * emulates the filtering that WordPress does in /wp-admin/includes/menu.php, with a few * additions of our own. * * - Removes inaccessible items and superfluous separators. * * - Sets accessible items to a capability that the user is guaranteed to have to prevent * _wp_menu_output() from choking on plugin-specific capabilities like "cap1,cap2+not:cap3". * * - Adds position-dependent CSS classes. * * @global array $menu * @global array $submenu * * @return void */ private function filter_global_menu() { global $menu, $submenu; global $_wp_menu_nopriv; //Caution: Modifying this array could lead to unexpected consequences. //Remove sub-menus which the user shouldn't be able to access, //and ensure the rest are visible. foreach ($submenu as $parent => $items) { foreach ($items as $index => $data) { if ( ! $this->current_user_can($data[1]) ) { unset($submenu[$parent][$index]); $_wp_submenu_nopriv[$parent][$data[2]] = true; } else { //The menu might be set to some kind of special capability that is only valid //within this plugin and not WP in general. Ensure WP doesn't choke on it. //(This is safe - we'll double-check the caps when the user tries to access a page.) $submenu[$parent][$index][1] = 'exist'; //All users have the 'exist' cap. } } if ( empty($submenu[$parent]) ) { unset($submenu[$parent]); } } //Remove consecutive submenu separators. This can happen if there are separators around a menu item //that is not accessible to the current user. foreach ($submenu as $parent => $items) { $found_separator = false; foreach ($items as $index => $item) { //Separator have a dummy #anchor as a URL. See wsMenuEditorExtras::create_submenu_separator(). if (strpos($item[2], '#submenu-separator-') === 0) { if ( $found_separator ) { unset($submenu[$parent][$index]); } $found_separator = true; } else { $found_separator = false; } } } //Remove menus that have no accessible sub-menus and require privileges that the user does not have. //Ensure the rest are visible. Run re-parent loop again. foreach ( $menu as $id => $data ) { if ( ! $this->current_user_can($data[1]) ) { $_wp_menu_nopriv[$data[2]] = true; } else { $menu[$id][1] = 'exist'; } //If there is only one submenu and it is has same destination as the parent, //remove the submenu. if ( ! empty( $submenu[$data[2]] ) && 1 == count ( $submenu[$data[2]] ) ) { $subs = $submenu[$data[2]]; $first_sub = array_shift($subs); if ( $data[2] == $first_sub[2] ) { unset( $submenu[$data[2]] ); } } //If submenu is empty... if ( empty($submenu[$data[2]]) ) { // And user doesn't have privs, remove menu. if ( isset( $_wp_menu_nopriv[$data[2]] ) ) { unset($menu[$id]); } } } unset($id, $data, $subs, $first_sub); //Remove any duplicated separators $separator_found = false; foreach ( $menu as $id => $data ) { if ( 0 == strcmp('wp-menu-separator', $data[4] ) ) { if ($separator_found) { unset($menu[$id]); } $separator_found = true; } else { $separator_found = false; } } unset($id, $data); //Remove the last menu item if it is a separator. $last_menu_key = array_keys( $menu ); $last_menu_key = array_pop( $last_menu_key ); if (!empty($menu) && 'wp-menu-separator' == $menu[$last_menu_key][4]) { unset($menu[$last_menu_key]); } unset( $last_menu_key ); //Add display-specific classes like "menu-top-first" and others. $menu = add_menu_classes($menu); } public function register_base_dependencies() { static $done = false; if ( $done ) { return; } $done = true; $this->register_jquery_plugins(); //Lodash library wp_register_auto_versioned_script('ame-lodash', plugins_url('js/lodash.min.js', $this->plugin_file)); //Knockout wp_register_auto_versioned_script('knockout', plugins_url('js/knockout.js', $this->plugin_file)); //Actor manager. wp_register_auto_versioned_script( 'ame-actor-manager', plugins_url('js/actor-manager.js', $this->plugin_file), array('ame-lodash') ); $roles = array(); $wp_roles = ameRoleUtils::get_roles(); foreach($wp_roles->roles as $role_id => $role) { $role['capabilities'] = $this->castValuesToBool($role['capabilities']); $roles[$role_id] = $role; } //Known users. $users = array(); $current_user = wp_get_current_user(); $logins_to_include = apply_filters('admin_menu_editor-users_to_load', array()); //Always include the current user. $logins_to_include[] = $current_user->get('user_login'); $logins_to_include = array_unique($logins_to_include); //Load user details. foreach($logins_to_include as $login) { $user = get_user_by('login', $login); if ( !empty($user) ) { $users[$login] = $this->user_to_property_map($user); } } //Compatibility workaround: Get the real roles of the current user even if other plugins corrupt the list. $users[$current_user->get('user_login')]['roles'] = array_values($this->get_user_roles($current_user)); $suspected_meta_caps = $this->detect_meta_caps($roles, $users); //The current user has all of the meta caps. That's how we know they're meta caps and not just regular //capabilities that simply haven't been granted to anyone. $users[$current_user->get('user_login')]['meta_capabilities'] = $suspected_meta_caps; //TODO: Include currentUserLogin $actor_data = array( 'roles' => $roles, 'users' => $users, 'isMultisite' => is_multisite(), 'capPower' => $this->load_cap_power(), 'suspectedMetaCaps' => $suspected_meta_caps, ); wp_localize_script('ame-actor-manager', 'wsAmeActorData', $actor_data); //Modules wp_register_auto_versioned_script( 'ame-access-editor', plugins_url('modules/access-editor/access-editor.js', $this->plugin_file), array('jquery', 'ame-lodash') ); //Let extras register their scripts. do_action('admin_menu_editor-register_scripts'); } /** * @access private */ public function register_jquery_plugins() { //jQuery JSON plugin wp_register_auto_versioned_script('jquery-json', plugins_url('js/jquery.json.js', $this->plugin_file), array('jquery')); //jQuery sort plugin wp_register_auto_versioned_script('jquery-sort', plugins_url('js/jquery.sort.js', $this->plugin_file), array('jquery')); //qTip2 - jQuery tooltip plugin wp_register_auto_versioned_script('jquery-qtip', plugins_url('js/jquery.qtip.min.js', $this->plugin_file), array('jquery')); //jQuery Form plugin. This is a more recent version than the one included with WP. wp_register_auto_versioned_script('ame-jquery-form', plugins_url('js/jquery.form.js', $this->plugin_file), array('jquery')); //jQuery cookie plugin wp_register_auto_versioned_script('ame-jquery-cookie', plugins_url('js/jquery.biscuit.js', $this->plugin_file), array('jquery')); } /** * Detect meta capabilities. * This only works if the current user is an admin. In Multisite, they must be a Super Admin. * * @param array $roles * @param array $users * @return array [capability => true] */ private function detect_meta_caps($roles, $users) { if ( !$this->current_user_can_edit_menu() || !is_super_admin() ) { return array(); } //Any capability that's assigned to a role probably isn't a meta capability. $allRealCaps = ameRoleUtils::get_all_capabilities(true); //Similarly, capabilities that are directly assigned to users are probably real. foreach($users as $user) { $allRealCaps = array_merge($allRealCaps, $user['capabilities']); } //Role IDs can also be used as capabilities. foreach($roles as $roleId => $role) { $allRealCaps[$roleId] = true; } //Collect all of the required capabilities from the admin menu. $menu = $this->get_default_menu(); ameMenu::for_each($menu['tree'], array($this, 'collect_menu_cap')); //Any capability that's part of the admin menu but not assigned to any role or user //is probably a meta capability. $suspectedMetaCaps = array_diff_key($this->caps_used_in_menu, $allRealCaps); //The current user is an admin and should have access to everything. If they don't have a cap, //that's probably a non-meta cap that isn't enabled for *anyone*. $suspectedMetaCaps = array_filter(array_keys($suspectedMetaCaps), 'current_user_can'); return array_fill_keys($suspectedMetaCaps, true); } /** * @access private * @param array $item */ public function collect_menu_cap($item) { if ( isset($item['defaults'], $item['defaults']['access_level']) ) { $this->caps_used_in_menu[$item['defaults']['access_level']] = true; } } /** @noinspection PhpUnusedPrivateMethodInspection */ /** * Unfinished feature: Detect which roles have which meta capabilities. * * Create a temp. user for each role, test which meta caps they have, then cache the results in a site option. * Put this part in an AJAX request to avoid a massive slowdown (takes several seconds even on a fast PC). * * @param array $suspected_meta_caps * @param string[] $roleIds * @return array */ private function analyse_role_meta_caps($suspected_meta_caps, $roleIds) { //$start = microtime(true); $results = array(); $real_current_user = wp_get_current_user(); foreach($roleIds as $role_id) { $id = wp_insert_user(array( 'role' => $role_id, 'user_login' => wp_slash('ametemp_' . wp_generate_password(14)), 'user_pass' => wp_generate_password(20), 'display_name' => 'Temporary user created by AME', )); $user = new WP_User($id); //Some plugins only check the current user and ignore the user ID passed to the "user_has_cap" filter. //To account for cases like that, we need to also change the current user. wp_set_current_user($user->ID); $results[$role_id] = array(); foreach($suspected_meta_caps as $meta_cap => $ignored) { $results[$role_id][$meta_cap] = $user->has_cap($meta_cap); } wp_delete_user($id); } //Restore the original user. wp_set_current_user($real_current_user->ID); /*$elapsed = microtime(true) - $start; printf('Meta cap analysis: %.2f ms%s
%s
.',
htmlentities($this->options['plugin_access'])
));
}
$action = isset($this->post['action']) ? $this->post['action'] : (isset($this->get['action']) ? $this->get['action'] : '');
do_action('admin_menu_editor-header', $action, $this->post);
if ( !empty($action) ) {
$this->handle_form_submission($this->post, $action);
}
//By default, show the "Hide" button only if the user has already hidden something with it,
//or if they're using the free version. Pro users should use role permissions instead, but can
//explicitly enable the button if they want.
if ( !isset($this->options['show_deprecated_hide_button']) ) {
if ( $this->is_pro_version() ) {
$this->options['show_deprecated_hide_button'] = ameMenu::has_hidden_items($this->merged_custom_menu);
$this->save_options();
} else {
$this->options['show_deprecated_hide_button'] = true;
}
}
if ( $this->current_tab === 'settings' ) {
$this->display_plugin_settings_ui();
} else if ( $this->current_tab == 'generate-menu-dashicons' ) {
require dirname(__FILE__) . '/generate-menu-dashicons.php';
} else if ( $this->current_tab === 'repair-database' ) {
$this->repair_database();
} else if ( $this->is_editor_page() ) {
$this->display_editor_ui();
} else {
do_action('admin_menu_editor-section-' . $this->current_tab);
}
//Let the Pro version script output it's extra HTML & scripts.
do_action('admin_menu_editor-footer');
do_action('admin_menu_editor-footer-' . $this->current_tab, $action);
}
private function repair_database() {
global $wpdb; /** @var wpdb $wpdb */
if ( !is_multisite() ) {
echo 'This is not Multisite. The "repair" function does not apply to your site.';
return;
}
echo '