Helm/app/Plugin.php
2026-01-23 10:30:48 -05:00

1176 lines
No EOL
50 KiB
PHP

<?php
namespace CaptainCoreHelm;
class Plugin
{
public static function boot(): void
{
(new self())->register();
}
/**
* Register all hooks, organized by function.
*/
public function register(): void
{
$this->registerCore();
$this->registerAssets();
$this->registerIntegrations();
}
/* ============================ 1. Core Registration ============================ */
private function registerCore(): void
{
// Activation
register_activation_hook(CCHELM_FILE, [$this, 'onActivate']);
// User Profile Options
add_action('personal_options', [$this, 'renderProfileField']);
add_action('edit_user_profile', [$this, 'renderProfileField']);
add_action('personal_options_update', [$this, 'saveProfileField']);
add_action('edit_user_profile_update', [$this, 'saveProfileField']);
// Body Classes
add_filter('admin_body_class', [$this, 'adminBodyClass']);
add_filter('body_class', [$this, 'bodyClass']);
// Toolbar Logic
add_filter('cch_toolbar_keep_ids', [$this, 'frontendKeepIds']);
add_filter('show_admin_bar', [$this, 'forceShowAdminBar'], 10);
add_action('get_header', [$this, 'removeAdminBarBumpStyles']);
// Layout Reset (Critical CSS)
add_action('admin_head', [$this, 'printLayoutResetStyles'], 0);
add_action('wp_head', [$this, 'printLayoutResetStylesFront'], 0);
// Island UI Rendering
add_action('admin_footer', [$this, 'renderIsland'], 100);
add_action('wp_footer', [$this, 'renderIsland'], 100);
// Menu Snapshotting
add_action('load-plugin-install.php', [$this, 'redirectPluginInstallToFeatured']);
add_action('admin_menu', [$this, 'normalizePluginInstallUrl'], 9998);
add_action('admin_menu', [$this, 'captureMenuSnapshot'], 9999);
add_filter('cch_menu_snapshot', [$this, 'normalizeMenuVendors']);
add_filter('cch_core_menu_ids', [$this, 'frontendCoreIds']);
// Theme Variables
add_action('admin_head', [$this, 'printThemeVars'], 0);
add_action('wp_head', [$this, 'printThemeVarsFront'], 0);
// Error Page Interception
add_filter('wp_die_handler', [$this, 'interceptWpDieHandler']);
}
private function registerAssets(): void
{
add_action('admin_enqueue_scripts', [$this, 'enqueueAdmin']);
add_action('wp_enqueue_scripts', [$this, 'enqueueFront']);
}
private function registerIntegrations(): void
{
// Block Editor
add_action('enqueue_block_editor_assets', [$this, 'disableEditorFullscreenByDefault']);
// Site Editor (FSE)
add_action('load-site-editor.php', [$this, 'siteEditorInit']);
// Customizer
add_action('customize_controls_init', [$this, 'customizerControlsInit']);
add_action('customize_controls_print_styles', [$this, 'printLayoutResetStyles'], 5); // Reuse reset
add_action('customize_preview_init', [$this, 'customizerPreviewInit'], 20);
add_action('customize_controls_enqueue_scripts', [$this, 'customizerControlsScripts'], 20);
// Etch
add_filter('show_admin_bar', [$this, 'forceShowAdminBarInEtch'], 9999);
add_action('wp_enqueue_scripts', [$this, 'enqueueAssetsForEtch']);
// Elementor
add_action('admin_init', [$this, 'maybeBootstrapElementorEditor']);
add_action('elementor/editor/before_enqueue_styles', [$this, 'elementorEnqueueStyles'], 1);
add_action('elementor/editor/before_enqueue_scripts', [$this, 'elementorEnqueueScripts'], 1);
add_action('elementor/editor/wp_head', [$this, 'printLayoutResetStyles'], 5); // Reuse reset
add_action('elementor/editor/footer', [$this, 'elementorRenderIsland'], PHP_INT_MAX);
add_action('elementor/preview/init', [$this, 'elementorPreviewInit']);
add_action('elementor/editor/after_enqueue_scripts', [$this, 'elementorEditorScripts'], 20);
// Divi & Beaver Builder (Render special styles in footer)
add_action('wp_footer', [$this, 'printBuilderCompatibilityStyles'], 200);
// Builder specific inits to force admin bar behavior
add_action('wp', [$this, 'diviBuilderInit']);
add_action('wp', [$this, 'beaverBuilderInit']);
// Dark Mode Easter Egg
add_action('wp_ajax_cch_toggle_dark_mode', [$this, 'ajaxToggleDarkMode']);
// Menu Cache Purge
add_action('wp_ajax_cch_purge_menu_cache', [$this, 'ajaxPurgeMenuCache']);
}
/* =========================== 2. User Preference ========================== */
public function onActivate(): void
{
if ($uid = get_current_user_id()) {
update_user_meta($uid, 'cch_helm_enabled', '1');
}
}
public function renderProfileField(\WP_User $user): void
{
$enabled = $this->isEnabledForUser($user->ID);
?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="cch_helm_enabled"><?php echo esc_html__('CaptainCore Helm', 'captaincore-helm'); ?></label></th>
<td>
<label>
<input type="checkbox" name="cch_helm_enabled" id="cch_helm_enabled" value="1" <?php checked($enabled, true); ?> />
<?php echo esc_html__('Use the Helm admin view (Island UI, Quick Menu, hidden left menu).', 'captaincore-helm'); ?>
</label>
<?php wp_nonce_field('cch_helm_profile', 'cch_helm_nonce'); ?>
</td>
</tr>
</table>
<?php
}
public function saveProfileField(int $userId): void
{
if (!current_user_can('edit_user', $userId)) return;
if (!isset($_POST['cch_helm_nonce']) || !wp_verify_nonce($_POST['cch_helm_nonce'], 'cch_helm_profile')) return;
$enabled = isset($_POST['cch_helm_enabled']) ? '1' : '0';
update_user_meta($userId, 'cch_helm_enabled', $enabled);
}
private function isEnabledForUser(int $userId = 0): bool
{
$userId = $userId ?: get_current_user_id();
if (!$userId) return false;
$val = get_user_meta($userId, 'cch_helm_enabled', true) === '1';
return (bool) apply_filters('cch_is_helm_enabled', $val, $userId);
}
private function isEnabled(): bool
{
return $this->isEnabledForUser(get_current_user_id());
}
private function isDarkMode(): bool
{
return get_user_meta(get_current_user_id(), 'cch_admin_dark_mode', true) === '1';
}
public function ajaxToggleDarkMode(): void
{
if (!check_ajax_referer('cch_helm_nonce', 'nonce', false) || !current_user_can('read')) {
wp_send_json_error();
}
$uid = get_current_user_id();
$current = get_user_meta($uid, 'cch_admin_dark_mode', true);
$new = $current ? '0' : '1';
update_user_meta($uid, 'cch_admin_dark_mode', $new);
wp_send_json_success(['active' => $new === '1']);
}
public function ajaxPurgeMenuCache(): void
{
if (!check_ajax_referer('cch_helm_nonce', 'nonce', false) || !current_user_can('read')) {
wp_send_json_error();
}
$uid = get_current_user_id();
delete_user_meta($uid, 'cch_menu_snapshot');
delete_user_meta($uid, 'cch_menu_hash');
wp_send_json_success(['purged' => true]);
}
/* ============================= 3. Body Classes =========================== */
public function adminBodyClass(string $classes): string
{
if (!$this->isEnabled()) return $classes;
$classes .= ' cch-hide-admin-menu cch-island-mode cch-php-boot';
if ($this->isDarkMode()) {
$classes .= ' cch-dark-mode';
}
return trim($classes);
}
public function bodyClass(array $classes): array
{
if ($this->isEnabled() && is_user_logged_in()) {
$classes[] = 'cch-island-mode';
$classes[] = 'cch-php-boot';
if ($this->isDarkMode()) {
$classes[] = 'cch-dark-mode';
}
}
return $classes;
}
/* ============================== 4. The Island ============================= */
public function forceShowAdminBar(bool $show): bool
{
if (!$this->isEnabled()) return $show;
// Optimizations to avoid forcing inside iframes where not needed
if (isset($_GET['_wp-find-template'])) return $show;
if (isset($_GET['brickspreview']) || isset($_GET['bricks_preview'])) return $show;
if (isset($_GET['fl_builder_ui_iframe'])) return $show;
return true;
}
public function removeAdminBarBumpStyles(): void
{
if ($this->isEnabled()) remove_action('wp_head', '_admin_bar_bump_cb');
}
public function renderIsland(): void
{
if (!$this->isEnabled() || !is_user_logged_in() || is_customize_preview()) return;
// Iframe prevention checks
if (isset($_GET['elementor-preview']) || isset($_GET['_wp-find-template']) ||
isset($_GET['brickspreview']) || isset($_GET['bricks_preview']) ||
isset($_GET['fl_builder_ui_iframe']) ||
// Customizer Iframe Checks
isset($_GET['customize_changeset_uuid']) || isset($_GET['customize_theme']) || isset($_GET['customize_messenger_channel'])
) {
return;
}
$this->renderIslandHtml();
}
/**
* Renders the HTML for the Island.
*/
public function renderIslandHtml(): void
{
wp_enqueue_style('dashicons');
list($editLink, $editTitle, $viewLink, $viewTitle) = $this->getIslandLinks();
$shortcut = $this->shortcutLabel();
// Styles
$this->printIslandCss();
echo '<div id="cch-island-container">';
// Render PHP Context Menu logic here to prevent flash
$this->renderContextMenu();
// Main Island (The Unified Dock)
echo '<div id="cch-island-main">';
echo '<button type="button" id="cch-island-toggle" aria-label="Open Quick Menu (' . esc_attr($shortcut) . ')">';
echo '<span class="dashicons dashicons-menu-alt cch-island-icon"></span>';
// The spinner
echo '<span class="dashicons dashicons-update cch-island-loader" style="display:none;"></span>';
// The label is here, but hidden via CSS until loading triggers
echo '<span class="cch-island-label">Menu</span>';
echo '</button>';
if (class_exists('QueryMonitor')) {
echo '<button type="button" id="cch-island-qm" class="cch-island-action" aria-label="Toggle Query Monitor" title="Query Monitor">';
echo '<span class="dashicons dashicons-performance"></span>';
echo '</button>';
}
if ($editLink) {
$editIcon = 'dashicons-edit';
echo '<a href="' . esc_url($editLink) . '" class="cch-island-action" title="' . esc_attr($editTitle) . '" aria-label="' . esc_attr($editTitle) . '">';
echo '<span class="dashicons ' . esc_attr($editIcon) . '"></span>';
echo '</a>';
} elseif ($viewLink) {
echo '<a href="' . esc_url($viewLink) . '" class="cch-island-action" title="' . esc_attr($viewTitle) . '" aria-label="' . esc_attr($viewTitle) . '">';
echo '<span class="dashicons dashicons-visibility"></span>';
echo '</a>';
}
echo '</div>'; // End Main
// Hide Button (Optional, floats outside)
echo '<button type="button" id="cch-ui-hide-btn" title="Hide Menu" aria-label="Hide Menu">';
echo '<span class="dashicons dashicons-arrow-down-alt2"></span>';
echo '</button>';
echo '</div>'; // End Container
// Toast
echo '<div id="cch-ui-toast">Menu hidden. Press ' . esc_html($shortcut) . ' to restore.</div>';
}
/**
* Logic to determine Edit/View links based on context (Builders, Backend, Frontend).
*/
private function getIslandLinks(): array
{
global $wp_admin_bar;
$editLink = ''; $editTitle = ''; $viewLink = ''; $viewTitle = '';
$isDivi = !empty($_GET['et_fb']);
$isBB = isset($_GET['fl_builder']);
// Frontend: Get edit link
if (!is_admin() && !$isDivi && !$isBB) {
if ($wp_admin_bar && $node = $wp_admin_bar->get_node('edit')) {
$editLink = $node->href;
$editTitle = strip_tags($node->title) ?: 'Edit';
}
if (!$editLink && is_singular()) {
$post = get_queried_object();
if ($post && isset($post->ID) && current_user_can('edit_post', $post->ID)) {
$editLink = get_edit_post_link($post->ID);
$editTitle = 'Edit';
}
}
// Fallback: If no edit link found, link to wp-admin dashboard
if (!$editLink) {
$editLink = admin_url();
$editTitle = 'Dashboard';
}
}
// Backend or Builders: Get view/exit link
if (is_admin() || $isDivi || $isBB) {
if ($isDivi) {
$viewLink = add_query_arg('et_fb', '0', get_permalink());
$viewTitle = 'Exit Visual Builder';
} elseif ($isBB) {
$viewLink = add_query_arg('fl_builder_action', 'close', get_permalink());
$viewTitle = 'Exit Beaver Builder';
} elseif ($wp_admin_bar) {
$node = $wp_admin_bar->get_node('view') ?: $wp_admin_bar->get_node('view-site');
if ($node) {
$viewLink = $node->href;
$viewTitle = strip_tags($node->title) ?: 'View';
}
}
if (!$viewLink && !$isDivi && !$isBB) {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if ($screen && $screen->base === 'post' && isset($_GET['post'])) {
$viewLink = get_permalink(intval($_GET['post']));
$viewTitle = 'View';
} elseif ($screen && $screen->base === 'post' && isset($_POST['post_ID'])) {
$viewLink = get_permalink(intval($_POST['post_ID']));
$viewTitle = 'View';
}
}
if (!$viewLink && !$isDivi && !$isBB) {
$viewLink = home_url('/');
$viewTitle = 'View Site';
}
}
return [$editLink, $editTitle, $viewLink, $viewTitle];
}
/**
* Detects context and renders the popup bubble menu if a match is found.
* Moved from JS to PHP to prevent visual flash on load.
*/
private function renderContextMenu(): void
{
$match = $this->calculateContextMatch();
if (!$match || empty($match['subs']) || count($match['subs']) < 2) {
return;
}
$currentUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$currentPage = isset($_GET['page']) ? $_GET['page'] : '';
$currentWcPath = isset($_GET['path']) ? $_GET['path'] : '';
echo '<div id="cch-context-menu" class="cch-context-menu">';
echo '<div class="cch-context-dropdown">';
foreach ($match['subs'] as $sub) {
$isCurrent = false;
$href = $sub['href'];
$parsed = parse_url($href);
$query = [];
if (isset($parsed['query'])) parse_str($parsed['query'], $query);
$subPath = isset($query['path']) ? $query['path'] : '';
$subPage = isset($query['page']) ? $query['page'] : '';
if ($currentWcPath && $subPath) {
// WooCommerce path-based comparison
if (urldecode($currentWcPath) === urldecode($subPath)) $isCurrent = true;
} elseif (strpos($currentUrl, $href) !== false) {
$isCurrent = true;
} elseif ($currentPage && $subPage === $currentPage && !$currentWcPath && !$subPath) {
$isCurrent = true;
}
$classes = 'cch-context-item' . ($isCurrent ? ' cch-context-current' : '');
// External Link Check
$isExternal = false;
if (isset($parsed['host']) && $parsed['host'] !== $_SERVER['HTTP_HOST']) {
$isExternal = true;
}
echo '<a href="' . esc_url($href) . '" class="' . esc_attr($classes) . '" ' . ($isExternal ? 'target="_blank"' : '') . '>';
echo esc_html($sub['label']);
if ($isExternal) {
echo '<span class="dashicons dashicons-external cch-external-link-icon"></span>';
}
echo '</a>';
}
echo '</div>';
echo '<button class="cch-context-label" type="button">';
echo '<span class="cch-context-text">' . esc_html($match['label']) . '</span>';
echo '<span class="dashicons dashicons-arrow-up-alt2"></span>';
echo '</button>';
echo '</div>';
}
private function calculateContextMatch(): ?array
{
$snapshot = get_user_meta(get_current_user_id(), 'cch_menu_snapshot', true);
if (!is_array($snapshot) || empty($snapshot)) return null;
// Current Request Data
$currentUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$currentPage = isset($_GET['page']) ? $_GET['page'] : '';
$currentPostType = isset($_GET['post_type']) ? $_GET['post_type'] : '';
$currentWcPath = isset($_GET['path']) ? $_GET['path'] : '';
$bestMatch = null;
$bestMatchScore = -1;
foreach ($snapshot as $item) {
$score = 0;
$itemHref = isset($item['href']) ? $item['href'] : '';
// Parse item URL
$parsed = parse_url($itemHref);
$query = [];
if (isset($parsed['query'])) parse_str($parsed['query'], $query);
$itemPage = isset($query['page']) ? $query['page'] : '';
$itemPostType = isset($query['post_type']) ? $query['post_type'] : '';
$itemPath = isset($query['path']) ? $query['path'] : '';
// 1. Post Type Guard (Crucial for blocking "Posts" menu on CPTs)
if ($currentPostType && $currentPostType !== 'post') {
$isGenericPostLink = (
strpos($itemHref, '/edit.php') !== false ||
strpos($itemHref, '/post-new.php') !== false
);
// If generic edit link but no post_type param, and we are on a CPT, reject.
if ($isGenericPostLink && empty($itemPostType)) {
$score = -1000;
}
// Match
if ($itemPostType === $currentPostType) {
$score += 100;
}
}
// 2. WC Path Matching
if ($currentWcPath && $itemPath) {
$dCurrent = urldecode($currentWcPath);
$dItem = urldecode($itemPath);
if ($dCurrent === $dItem) {
$score += 150;
} elseif (strpos($dCurrent, '/analytics') === 0 && strpos($dItem, '/analytics') === 0) {
$score += 80;
} elseif (strpos($dCurrent, $dItem) === 0 && strlen($dItem) > 1) {
$score += 50;
}
}
// 3. Page Param Match
if ($currentPage && $itemPage) {
if ($currentPage === $itemPage) {
$score += 60;
} else {
// Prefix match (e.g. wc-admin vs wc-admin-settings)
$currPrefix = strtok($currentPage, '_-');
$itemPrefix = strtok($itemPage, '_-');
if ($currPrefix === $itemPrefix && (strpos($itemPage, '_') !== false || strpos($itemPage, '-') !== false)) {
$score += 10;
}
}
}
// 4. Substring Match
if (strpos($currentUrl, $itemHref) !== false) {
$score += 20;
}
// 5. Recursive Sub-item Check
if (!empty($item['subs'])) {
$subScore = 0;
foreach ($item['subs'] as $sub) {
$subHref = $sub['href'];
$sParsed = parse_url($subHref);
$sQuery = [];
if (isset($sParsed['query'])) parse_str($sParsed['query'], $sQuery);
$subPage = isset($sQuery['page']) ? $sQuery['page'] : '';
if ($currentPage && $subPage === $currentPage) {
$subScore = max($subScore, 100);
} elseif (strpos($currentUrl, $subHref) !== false) {
$subScore = max($subScore, 50);
}
if ($currentPostType && strpos($subHref, 'post_type=' . $currentPostType) !== false) {
$subScore = max($subScore, 40);
}
}
$score += $subScore;
}
if ($score > $bestMatchScore && $score > 0) {
$bestMatchScore = $score;
$bestMatch = $item;
}
}
return $bestMatch;
}
private function printIslandCss(): void
{
echo '<style>
#cch-island-container { position: fixed; bottom: 0; left: 50%; transform: translateX(-50%); z-index: 99999; display: flex; flex-direction: column; align-items: center; gap: 6px; padding-bottom: 10px; transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); }
#cch-island-container.cch-ui-hidden { transform: translateX(-50%) translateY(200%) !important; }
#cch-island-main { display: flex; align-items: center; justify-content: center; gap: 12px; }
/* Buttons */
#cch-island-toggle { background: var(--cch-color-1, #1d2327); color: #fff; border: 1px solid rgba(255,255,255,0.1); border-radius: 999px; height: 48px; padding: 0 24px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.25); display: flex; align-items: center; gap: 8px; font-family: -apple-system, sans-serif; font-size: 14px; font-weight: 500; transition: transform 0.1s ease; }
#cch-island-toggle .dashicons { font-size: 20px; width: 20px; height: 20px; left: 4px; position: relative; }
.cch-island-action { background: var(--cch-color-1, #1d2327); color: #fff; border: 1px solid rgba(255,255,255,0.1); border-radius: 50%; width: 48px; height: 48px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.25); display: flex; align-items: center; justify-content: center; text-decoration: none; transition: transform 0.1s ease; }
.cch-island-action .dashicons { font-size: 20px; width: 20px; height: 20px; }
#cch-ui-hide-btn { width: 16px; height: 16px; background: transparent; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--cch-color-1, #1d2327); }
#cch-ui-hide-btn:hover { opacity: 0.7; }
#cch-ui-hide-btn .dashicons { font-size: 12px; width: 12px; height: 12px; margin-top: 1px; }
/* Toast */
#cch-ui-toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%) translateY(20px); background: #1d2327; color: #fff; padding: 10px 20px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.15); font-family: -apple-system, sans-serif; font-size: 13px; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.4); opacity: 0; pointer-events: none; transition: opacity 0.3s ease, transform 0.3s ease; z-index: 100000; white-space: nowrap; }
#cch-ui-toast.cch-show { opacity: 1; transform: translateX(-50%) translateY(0); }
</style>';
}
public function frontendKeepIds(array $ids): array
{
if (!is_admin() && $this->isEnabled()) {
$ids[] = 'wp-admin-bar-my-account';
$ids[] = 'wp-admin-bar-site-name';
}
return $ids;
}
/* ============================= 5. Critical CSS =========================== */
public function printLayoutResetStyles(): void
{
if (!$this->isEnabled()) return;
// Optimization: Do not run inside builder iframes
if (isset($_GET['brickspreview']) || isset($_GET['bricks_preview']) || isset($_GET['fl_builder_ui_iframe'])) return;
echo '<style id="cch-critical-layout">
html { margin-top: 0 !important; }
body { margin-top: 0 !important; }
#wpadminbar { display: none !important; }
.elementor-editor-active #wpwrap { margin-top: 0 !important; }
.edit-site { margin-top: 0 !important; }
.wp-full-overlay { top: 0 !important; } /* Customizer */
/* Force Z-Index */
#cch-island-container { z-index: 500001; }
#cch-popout { z-index: 500002; }
@media screen { html { margin-top: 0 !important; } }
@media screen and (max-width: 782px) { html { margin-top: 0 !important; } }
@keyframes cch-spin { 100% { transform: rotate(360deg); } }
</style>';
}
public function printLayoutResetStylesFront(): void
{
if (!$this->isEnabled() || !is_user_logged_in()) return;
$this->printLayoutResetStyles();
}
/* ================================ 6. Assets ============================== */
public function enqueueAdmin(): void
{
if (!$this->isEnabled()) return;
$this->enqueueAssetsLogic(true);
}
public function enqueueFront(): void
{
if (!$this->isEnabled() || !is_user_logged_in()) return;
$this->enqueueAssetsLogic(false);
}
private function enqueueAssetsLogic(bool $isAdmin): void
{
wp_enqueue_style('cch-helm', CCHELM_URL . 'assets/css/helm.css', [], CCHELM_VER);
wp_enqueue_style('dashicons');
// Check user preference for Dark Mode
$isDarkMode = $this->isDarkMode();
// Register it so JS can toggle it, but only enqueue if active
wp_register_style('cch-dark-overrides', CCHELM_URL . 'assets/css/dark-admin.css', [], CCHELM_VER);
if ($isDarkMode && is_admin()) {
wp_enqueue_style('cch-dark-overrides');
}
$config = $this->getJsConfig($isAdmin);
// Pass the state to JS
$config['darkModeActive'] = (bool) $isDarkMode;
$config['nonce'] = wp_create_nonce('cch_helm_nonce'); // Needed for AJAX
$config['darkCssUrl'] = CCHELM_URL . 'assets/css/dark-admin.css'; // Needed for live toggle
wp_register_script('cch-helm', CCHELM_URL . 'assets/js/helm.js', [], CCHELM_VER, true);
wp_enqueue_script('cch-helm');
wp_add_inline_script('cch-helm', 'window.CCHELM_CONFIG = ' . wp_json_encode($config) . ';', 'before');
}
private function getJsConfig(bool $isAdmin): array
{
$coreIds = apply_filters('cch_core_menu_ids', $this->defaultCoreIds());
$toolbarKeep = apply_filters('cch_toolbar_keep_ids', $this->defaultToolbarKeepIds($isAdmin));
$updatesCount = 0;
if (function_exists('wp_get_update_data')) {
$data = wp_get_update_data();
$updatesCount = isset($data['counts']['plugins']) ? (int) $data['counts']['plugins'] : 0;
} else {
$updates = get_site_transient('update_plugins');
if (isset($updates->response) && is_array($updates->response)) {
$updatesCount = (int) count($updates->response);
}
}
return [
'coreIds' => array_values($coreIds),
'toolbarKeepIds' => array_values($toolbarKeep),
'updatesCount' => (int) $updatesCount,
'menuSnapshot' => get_user_meta(get_current_user_id(), 'cch_menu_snapshot', true) ?: [],
'toolbarLabelMap' => apply_filters('cch_toolbar_label_map', ['wp-admin-bar-comments' => 'Comments']),
'toolbarIconMap' => apply_filters('cch_toolbar_icon_map', [
'wp-admin-bar-site-name' => 'dashicons-admin-home',
'wp-admin-bar-new-content' => 'dashicons-plus',
'wp-admin-bar-comments' => 'dashicons-admin-comments',
'wp-admin-bar-updates' => 'dashicons-update',
'wp-admin-bar-customize' => 'dashicons-admin-customize',
'wp-admin-bar-search' => 'dashicons-search',
'wp-admin-bar-my-account' => 'dashicons-admin-users',
'wp-admin-bar-edit' => 'dashicons-edit',
]),
'toolbarSkipIds' => array_values(apply_filters('cch_toolbar_skip_ids', ['wp-admin-bar-menu-toggle', 'wp-admin-bar-search'])),
];
}
private function defaultCoreIds(): array
{
return ['menu-dashboard', 'menu-posts', 'menu-media', 'menu-pages', 'menu-comments', 'menu-appearance', 'menu-plugins', 'menu-users', 'menu-tools', 'menu-settings'];
}
private function defaultToolbarKeepIds(bool $isAdmin): array
{
if ($isAdmin) {
return ['wp-admin-bar-cch-popout-toggle', 'wp-admin-bar-site-name', 'wp-admin-bar-new-content', 'wp-admin-bar-comments', 'wp-admin-bar-my-account', 'wp-admin-bar-view'];
}
return ['wp-admin-bar-cch-popout-toggle', 'wp-admin-bar-edit', 'wp-admin-bar-new-content', 'wp-admin-bar-menu-toggle'];
}
public function frontendCoreIds(array $ids): array
{
return is_admin() ? $ids : $this->defaultCoreIds();
}
/* ============================ 7. Menu Snapshot =========================== */
public function redirectPluginInstallToFeatured(): void
{
if (!isset($_GET['tab']) && empty($_GET)) {
wp_safe_redirect(admin_url('plugin-install.php?tab=featured'));
exit;
}
}
public function normalizePluginInstallUrl(): void
{
global $submenu;
if (!isset($submenu['plugins.php']) || !is_array($submenu['plugins.php'])) {
return;
}
foreach ($submenu['plugins.php'] as $key => $item) {
if (isset($item[2]) && $item[2] === 'plugin-install.php') {
$submenu['plugins.php'][$key][2] = 'plugin-install.php?tab=featured';
}
}
}
public function captureMenuSnapshot(): void
{
if (!is_user_logged_in()) return;
$snapshot = $this->buildMenuSnapshot();
// Create a hash of the current menu structure
$newHash = md5(json_encode($snapshot));
// Get the stored hash
$uid = get_current_user_id();
$storedHash = get_user_meta($uid, 'cch_menu_hash', true);
// Only write to DB if the menu has actually changed
if ($newHash !== $storedHash) {
update_user_meta($uid, 'cch_menu_snapshot', $snapshot);
update_user_meta($uid, 'cch_menu_hash', $newHash);
}
}
public function normalizeMenuVendors(array $items): array
{
foreach ($items as &$it) {
// Gravity Forms
$isGfId = isset($it['id']) && $it['id'] === 'toplevel_page_gf_edit_forms';
$isGfSlug = false;
if (!empty($it['href'])) {
$q = [];
parse_str((string) parse_url($it['href'], PHP_URL_QUERY), $q);
$isGfSlug = isset($q['page']) && $q['page'] === 'gf_edit_forms';
}
if ($isGfId || $isGfSlug) $it['label'] = 'Gravity Forms';
// WooCommerce
if (!empty($it['id'])) {
if ($it['id'] === 'toplevel_page_woocommerce-marketing') {
$target = admin_url('admin.php?page=wc-admin&path=/marketing');
$it['href'] = !empty($it['subs'][0]['href']) ? $it['subs'][0]['href'] : $target;
}
if ($it['id'] === 'toplevel_page_woocommerce') {
$target = admin_url('admin.php?page=wc-admin');
$it['href'] = !empty($it['subs'][0]['href']) ? $it['subs'][0]['href'] : $target;
}
}
}
return $items;
}
private function buildMenuSnapshot(): array
{
global $menu, $submenu;
if (!is_array($menu) || empty($menu)) return [];
$items = [];
foreach ($menu as $m) {
if (strpos($m[4] ?? '', 'wp-menu-separator') !== false) continue;
$label = $this->cleanLabel((string)($m[0] ?? ''));
$slug = (string)($m[2] ?? '');
$href = (strpos($slug, '.php') !== false || strpos($slug, 'admin.php') !== false || strpos($slug, 'edit.php') !== false)
? admin_url($slug) : admin_url('admin.php?page=' . $slug);
$subs = [];
$seenLabels = [];
if (!empty($submenu[$slug])) {
foreach ($submenu[$slug] as $s) {
$subLabel = $this->cleanLabel($s[0] ?? '');
$subHref = $this->makeAdminHref((string)($s[2] ?? ''), $slug);
if ($subLabel === '' || $subHref === admin_url('admin.php?page=') || !$this->canAccessHref($subHref)) continue;
// Skip duplicate labels (e.g., WooCommerce Orders appears twice with different URLs)
$labelKey = strtolower($subLabel);
if (isset($seenLabels[$labelKey])) continue;
$seenLabels[$labelKey] = true;
$subs[] = ['label' => $subLabel, 'href' => esc_url_raw($subHref)];
}
}
if (($label === '' || $href === admin_url('admin.php?page=')) && empty($subs)) continue;
$topOk = $this->canAccessHref($href);
if (!$topOk && empty($subs)) continue;
$items[] = [
'id' => (string)($m[5] ?? ''),
'label' => $label,
'href' => esc_url_raw($href),
'iconClass' => (string)($m[6] ?? ''),
'subs' => $subs,
];
}
return apply_filters('cch_menu_snapshot', $items);
}
private function makeAdminHref(string $target, string $parentSlug): string
{
$t = ltrim($target, '/');
if (preg_match('#^https?://#i', $t)) return esc_url_raw($t);
$starts = ['admin.php','index.php','edit.php','upload.php','themes.php','plugins.php','post-new.php','users.php','user-new.php','tools.php','options-general.php','options-writing.php','options-reading.php','options-discussion.php','options-media.php','options-permalink.php','options-privacy.php','customize.php','site-editor.php','theme-editor.php','nav-menus.php','widgets.php','media-new.php','edit-comments.php','edit-tags.php','update-core.php','profile.php','plugin-install.php','plugin-editor.php','theme-install.php','site-health.php','import.php','export.php','export-personal-data.php','erase-personal-data.php'];
foreach ($starts as $p) {
if (stripos($t, $p) === 0) return esc_url_raw(admin_url($t));
}
$isBarePhp = substr($t, -4) === '.php' && strpos($t, '/') === false;
if ($isBarePhp && strpos($parentSlug, '.php') !== false) {
return esc_url_raw(admin_url($parentSlug . '?page=' . $t));
}
return esc_url_raw(admin_url('admin.php?page=' . $t));
}
private function cleanLabel(string $labelHtml): string
{
$label = wp_strip_all_tags(preg_replace('#<span[^>]*class=("|\')[^"\']*(screen-reader-text|awaiting-mod|update-plugins|count-[^"\'])[^"\']*\\1[^>]*>.*?</span>#si', '', $labelHtml));
$label = html_entity_decode($label, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$label = preg_replace('/\s+/u', ' ', trim($label));
$label = preg_replace('/(?:[\s\x{00A0}]*[()\[\]•·|:\-\x{2013}\x{2014}]*)?\d+(?:\+)?$/u', '', $label);
return preg_replace('/\b\d+\s+.*in moderation\b/i', '', $label);
}
private function canAccessHref(string $href): bool
{
$p = wp_parse_url($href);
if (!is_array($p) || empty($p['path'])) return true;
$path = ltrim((string) $p['path'], '/');
$q = [];
if (!empty($p['query'])) parse_str((string) $p['query'], $q);
// Taxonomy
if ($path === 'wp-admin/edit-tags.php') {
$taxName = isset($q['taxonomy']) ? (string) $q['taxonomy'] : '';
$tax = $taxName !== '' ? get_taxonomy($taxName) : null;
if (!$tax) return false;
if ($taxName === 'link_category') {
return ((bool) get_option('link_manager_enabled')) && current_user_can('manage_links');
}
$showUi = isset($tax->show_ui) ? (bool) $tax->show_ui : true;
$cap = isset($tax->cap->manage_terms) ? $tax->cap->manage_terms : 'manage_categories';
return $showUi && current_user_can($cap);
}
// Link Manager
if ($path === 'wp-admin/link-manager.php' || ($path === "wp-admin/admin.php" && isset($q['page']) && $q['page'] === 'edit-tags.php?taxonomy=link_category')) {
return ((bool) get_option('link_manager_enabled')) && current_user_can('manage_links');
}
// Post types
if ($path === 'wp-admin/edit.php') {
$pt = isset($q['post_type']) ? get_post_type_object($q['post_type']) : get_post_type_object('post');
if (!$pt) return false;
$showUi = isset($pt->show_ui) ? (bool) $pt->show_ui : true;
$cap = isset($pt->cap->edit_posts) ? $pt->cap->edit_posts : 'edit_posts';
return $showUi && current_user_can($cap);
}
return true;
}
/* ============================ 8. Editor Tweaks =========================== */
public function disableEditorFullscreenByDefault(): void
{
if (!is_admin() || !$this->isEnabled()) return;
$js = "jQuery(window).load(function(){ if(typeof wp!=='undefined' && wp.data && wp.data.select('core/edit-post') && wp.data.select('core/edit-post').isFeatureActive('fullscreenMode')){ wp.data.dispatch('core/edit-post').toggleFeature('fullscreenMode'); } });";
wp_add_inline_script('wp-blocks', $js);
}
public function siteEditorInit(): void
{
if (!$this->isEnabled()) return;
add_filter('show_admin_bar', '__return_true', PHP_INT_MAX);
require_once ABSPATH . WPINC . '/class-wp-admin-bar.php';
require_once ABSPATH . WPINC . '/admin-bar.php';
// Fix z-index for Site Editor
add_action('admin_head', function() {
echo '<style>
html, body, .edit-site { margin-top: 0 !important; }
#wpadminbar { display: none !important; }
#cch-island-container, #cch-popout, #cch-ui-toast { z-index: 9999999 !important; }
</style>';
}, 5);
add_action('admin_enqueue_scripts', function(){ wp_enqueue_script('admin-bar'); }, 5);
}
/* ========================= 9. Customizer Integration ===================== */
public function customizerControlsInit(): void
{
if (!$this->isEnabled()) return;
add_action('customize_controls_enqueue_scripts', [$this, 'enqueueAdmin'], 20);
add_action('customize_controls_print_footer_scripts', function() {
echo '<script>document.body.classList.add("cch-island-mode", "cch-hide-admin-menu");</script>';
$this->renderIslandHtml(); // Use standard HTML render
}, 1);
}
public function customizerPreviewInit(): void
{
if (!$this->isEnabled()) return;
$js = "(function(api){ if(window.__CCHELM_boundPreviewShortcut)return; window.__CCHELM_boundPreviewShortcut=true; const isMac=()=>/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)||/Mac/i.test(navigator.platform); const isOpenShortcut=(e)=>{ const k=(e.key&&e.key.toLowerCase()==='k')||(e.code&&e.code.toLowerCase()==='keyk')||e.keyCode===75; if(!k)return false; return isMac()?(e.metaKey&&e.altKey&&!e.ctrlKey):(e.ctrlKey&&e.altKey&&!e.metaKey); }; api.bind('preview-ready',function(){ window.addEventListener('keydown',function(e){ if(!isOpenShortcut(e))return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); api.preview.send('cch:toggle'); },true); }); })(wp.customize);";
wp_add_inline_script('customize-preview', $js);
}
public function customizerControlsScripts(): void
{
if (!$this->isEnabled()) return;
$js = "(function(api){ if(window.__CCHELM_boundControlsListener)return; window.__CCHELM_boundControlsListener=true; api.bind('ready',function(){ api.previewer.bind('cch:toggle',function(){ var btn=document.getElementById('cch-island-toggle'); if(btn)btn.click(); }); }); })(wp.customize);";
wp_add_inline_script('customize-controls', $js);
}
/* ========================= 10. Etch Integration ========================== */
public function forceShowAdminBarInEtch(bool $show): bool
{
return (isset($_GET['etch']) && $_GET['etch'] === 'magic') ? true : $show;
}
public function enqueueAssetsForEtch(): void
{
if (!isset($_GET['etch']) || $_GET['etch'] !== 'magic' || !$this->isEnabled()) return;
wp_enqueue_script('admin-bar');
wp_register_style('admin-bar-cch-helm', includes_url('css/admin-bar.min.css'), [], '1.0.0', 'all');
wp_enqueue_style('admin-bar-cch-helm');
wp_enqueue_script('cch-helm');
}
/* ====================== 11. Elementor Integration ======================== */
private function isElementorEditorRequest(): bool
{
return is_admin() && isset($_GET['action']) && $_GET['action'] === 'elementor';
}
public function maybeBootstrapElementorEditor(): void
{
if ($this->isEnabled() && $this->isElementorEditorRequest()) add_filter('show_admin_bar', '__return_true', PHP_INT_MAX);
}
public function elementorEnqueueStyles(): void
{
if ($this->isEnabled() && $this->isElementorEditorRequest()) {
wp_enqueue_style('admin-bar');
wp_enqueue_style('dashicons');
}
}
public function elementorEnqueueScripts(): void
{
if ($this->isEnabled() && $this->isElementorEditorRequest()) {
wp_enqueue_script('admin-bar');
$this->enqueueAdmin();
}
}
public function elementorRenderIsland(): void
{
if (!$this->isEnabled() || !$this->isElementorEditorRequest()) return;
require_once ABSPATH . WPINC . '/class-wp-admin-bar.php';
require_once ABSPATH . WPINC . '/admin-bar.php';
if (!is_object($GLOBALS['wp_admin_bar'])) {
$GLOBALS['wp_admin_bar'] = new \WP_Admin_Bar();
$GLOBALS['wp_admin_bar']->initialize();
}
$this->renderIsland();
}
public function elementorPreviewInit(): void
{
if (!$this->isEnabled()) return;
if (isset($GLOBALS['__CCHELM_boundElementorPreview'])) return;
$GLOBALS['__CCHELM_boundElementorPreview'] = true;
add_action('wp_enqueue_scripts', function () {
$js = "(function(){ const isMac=()=>/Mac|iPhone|iPad|iPod/i.test(navigator.userAgent)||/Mac/i.test(navigator.platform); const isOpenShortcut=(e)=>{ const k=(e.key&&e.key.toLowerCase()==='k')||(e.code&&e.code.toLowerCase()==='keyk')||e.keyCode===75; if(!k)return false; return isMac()?(e.metaKey&&e.altKey&&!e.ctrlKey):(e.ctrlKey&&e.altKey&&!e.metaKey); }; window.addEventListener('keydown',function(e){ if(!isOpenShortcut(e))return; e.preventDefault(); e.stopPropagation(); parent.postMessage({type:'cch:toggle'},'*'); },true); })();";
wp_add_inline_script('jquery-core', $js);
}, 20);
}
public function elementorEditorScripts(): void
{
if (!$this->isEnabled()) return;
$js = "(function(){ window.addEventListener('message',function(e){ if(!e.data||e.data.type!=='cch:toggle')return; var btn=document.getElementById('cch-island-toggle'); if(btn)btn.click(); }); })();";
wp_add_inline_script('jquery-core', $js);
}
/* ===================== 12. Divi/BB Integration ======================= */
public function diviBuilderInit(): void
{
if (!empty($_GET['et_fb']) && $this->isEnabled()) {
add_filter('show_admin_bar', '__return_true', 999);
}
}
public function beaverBuilderInit(): void
{
if (isset($_GET['fl_builder']) && !isset($_GET['fl_builder_ui_iframe']) && $this->isEnabled()) {
add_filter('show_admin_bar', '__return_true', 9999);
add_action('wp_enqueue_scripts', [$this, 'enqueueFront'], 9999);
add_action('fl_builder_ui_enqueue_scripts', [$this, 'enqueueFront']);
}
}
public function printBuilderCompatibilityStyles(): void
{
if (!$this->isEnabled()) return;
// Divi
if (!empty($_GET['et_fb'])) {
echo '<style id="cch-divi-integration">
html { margin-top: 0 !important; } #wpadminbar { display: none !important; }
body #cch-island-container { top: 20px !important; bottom: auto !important; z-index: 9999990 !important; }
body #cch-island-container.cch-ui-hidden { transform: translateX(-50%) translateY(-200%) !important; }
body #cch-popout { z-index: 9999999 !important; }
body #cch-ui-toast { top: 30px !important; bottom: auto !important; z-index: 9999999 !important; }
#cch-ui-hide-btn .dashicons { transform: rotate(180deg); }
</style>';
}
// Beaver Builder
if (isset($_GET['fl_builder']) && !isset($_GET['fl_builder_ui_iframe'])) {
echo '<style id="cch-beaver-builder-integration">
html { margin-top: 0 !important; } #wpadminbar { display: none !important; }
body #cch-island-container { z-index: 9999990 !important; }
body #cch-popout, body #cch-ui-toast { z-index: 9999999 !important; }
</style>';
}
}
/* ==================== 13. Error Page Injection ======================= */
public function interceptWpDieHandler($handler)
{
return [$this, 'renderWpDieIsland'];
}
public function renderWpDieIsland($message, $title = '', $args = [])
{
if (!$this->isEnabled()) {
_default_wp_die_handler($message, $title, $args);
return;
}
// Bail if this is a JSON, XML, or AJAX request (don't inject HTML UI)
if (
(function_exists('wp_is_json_request') && wp_is_json_request()) ||
(function_exists('wp_is_xml_request') && wp_is_xml_request()) || // WP 5.2+
(defined('DOING_AJAX') && DOING_AJAX) ||
(defined('REST_REQUEST') && REST_REQUEST)
) {
_default_wp_die_handler($message, $title, $args);
return;
}
$configJson = wp_json_encode($this->getJsConfig(is_admin()));
ob_start();
$this->printThemeVars();
$this->renderIslandHtml();
$islandHtml = ob_get_clean();
$cssUrl = CCHELM_URL . 'assets/css/helm.css?ver=' . CCHELM_VER;
$jsUrl = CCHELM_URL . 'assets/js/helm.js?ver=' . CCHELM_VER;
$dashiconsUrl = includes_url('css/dashicons.min.css');
$injection = "<!-- Helm -->
<link rel='stylesheet' href='{$dashiconsUrl}' media='all' />
<link rel='stylesheet' href='{$cssUrl}' media='all' />
<style>html, body { margin-bottom: 0 !important; } #cch-island-container, #cch-popout { z-index: 2147483647; }</style>
{$islandHtml}
<script>window.CCHELM_CONFIG = {$configJson};</script>
<script src='{$jsUrl}'></script>";
if (function_exists('_wp_die_process_input')) list($message, $title, $args) = _wp_die_process_input($message, $title, $args);
if (is_string($message)) $message .= $injection;
_default_wp_die_handler($message, $title, $args);
}
/* ======================= 14. Theme & Utils ========================== */
private function isMacUa(): bool
{
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
return (stripos($ua, 'macintosh') !== false || stripos($ua, 'mac os x') !== false || stripos($ua, 'iphone') !== false);
}
private function shortcutLabel(): string
{
return $this->isMacUa() ? '⌘⌥K' : 'Ctrl+Alt+K';
}
public function printThemeVars(): void
{
if (!$this->isEnabled()) return;
$colors = $this->getSchemeColors();
echo '<style id="cch-theme-vars">body{';
foreach ($colors as $index => $color) {
echo '--cch-color-' . intval($index) . ':' . esc_html(trim($color)) . ';';
}
echo '}</style>';
}
public function printThemeVarsFront(): void
{
if ($this->isEnabled() && is_user_logged_in()) $this->printThemeVars();
}
private function getSchemeColors(): array
{
$scheme = get_user_option('admin_color') ?: 'fresh';
global $_wp_admin_css_colors;
if (empty($_wp_admin_css_colors) && function_exists('register_admin_color_schemes')) register_admin_color_schemes();
if (isset($_wp_admin_css_colors[$scheme]->colors) && !empty($_wp_admin_css_colors[$scheme]->colors)) {
return (array) $_wp_admin_css_colors[$scheme]->colors;
}
$defaults = $this->defaultAdminColorSchemes();
return $defaults[$scheme] ?? $defaults['fresh'];
}
private function defaultAdminColorSchemes(): array
{
return [
'fresh' => ['#1d2327', '#2c3338', '#2271b1', '#72aee6'],
'light' => ['#e5e5e5', '#999999', '#d64e07', '#04a4cc'],
'modern' => ['#1e1e1e', '#3858e9', '#7b90ff'],
'blue' => ['#096484', '#4796b3', '#52accc', '#74B6CE'],
'midnight' => ['#25282b', '#363b3f', '#69a8bb', '#e14d43'],
'sunrise' => ['#b43c38', '#cf4944', '#dd823b', '#ccaf0b'],
'ectoplasm' => ['#413256', '#523f6d', '#a3b745', '#d46f15'],
'ocean' => ['#627c83', '#738e96', '#9ebaa0', '#aa9d88'],
'coffee' => ['#46403c', '#59524c', '#c7a589', '#9ea476'],
];
}
}