options_repository = $options_repository; $this->capabilities_service = $capabilities_service; } /** * Append an anchor class to every submenu item. * * Hooked to `add_menu_classes`, the last step of `wp-admin/includes/menu.php`: * `admin_menu` is not enough because plugins register items at any priority * (Elementor uses PHP_INT_MAX), while this filter runs after all of them and * still before `menu-header.php` renders the sidebar. The menu itself is * passed through untouched; only `$submenu` is edited. * * @since 1.0.32 * @param mixed $menu Top level menu items, as handed over by the filter. * @return mixed The same menu items. */ public function add_anchor_classes($menu = null) { if (!$this->should_add_anchors()) { return $menu; } global $submenu; if (!is_array($submenu)) { return $menu; } foreach ($submenu as $parent => $items) { if (!is_array($items)) { continue; } foreach ($items as $key => $item) { if (!is_array($item) || !isset($item[2]) || !is_scalar($item[2])) { continue; } $anchor = $this->build_anchor_class((string) $parent, (string) $item[2]); if ($anchor === '') { continue; } $existing = isset($item[4]) && is_scalar($item[4]) ? (string) $item[4] : ''; if (in_array($anchor, preg_split('/\s+/', $existing) ?: [], true)) { continue; } // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Editing $submenu on admin_menu is the documented way to alter the admin menu. $submenu[$parent][$key][4] = trim($existing . ' ' . $anchor); } } return $menu; } /** * Decide whether anchor classes are needed on this request. * * @since 1.0.32 * @return bool True when submenu items should be marked. */ private function should_add_anchors(): bool { /** * Filters whether admin submenu items receive selector anchor classes. * * @since 1.0.32 * @param bool $enabled Whether anchor classes are added. */ if (!apply_filters('adsd_enable_menu_anchors', true)) { return false; } if (!$this->capabilities_service->can_use_selector()) { return false; } // Anchors are only useful while rules are being applied, or while the // user is picking blocks and new rules are about to be created. if ($this->is_selection_mode_active()) { return true; } return !empty($this->options_repository->get_active_rules()); } /** * Check whether the block picker is currently active. * * @since 1.0.32 * @return bool True when selection mode is on. */ private function is_selection_mode_active(): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL parameter check, not form submission if (!isset($_GET['adsd_selection_mode_active'])) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- URL parameter check, not form submission return sanitize_text_field(wp_unslash($_GET['adsd_selection_mode_active'])) === '1'; } /** * Build the anchor class for a submenu item. * * The readable form doubles as documentation in the rules table; overly long * slug combinations fall back to a hash so the class stays usable. * * @since 1.0.32 * @param string $parent_slug Parent menu slug. * @param string $item_slug Submenu item slug. * @return string Class name, or an empty string when none can be built. */ private function build_anchor_class(string $parent_slug, string $item_slug): string { $item = $this->normalize_slug($item_slug); if ($item === '') { return ''; } $parent = $this->normalize_slug($parent_slug); $readable = self::CLASS_PREFIX . ($parent === '' ? $item : $parent . '-' . $item); if (strlen($readable) <= self::MAX_CLASS_LENGTH) { return $readable; } return self::CLASS_PREFIX . substr(md5($parent_slug . '|' . $item_slug), 0, 10); } /** * Reduce a menu slug to characters usable in a class name. * * Menu slugs are either file names (`update-core.php`) or query strings * (`admin.php?page=elementor-one-upgrade`); for the latter the `page` * parameter alone identifies the screen. * * @since 1.0.32 * @param string $slug Menu slug. * @return string Normalized slug. */ private function normalize_slug(string $slug): string { if (preg_match('/[?&]page=([^&]+)/', $slug, $matches) === 1) { $slug = $matches[1]; } $slug = (string) preg_replace('/\.php$/i', '', $slug); $slug = (string) preg_replace('/[^A-Za-z0-9_-]+/', '-', $slug); return trim(strtolower($slug), '-'); } }