| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* The Darkify admin menu. |
| 5 |
* |
| 6 |
* Registers the top-level menu page that hosts the React SPA, plus a "Get Help" |
| 7 |
* submenu entry of its own. Both render the same SPA root node; which page the |
| 8 |
* app opens on is derived from the `?page=` slug (see resolveInitialRoute in |
| 9 |
* darkify-react/src/main.jsx), so "Get Help" lands directly on the dedicated Help |
| 10 |
* page while the main entry lands on the settings sections. |
| 11 |
* |
| 12 |
* The menu title, slug, icon and position all come from the args the config class |
| 13 |
* already declared via `DarkifyOptions::options()` |
| 14 |
* (src/Admin/Views/DarkifyOptions.php), so the menu keeps its existing slug |
| 15 |
* (`darkify`), icon and place in the sidebar — an existing bookmark to |
| 16 |
* `?page=darkify` still lands on the settings screen, it is simply a React one now. |
| 17 |
* |
| 18 |
* @package darkify |
| 19 |
* @subpackage darkify/src/Admin |
| 20 |
* @author ThemeAtelier<themeatelierbd@gmail.com> |
| 21 |
*/ |
| 22 |
|
| 23 |
namespace ThemeAtelier\Darkify\Admin; |
| 24 |
|
| 25 |
use ThemeAtelier\Darkify\Admin\Schema\SchemaRegistry; |
| 26 |
|
| 27 |
if (! defined('ABSPATH')) { |
| 28 |
die; |
| 29 |
} |
| 30 |
|
| 31 |
class Menu |
| 32 |
{ |
| 33 |
/** |
| 34 |
* The option key whose page args drive the menu. |
| 35 |
*/ |
| 36 |
const OPTION_KEY = 'darkify'; |
| 37 |
|
| 38 |
/** |
| 39 |
* The main SPA page slug (unchanged from the old options screen). |
| 40 |
*/ |
| 41 |
const PAGE_SLUG = 'darkify'; |
| 42 |
|
| 43 |
/** |
| 44 |
* The Help page's own slug. It is a separate WordPress menu entry — not a tab, |
| 45 |
* modal or panel — so Help is reachable in one click from anywhere in wp-admin. |
| 46 |
*/ |
| 47 |
const HELP_SLUG = 'darkify-help'; |
| 48 |
|
| 49 |
public function __construct() |
| 50 |
{ |
| 51 |
\add_action('admin_menu', [$this, 'register']); |
| 52 |
// Old bookmarks / links to the retired "Get Help" screen are forwarded to |
| 53 |
// the SPA's own Help route (see redirect_legacy_help_page()). |
| 54 |
\add_action('admin_page_access_denied', [$this, 'redirect_legacy_help_page']); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* Register the top-level menu page, plus its submenu entries: an explicit |
| 59 |
* "Darkify" entry, "Get Help", and the free plugin's existing |
| 60 |
* "Upgrade To Pro! 👑" link. |
| 61 |
* |
| 62 |
* Every settings section is reached from the React admin's own header nav |
| 63 |
* instead of WP submenus, but Get Help gets its own submenu entry (matching |
| 64 |
* Pro's Menu.php) so it is reachable in one click from anywhere in wp-admin, |
| 65 |
* not only from inside the settings screen. The upgrade submenu is the same |
| 66 |
* external pricing link the free plugin has always registered — the |
| 67 |
* upgrade flow is deliberately unchanged by the React migration. |
| 68 |
*/ |
| 69 |
public function register(): void |
| 70 |
{ |
| 71 |
$args = SchemaRegistry::options(self::OPTION_KEY); |
| 72 |
|
| 73 |
$menu_title = $args['menu_title'] ?? \esc_html__('Darkify', 'darkify'); |
| 74 |
$capability = \apply_filters('darkify_ui_permission', $args['menu_capability'] ?? 'manage_options'); |
| 75 |
$position = $args['menu_position'] ?? null; |
| 76 |
$icon = $args['menu_icon'] ?? 'dashicons-lightbulb'; |
| 77 |
|
| 78 |
\add_menu_page( |
| 79 |
$menu_title, |
| 80 |
$menu_title, |
| 81 |
$capability, |
| 82 |
self::PAGE_SLUG, |
| 83 |
[$this, 'render'], |
| 84 |
$icon, |
| 85 |
$position |
| 86 |
); |
| 87 |
|
| 88 |
// Registered explicitly with the parent's own slug so it reads "Darkify" |
| 89 |
// instead of WordPress auto-duplicating the top-level menu title as the |
| 90 |
// first submenu item — that auto-duplication only happens when nothing |
| 91 |
// else claims the parent slug as a submenu (see Pro's Menu.php, same fix). |
| 92 |
\add_submenu_page( |
| 93 |
self::PAGE_SLUG, |
| 94 |
$menu_title, |
| 95 |
\esc_html__('Darkify', 'darkify'), |
| 96 |
$capability, |
| 97 |
self::PAGE_SLUG, |
| 98 |
[$this, 'render'] |
| 99 |
); |
| 100 |
|
| 101 |
// Get Help is no longer a WP submenu — it lives in the SPA rail's footer |
| 102 |
// (see AppSidebar). The retired `?page=darkify-help` still forwards to the |
| 103 |
// SPA's /help route (see below). |
| 104 |
|
| 105 |
\add_submenu_page( |
| 106 |
self::PAGE_SLUG, |
| 107 |
\__('Upgrade To Pro! 👑', 'darkify'), |
| 108 |
\sprintf('<span style="color: #35b747;font-weight:600;" class="darkify-get-pro-text">%s</span>', \__('Upgrade To Pro! 👑', 'darkify')), |
| 109 |
'manage_options', |
| 110 |
'https://darkifywp.com/pricing/?utm_source=darkify_plugin&utm_medium=submenu_page&utm_campaign=regular' |
| 111 |
); |
| 112 |
} |
| 113 |
|
| 114 |
/** |
| 115 |
* Forward the retired `?page=darkify-help` screen to the SPA's Help route. |
| 116 |
* |
| 117 |
* "Get Help" used to be its own menu page; it is now a route inside the one |
| 118 |
* SPA page. Without this, an existing bookmark (or any third-party link) to |
| 119 |
* the old slug would hit WordPress's "you are not allowed to access this |
| 120 |
* page" wall, since the slug is no longer a registered page. |
| 121 |
* |
| 122 |
* Hooked on `admin_page_access_denied` — which wp-admin/includes/menu.php |
| 123 |
* fires immediately before that `wp_die( …, 403 )` — because that check runs |
| 124 |
* while menu.php is being required, i.e. BEFORE `admin_init`; an admin_init |
| 125 |
* hook would never get the chance to redirect. The slug guard keeps this |
| 126 |
* scoped to our own retired page, so every other denied page still dies |
| 127 |
* normally. |
| 128 |
*/ |
| 129 |
public function redirect_legacy_help_page(): void |
| 130 |
{ |
| 131 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen check, no state is changed. |
| 132 |
$page = isset($_GET['page']) ? \sanitize_key(\wp_unslash($_GET['page'])) : ''; |
| 133 |
if ($page !== self::HELP_SLUG) { |
| 134 |
return; |
| 135 |
} |
| 136 |
|
| 137 |
\wp_safe_redirect(\admin_url('admin.php?page=' . self::PAGE_SLUG . '#/help')); |
| 138 |
exit; |
| 139 |
} |
| 140 |
|
| 141 |
/** |
| 142 |
* The page body: just the node React mounts on. Every menu entry renders this |
| 143 |
* same root — the SPA reads the `?page=` slug to decide which route to open. |
| 144 |
* |
| 145 |
* The `darkify_ignore` class opts the whole React admin out of Darkify's own |
| 146 |
* dark-mode engine. When "Admin Panel Dark Mode" is on, that engine runs on |
| 147 |
* every admin screen and force-rewrites element backgrounds/colors with |
| 148 |
* `!important` (classifying nodes as `darkify_style_bg` / `darkify_style_button` |
| 149 |
* etc. — see src/assets/css/client_main.css). On our settings screen that |
| 150 |
* stomped the app's own theme — most visibly the colour-preset swatches, whose |
| 151 |
* inline background was overwritten so every preview looked empty. The React |
| 152 |
* admin already themes itself via the `.dark` class, so the engine must leave |
| 153 |
* its subtree alone; `.darkify_ignore` / `.darkify_ignore *` is that engine's |
| 154 |
* built-in exclusion hook. |
| 155 |
*/ |
| 156 |
public function render(): void |
| 157 |
{ |
| 158 |
echo '<div id="darkify_react" class="darkify_ignore"></div>'; |
| 159 |
} |
| 160 |
} |
| 161 |
|