menu title (may contain HTML), 1 => capability, // 2 => menu slug, 4 => classes, 6 => icon $raw_title = isset($item[0]) ? $item[0] : ""; $capability = isset($item[1]) ? $item[1] : ""; $slug = isset($item[2]) ? $item[2] : ""; $classes = isset($item[4]) ? $item[4] : ""; $icon = isset($item[6]) ? $item[6] : ""; // Separators have a wp-menu-separator class and no real title. if (strpos($classes, "wp-menu-separator") !== false) { $items[] = ["separator" => true]; continue; } if (empty($capability) || !current_user_can($capability)) { continue; } // Aurora's own top-level page is represented in the sidebar's // dedicated branded nav section (see Shell::print_mount()'s // "auroraNav"), not in this WP-native-menu list — skip it here so it // isn't shown twice. if ($slug === "aurora-admin") { continue; } $title_data = self::clean_title($raw_title); $entry = [ "id" => $slug, "title" => $title_data["title"], "url" => self::resolve_url($slug), "icon" => self::resolve_icon($icon), "count" => $title_data["count"], "separator" => false, "submenu" => [], ]; if (isset($submenu[$slug]) && is_array($submenu[$slug])) { foreach ($submenu[$slug] as $sub) { $sub_cap = isset($sub[1]) ? $sub[1] : ""; if (empty($sub_cap) || !current_user_can($sub_cap)) { continue; } $sub_title = self::clean_title(isset($sub[0]) ? $sub[0] : ""); $sub_slug = isset($sub[2]) ? $sub[2] : ""; $entry["submenu"][] = [ "id" => $sub_slug, "title" => $sub_title["title"], "url" => self::resolve_url($sub_slug, $slug), "count" => $sub_title["count"], ]; } } $items[] = $entry; } return $items; } /** * Strips the markup WordPress packs into menu titles (update/count * bubbles) and pulls the numeric count out separately. * * @return array{title:string,count:int|null} */ private static function clean_title($raw) { $count = null; if (preg_match('/]*>(\d+)/', $raw, $matches)) { $count = (int) $matches[1]; } // WordPress menu titles are "Label count bubble" — the // human label is always the plain text before the first tag, so take // that rather than stripping tags (which would leave the count digits // dangling in the title). $label = $raw; $first_tag = strpos($raw, "<"); if ($first_tag !== false) { $label = substr($raw, 0, $first_tag); } $title = trim(wp_strip_all_tags($label)); return ["title" => $title, "count" => $count]; } /** * Builds the admin URL for a menu slug, mirroring how WordPress decides * between a direct file (foo.php) and a registered page (admin.php?page=). */ private static function resolve_url($slug, $parent_slug = "") { if (empty($slug)) { return ""; } if ($slug === "plugins.php") { return admin_url("plugins.php?page=aurora-admin-plugins"); } // Already an absolute URL. if (preg_match('#^https?://#', $slug)) { return $slug; } // A slug pointing at a real admin file, possibly with a query string. if (strpos($slug, ".php") !== false) { return admin_url($slug); } // Submenu of a file-based parent (e.g. edit.php) — the child slug is a // file too, handled above; otherwise it's a page registered under that // parent's screen, reached via the parent file with ?page=. if (!empty($parent_slug) && strpos($parent_slug, ".php") !== false) { return admin_url( $parent_slug . (strpos($parent_slug, "?") !== false ? "&" : "?") . "page=" . $slug ); } return admin_url("admin.php?page=" . $slug); } /** * Normalizes the menu icon into something the Vue side can render: * a dashicon class name, an image URL, or null for "use a default". */ private static function resolve_icon($icon) { if (empty($icon) || $icon === "none" || $icon === "div") { return null; } if (strpos($icon, "dashicons-") === 0) { return ["type" => "dashicon", "value" => $icon]; } if (strpos($icon, "data:image") === 0 || preg_match('#^https?://#', $icon)) { return ["type" => "image", "value" => $icon]; } return null; } }