PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / trunk
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) vtrunk
2.1.2 2.1.1 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 trunk 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 All 56 releases
darkify / src / Admin / Admin.php

Admin.php in Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) trunk, at src/Admin/Admin.php

641 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The admin-facing functionality of the plugin.
5 *
6 * @link https://themeatelier.net
7 * @since 1.0.0
8 *
9 * @package darkify
10 * @subpackage darkify/Admin
11 * @author ThemeAtelier<themeatelierbd@gmail.com>
12 */
13
14 namespace ThemeAtelier\Darkify\Admin;
15
16 use ThemeAtelier\Darkify\Admin\ReviewNotice\ReviewNotice;
17 use ThemeAtelier\Darkify\Admin\ReviewNotice\ThemeAtelier_Offer_Banner;
18 use ThemeAtelier\Darkify\Admin\Schema\SchemaDefaults;
19 use ThemeAtelier\Darkify\Admin\Views\DarkifyOptions;
20 use ThemeAtelier\Darkify\Includes\DarkifyExternalSupport;
21 use ThemeAtelier\Darkify\Includes\DarkifyUtils;
22 use ThemeAtelier\Darkify\Admin\DBUpdates;
23
24 /**
25 * The admin class
26 */
27 class Admin
28 {
29
30 /**
31 * The min of this plugin.
32 *
33 * @since 1.0.0
34 * @access private
35 * @var string $min The slug of this plugin.
36 */
37 private $min;
38 public $utils;
39 public $settings;
40 public $external_support;
41 private $plugin_name;
42 private $version;
43 public $unique_id;
44
45 /**
46 * Whether enqueue_scripts() actually put the admin engine in the queue on
47 * this request. Read by darkify_restore_no_conflict_scripts(), which must
48 * only re-add handles Darkify itself asked for.
49 *
50 * @var bool
51 */
52 private $engine_enqueued = false;
53
54 /**
55 * The class constructor.
56 *
57 * @param string $plugin_name The slug of the plugin.
58 * @param string $version Current version of the plugin.
59 */
60 public function __construct($plugin_name, $version)
61 {
62 $this->plugin_name = $plugin_name;
63 $this->version = $version;
64 $this->utils = new DarkifyUtils($this);
65 $this->external_support = new DarkifyExternalSupport($this);
66 $this->min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
67 if (function_exists('wp_rand')) {
68 $this->unique_id = wp_rand();
69 }
70
71 add_action('current_screen', function ($screen) {
72 $options = get_option('darkify');
73 $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
74 $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
75 $is_block_editor = method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
76
77 // Block Editor Dark Mode is INDEPENDENT of Admin Panel Dark Mode:
78 // the block editor darkens whenever Block Editor Dark Mode is on,
79 // even if the rest of wp-admin stays light. Every other admin screen
80 // still keys on Admin Panel Dark Mode.
81 $needs_engine = $is_block_editor
82 ? (bool) $block_editor_dark_mode
83 : (bool) $enable_admin_panel_dark_mode;
84
85 // Darkify's own settings/help screens additionally load it even while
86 // the option is OFF, so flipping "Admin Panel Dark Mode" in the React
87 // admin can show the admin-bar icon (and have it actually work)
88 // immediately instead of only after a reload. Loading it here cannot
89 // darken anything on its own: both the FOUC snippet in
90 // header_script.php and darkify_check_preloading() in client_main.js
91 // are gated on `darkify_admin_panel_dark_enabled`.
92 if ($needs_engine || $this->is_darkify_spa_page()) {
93 add_action('admin_bar_menu', array($this, 'darkify_admin_bar_switch'), 9999);
94 add_action('admin_print_scripts', array($this, 'darkify_admin_header_script'), 1);
95 add_action('admin_footer', array($this, 'darkify_admin_footer_script'));
96 }
97 });
98
99 new ReviewNotice();
100 new DBUpdates();
101 if (! defined('THEMEATELIER_OFFER_BANNER_LOADED')) {
102 define('THEMEATELIER_OFFER_BANNER_LOADED', true);
103 ThemeAtelier_Offer_Banner::instance();
104 }
105
106 // Building the schema is what evaluates every `__()` in
107 // src/Admin/Views/*.php, so the text domain has to be registered first —
108 // Darkify::load_textdomain() runs on this same hook at priority 1.
109 add_action('after_setup_theme', array($this, 'init_components'));
110 add_filter('autoptimize_filter_js_exclude', array($this, 'darkify_exclude_js_from_cache_plugins'));
111
112 /*
113 * "No-conflict mode" compatibility.
114 *
115 * Fluent Forms, FluentCRM, Fluent Booking, FluentSMTP, WP Social Ninja
116 * and friends dequeue *every* script served from wp-content/plugins on
117 * their own admin screens (Fluent Forms: app/Hooks/actions.php, on
118 * `wp_print_scripts` priority 1). That runs after `admin_enqueue_scripts`,
119 * so it strips Darkify's engine while the admin-bar node — printed by
120 * `admin_bar_menu` and untouched by the dequeue — keeps its inline
121 * onclick, producing "darkify_switch_trigger is not defined" and a dead
122 * toggle on e.g. admin.php?page=fluent_forms.
123 *
124 * Two layers: the published allow-list filters where a plugin offers one,
125 * and a generic re-enqueue after their sweep for the ones that don't.
126 */
127 add_filter('fluentform/exclude_js_slugs_from_dequeue', array($this, 'darkify_no_conflict_allow_slugs'));
128 add_filter('fluent_crm_asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
129 add_filter('fluent_booking/asset_listed_slugs', array($this, 'darkify_no_conflict_allow_slugs'));
130 add_action('wp_print_scripts', array($this, 'darkify_restore_no_conflict_scripts'), 100);
131 add_action('wp_print_styles', array($this, 'darkify_restore_no_conflict_styles'), 100);
132
133 add_action('admin_init', [$this, 'load_classic_editor_scripts']);
134 add_filter('admin_footer_text', array($this, 'darkify_admin_footer'), 1, 2);
135 $active_plugins = get_option('active_plugins');
136 foreach ($active_plugins as $active_plugin) {
137 $_temp = strpos($active_plugin, 'darkify.php');
138 if (false != $_temp) {
139 add_filter('plugin_action_links_' . DARKIFY_BASENAME, array($this, 'add_plugin_action_links'), 10, 2);
140 add_filter('plugin_row_meta', array($this, 'after_darkify_row_meta'), 10, 4);
141 }
142 }
143 }
144
145 public function init_components()
146 {
147 DarkifyOptions::options('darkify');
148 $this->seed_default_options();
149 }
150
151 /**
152 * Write the schema's defaults into the `darkify` option on a fresh install.
153 *
154 * Nothing used to create that option until the first save in the admin. Up to
155 * that point the two halves of the plugin disagreed: the React admin resolved
156 * each field from the schema (so the Switch Toggler showed Orbit, the declared
157 * default), while the frontend read the raw option — found nothing — and fell
158 * back to the literal in its own template, rendering the Classic switcher. Same
159 * class of mismatch for every other field whose template fallback differs from
160 * its declared default.
161 *
162 * Seeding makes the stored values the single source both sides read, so a site
163 * ships with exactly what the admin displays before anyone touches Settings.
164 *
165 * Runs on `init` (every request, admin and frontend), right after
166 * the config classes have registered their sections, and is a no-op once the
167 * option exists — so it never overwrites a user's saved settings, and an
168 * upgrade from an older version is left alone.
169 */
170 private function seed_default_options()
171 {
172 $existing = get_option('darkify');
173 if (is_array($existing) && ! empty($existing)) {
174 return;
175 }
176
177 $defaults = SchemaDefaults::for_option('darkify');
178 if (empty($defaults)) {
179 return;
180 }
181
182 // `update_option` rather than `add_option`: an install that already holds
183 // an empty/corrupt value (a stray '' or false) gets seeded too.
184 update_option('darkify', $defaults);
185 }
186
187 /**
188 * Whether the current screen is one of Darkify's own React SPA screens
189 * (Settings / Get Help) — the only place the Admin Panel Dark Mode option can
190 * be toggled, and therefore the only place that needs the engine loaded up
191 * front so the admin-bar icon can react instantly.
192 */
193 public function is_darkify_spa_page(): bool
194 {
195 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen check.
196 $page = isset($_GET['page']) ? \sanitize_key(\wp_unslash($_GET['page'])) : '';
197 return \in_array($page, Assets::SPA_SLUGS, true);
198 }
199
200 public function darkify_exclude_js_from_cache_plugins($excludes)
201 {
202 // Dark Reader proxies document.styleSheets and injects its own <style>
203 // elements; an optimizer that concatenates or defers it changes when it
204 // sees the page's sheets, which shows up as a half-darkened admin.
205 $excludes .= ',client_main.js,client_main.min.js'
206 . ',darkreader.js,darkreader.min.js'
207 . ',admin_darkreader.js,admin_darkreader.min.js';
208 return $excludes;
209 }
210
211 public function enqueue_styles()
212 {
213 $options = get_option('darkify');
214 $enable_admin_panel_dark_mode = isset($options["enable_admin_panel_dark_mode"]) ? $options["enable_admin_panel_dark_mode"] : false;
215 // Block Editor Dark Mode darkens the editor independently of Admin Panel
216 // Dark Mode, so the stylesheet must load there too.
217 $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
218 $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
219 $is_block_editor = $screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
220 $allow_admin_dark = $enable_admin_panel_dark_mode || ($is_block_editor && $block_editor_dark_mode);
221 // Darkify's own screens always get the stylesheet so the admin-bar icon
222 // can be revealed the moment the option is switched on. It only styles
223 // `.darkify_*` classes, so it is inert until the engine adds them.
224 if (($allow_admin_dark || $this->is_darkify_spa_page()) && $this->darkify_is_dark_mode_allowed()) {
225 if (!wp_style_is('darkify-admin-switch', 'enqueued')) {
226 wp_enqueue_style('darkify-admin-switch', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $this->min . '.css', array(), $this->darkify_asset_version('src/assets/css/client_main' . $this->min . '.css'), 'all');
227 }
228 }
229 // Review notice CSS
230 wp_enqueue_style('darkify-review-notice', DARKIFY_DIR_URL . 'src/Admin/ReviewNotice/assets/css/review-notice' . $this->min . '.css', array(), DARKIFY_VERSION, 'all');
231 }
232
233 public function enqueue_scripts($hook)
234 {
235 $options = get_option('darkify');
236 $enable_admin_panel_dark_mode = isset($options["enable_admin_panel_dark_mode"]) ? $options["enable_admin_panel_dark_mode"] : false;
237 $is_spa = $this->is_darkify_spa_page();
238 if (! $this->darkify_is_dark_mode_allowed()) {
239 return;
240 }
241
242 // Mirror the `current_screen` engine-load rule exactly, so the engine is
243 // only enqueued where its inline config (header_script.php) is ALSO
244 // printed. Block Editor Dark Mode is independent of Admin Panel Dark
245 // Mode: the block editor loads the engine when Block Editor Dark Mode is
246 // on (regardless of Admin Panel Dark Mode); every other admin screen
247 // keys on Admin Panel Dark Mode. (A SPA screen always gets the config.)
248 if (! $is_spa) {
249 $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
250 $is_block_editor = $screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor();
251 $block_editor_dark_mode = isset($options['block_editor_dark_mode']) ? $options['block_editor_dark_mode'] : false;
252 $allow = $is_block_editor ? (bool) $block_editor_dark_mode : (bool) $enable_admin_panel_dark_mode;
253 if (! $allow) {
254 return;
255 }
256 }
257
258 /*
259 * wp-admin runs the Dark Reader engine, NOT the per-element classifier in
260 * client_main.js. That engine walks every element and reads
261 * getComputedStyle on each, then re-walks on DOM mutation; wp-admin (the
262 * block editor especially) mutates continuously, so it never settles and
263 * the editor locks up. Dark Reader works per stylesheet instead, so the
264 * cost does not scale with element churn at all.
265 *
266 * The frontend is untouched and still runs client_main.js.
267 * See src/assets/js/admin_darkreader.js for the full rationale.
268 */
269 $darkify_lib_rel = 'src/assets/js/vendor/darkreader.min.js';
270 $darkify_engine_rel = 'src/assets/js/admin_darkreader' . $this->min . '.js';
271 $darkify_lib_url = DARKIFY_DIR_URL . $darkify_lib_rel;
272
273 /*
274 * Both in <head> (in_footer = false), deliberately. Dark Reader does not
275 * need a parsed body to start, and enabling it before first paint is what
276 * keeps the admin from flashing light and then darkening.
277 */
278 wp_enqueue_script(
279 'darkify-darkreader',
280 $darkify_lib_url,
281 array(),
282 $this->darkify_asset_version($darkify_lib_rel),
283 false
284 );
285
286 /*
287 * The block editor canvas and the classic editor's TinyMCE body are
288 * separate documents, and Dark Reader binds the realm it was loaded into.
289 * The engine injects its own copy into each same-origin frame, so it
290 * needs a URL it can point a <script> at from inside that frame.
291 *
292 * Darkify's own React settings screens are excluded from Dark Reader:
293 * they ship a full dark theme of their own (`.dark` in
294 * darkify-react/src/index.css) which also covers the surrounding wp-admin
295 * chrome, so darkening them again produces a double-dark UI — and because
296 * Dark Reader 4.9 cannot parse the `oklch()` values those tokens use, the
297 * unresolved properties settle on `--destructive` and paint card borders
298 * and toggles red. The engine still loads there: it owns the toggle the
299 * admin-bar icon calls, and the SPA mirrors the class onto `.dark` itself.
300 */
301 wp_add_inline_script(
302 'darkify-darkreader',
303 'window.darkifyDarkReaderSrc = ' . wp_json_encode($darkify_lib_url) . ';'
304 . 'window.darkifyAdminSelfThemed = ' . ($is_spa ? 'true' : 'false') . ';',
305 'before'
306 );
307
308 wp_enqueue_script(
309 'darkify-admin-engine',
310 DARKIFY_DIR_URL . $darkify_engine_rel,
311 array('darkify-darkreader'),
312 $this->darkify_asset_version($darkify_engine_rel),
313 false
314 );
315
316 $this->engine_enqueued = true;
317 }
318
319 /**
320 * Add Darkify to a no-conflict allow-list.
321 *
322 * Both filter shapes this is attached to pass an array of substrings matched
323 * against the script `src`, so the plugin folder name is the right token.
324 *
325 * @param array $slugs Allowed slugs.
326 *
327 * @return array
328 */
329 public function darkify_no_conflict_allow_slugs($slugs)
330 {
331 if (! is_array($slugs)) {
332 return $slugs;
333 }
334
335 $slugs[] = 'darkify';
336
337 return array_unique($slugs);
338 }
339
340 /**
341 * Put the admin engine back after a no-conflict sweep dropped it.
342 *
343 * Runs on `wp_print_scripts` at 100 — after the sweeps, which all hook it at
344 * 1 — and before `do_items()`, so a re-enqueue here still prints. Gated on
345 * `$engine_enqueued`, so it never resurrects a script Darkify itself chose
346 * not to load (or dequeued in admin_dequeue_for_specefic_pages).
347 */
348 public function darkify_restore_no_conflict_scripts()
349 {
350 if (! $this->engine_enqueued || ! is_admin()) {
351 return;
352 }
353
354 foreach (array('darkify-darkreader', 'darkify-admin-engine') as $handle) {
355 if (wp_script_is($handle, 'registered') && ! wp_script_is($handle, 'enqueued')) {
356 wp_enqueue_script($handle);
357 }
358 }
359 }
360
361 /**
362 * Same sweep, styles side: the engine adds `.darkify_dark_mode_enabled` and
363 * the switch classes, and client_main.css is what they mean. Losing only the
364 * stylesheet leaves a half-styled admin bar icon.
365 */
366 public function darkify_restore_no_conflict_styles()
367 {
368 if (! $this->engine_enqueued || ! is_admin()) {
369 return;
370 }
371
372 if (wp_style_is('darkify-admin-switch', 'registered') && ! wp_style_is('darkify-admin-switch', 'enqueued')) {
373 wp_enqueue_style('darkify-admin-switch');
374 }
375 }
376
377 /**
378 * Cache-busting version for a bundled asset: the plugin version plus the
379 * file's mtime, so rebuilding client_main.js/.css (which does NOT bump the
380 * plugin version) still changes the URL and browsers fetch the new file
381 * instead of serving a stale one. Mirrors Admin\Assets::asset_version().
382 */
383 public function darkify_asset_version(string $relative_path): string
384 {
385 $abs = DARKIFY_PATH . $relative_path;
386 $mtime = \is_readable($abs) ? \filemtime($abs) : false;
387 return $mtime ? DARKIFY_VERSION . '.' . $mtime : DARKIFY_VERSION;
388 }
389
390 /*
391 Dequeue Darkify scripts and styles on specific admin pages (e.g., MainWP pages)
392 */
393
394 public function admin_dequeue_for_specefic_pages()
395 {
396 $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
397 $dequeue_pages = array(
398 'mainwp_tab',
399 'managesites',
400 'ManageClients',
401 'CostSummary',
402 'InsightsOverview',
403 'Extensions',
404 'RESTAPI',
405 'mainwp-setup',
406 'ThemesManage',
407 'PluginsManage',
408 'InsightsManage',
409 'ManageGroups',
410 'UpdatesManage',
411 'PluginsInstall',
412 'PluginsAutoUpdate',
413 'PluginsIgnore',
414 'PluginsAbandoned',
415 'PluginsIgnoredAbandoned',
416 'ThemesInstall',
417 'ThemesAutoUpdate',
418 'ThemesIgnore',
419 'ThemesAbandoned',
420 'ThemesIgnoredAbandoned',
421 'UserBulkManage',
422 'UserBulkAdd',
423 'BulkImportUsers',
424 'UpdateAdminPasswords',
425 'PostBulkManage',
426 'Extensions-Mainwp-Backups',
427 'Extensions-Mainwp-Security',
428 'Extensions-Mainwp-Analytics',
429 'Mainwp-Monitoring',
430 'Extensions-Mainwp-Agency',
431 'Extensions-Mainwp-Administrative',
432 'Extensions-Mainwp-Development',
433 'Extensions-Mainwp-Performance',
434 'ClientAddNew',
435 'ClientImport',
436 'ManageCostTracker',
437 'CostTrackerAdd',
438 'AddApiKeys',
439 );
440
441
442 if (in_array($page, $dequeue_pages, true)) {
443 wp_deregister_style('darkify-admin-switch');
444 wp_dequeue_script('darkify-admin-client-main');
445 wp_dequeue_script('darkify-admin-engine');
446 wp_dequeue_script('darkify-darkreader');
447 // Keep darkify_restore_no_conflict_scripts() from undoing this.
448 $this->engine_enqueued = false;
449 }
450 }
451
452 /**
453 * Replace wp-admin's footer credit with Darkify's review request, on
454 * Darkify's own screens only.
455 *
456 * Screens are matched by page slug (`is_darkify_spa_page()`) rather than by
457 * a hardcoded screen id, so both the Settings and Get Help SPA screens match
458 * and the check keeps working if the menu is ever retitled. The slug list is
459 * the same source of truth the assets already load from.
460 *
461 * @param string $text Existing footer text.
462 *
463 * @return string
464 */
465 public function darkify_admin_footer($text)
466 {
467 if (! $this->is_darkify_spa_page()) {
468 return $text;
469 }
470
471 return sprintf(
472 /* translators: 1: start strong tag, 2: close strong tag. 3: start link 4: close link */
473 __('<i>Enjoying %1$sDarkify?%2$s Please rate us %3$sWordPress.org%4$s. Your positive feedback will help us grow more. Thank you! 😊</i>', 'darkify'),
474 '<strong>',
475 '</strong>',
476 '<span class="greet-footer-text-star">�
477
478
479
480
481 </span> <a href="https://wordpress.org/support/plugin/darkify/reviews/#new-post" target="_blank" rel="noopener noreferrer">',
482 '</a>'
483 );
484 }
485
486 // Plugin settings in plugin list
487 public function add_plugin_action_links(array $links)
488 {
489 $new_links = array(
490 sprintf('<a href="' . esc_url(admin_url('admin.php?page=darkify')) . '">' . esc_html__('Settings', 'darkify') . '</a>'),
491 sprintf('<a target="_blank" href="https://wordpress.org/support/plugin/darkify/">' . esc_html__('Support', 'darkify') . '</a>'),
492 );
493
494 $links[] = sprintf('<a style="font-weight: bold;color:#35b747" target="_blank" href="https://darkifywp.com/pricing/?utm_source=darkify_plugin&utm_medium=action_link&utm_campaign=regular">%s</a>', esc_html__('Go Pro!', 'darkify'));
495
496 return array_merge($new_links, $links);
497 }
498
499 /**
500 * Add plugin row meta link.
501 *
502 * @since 2.0
503 *
504 * @param array $plugin_meta .
505 * @param string $file .
506 *
507 * @return array
508 */
509 public function after_darkify_row_meta($plugin_meta, $file)
510 {
511
512 if (DARKIFY_BASENAME === $file) {
513 $plugin_meta[] = '<a href="' . DARKIFY_DEMO_URL . '" target="_blank">' . __('Live Demo', 'darkify') . '</a>';
514 }
515
516 return $plugin_meta;
517 }
518
519 public function darkify_admin_bar_switch($wp_admin_bar)
520 {
521 if (! $this->darkify_is_dark_mode_allowed()) {
522 return;
523 }
524
525 /*
526 * The block editor already carries its own toggle in the top toolbar
527 * (see Frontend/Templates/footer_script.php), firing the same
528 * darkify_switch_trigger() this node would — two controls for one state,
529 * side by side.
530 *
531 * The toolbar one is the keeper, not this one: Gutenberg hides the WP
532 * admin bar in fullscreen mode, which is the default, so this node is
533 * simply absent for most people editing a post. Dropping the toolbar
534 * button instead would leave them no toggle at all.
535 *
536 * The classic editor is deliberately not included here. Its TinyMCE moon
537 * toggles only the content frame, while this switch toggles the whole
538 * admin — two different controls, so both belong on screen.
539 */
540 $screen = \function_exists('get_current_screen') ? \get_current_screen() : null;
541 if ($screen && \method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
542 return;
543 }
544
545 $options = get_option('darkify');
546 $enable_admin_panel_dark_mode = isset($options['enable_admin_panel_dark_mode']) ? $options['enable_admin_panel_dark_mode'] : false;
547
548 // Everywhere but Darkify's own screens the node is only registered when
549 // the option is on, so it is always visible. On the SPA screens it is
550 // registered regardless and starts hidden while the option is off — the
551 // React admin then just flips this class (see WpAdminBarBridge), which is
552 // what makes the icon appear/disappear without a reload.
553 $classes = 'darkify_admin_bar_switch_container';
554 if (! $enable_admin_panel_dark_mode) {
555 $classes .= ' darkify_admin_bar_hidden';
556 }
557
558 $wp_admin_bar->add_node(array(
559 'parent' => 'top-secondary',
560 'id' => 'darkify_admin_bar_switch_container',
561 'meta' => array(
562 'class' => $classes,
563 // Guarded call: a plugin's "no-conflict mode" can still strip the
564 // engine on its own screens (see the compatibility hooks in the
565 // constructor). A missing function should be an inert click, not
566 // an uncaught ReferenceError in every visitor's console.
567 // No quotes in the expression: the admin bar escapes `meta`
568 // attributes, and the escaped quotes come back as literal
569 // entities inside the handler ("Invalid or unexpected token").
570 'onclick' => 'window.darkify_switch_trigger&&window.darkify_switch_trigger()',
571 ),
572 ));
573 }
574
575 public function darkify_is_dark_mode_allowed()
576 {
577
578 $options = get_option('darkify');
579 $options = is_array($options) ? $options : array();
580 // Guarded read: `disallowed_admin_pages` has no schema default, so the
581 // React settings save doesn't persist it until the field is used. Reading
582 // it unguarded would emit an "Undefined array key" warning on every
583 // dark-mode hook (see the Pro plugin's Admin.php for the full story).
584 $disallowed_admin_pages = isset($options['disallowed_admin_pages']) ? $options['disallowed_admin_pages'] : '';
585 /* Disable on Disallowed Admin Plugins */
586 if ($this->utils->isRestrictedByDisallowedAdminPages($disallowed_admin_pages)) {
587 return False;
588 }
589 return True;
590 }
591
592 public function darkify_admin_header_script()
593 {
594 if ($this->darkify_is_dark_mode_allowed()) {
595 include_once DarkifyUtils::darkify_locate_template('header_script.php');
596 }
597 }
598
599 public function darkify_admin_footer_script()
600 {
601 if ($this->darkify_is_dark_mode_allowed()) {
602 include_once DarkifyUtils::darkify_locate_template('footer_script.php');
603 }
604 }
605
606 /**
607 * Load classic editor scripts.
608 *
609 * @since 5.0.0
610 */
611 public function load_classic_editor_scripts()
612 {
613 $options = get_option('darkify');
614 $classic_editor_dark_mode = isset($options['classic_editor_dark_mode']) ? $options['classic_editor_dark_mode'] : '';
615 if (! current_user_can('edit_posts') && ! current_user_can('edit_pages')) {
616 return;
617 }
618 if (get_user_option('rich_editing') !== 'true') {
619 return;
620 }
621
622 // Bail if admin_classic_editor is not enabled.
623 if (! wp_validate_boolean($classic_editor_dark_mode)) {
624 return;
625 }
626
627 add_filter('mce_external_plugins', function ($plugins) {
628 // Honour the same debug switch as every other bundled asset; gulp
629 // builds the .min beside it and this was the one enqueue still
630 // pointing at the readable copy in production.
631 $plugins['darkify_button'] = plugins_url('src/assets/js/admin-classic-editor' . $this->min . '.js', DARKIFY_FILE);
632 return $plugins;
633 });
634
635 add_filter('mce_buttons', function ($buttons) {
636 $buttons[] = 'darkify_button';
637 return $buttons;
638 });
639 }
640 }
641