PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 2.1.0
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v2.1.0
2.1.3 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 All 57 releases
darkify / src / Admin / Admin.php

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

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