PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 1.5.5
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v1.5.5
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 / Frontend / Frontend.php

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

495 lines 24.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ThemeAtelier\Darkify\Frontend;
4
5 use ThemeAtelier\Darkify\Includes\DarkifyExternalSupport;
6 use ThemeAtelier\Darkify\Includes\DarkifyUtils;
7
8 if (!defined('ABSPATH')) {
9 die;
10 } // Cannot access directly.
11
12 /**
13 * The public-facing functionality of the plugin.
14 *
15 * @link https://themeatelier.net
16 * @since 1.0.0
17 *
18 * @package Darkify
19 * @subpackage Darkify/public
20 */
21
22 /**
23 * The public-facing functionality of the plugin.
24 *
25 * Defines the plugin name, version, and two examples hooks for how to
26 * enqueue the public-facing stylesheet and JavaScript.
27 *
28 * @package Darkify
29 * @subpackage Darkify/public
30 * @author ThemeAtelier <themeatelierbd@gmail.com>
31 */
32 class Frontend
33 {
34
35 /**
36 * The ID of this plugin.
37 *
38 * @since 1.0.0
39 * @access private
40 * @var string $plugin_name The ID of this plugin.
41 */
42 private $plugin_name;
43
44 /**
45 * The version of this plugin.
46 *
47 * @since 1.0.0
48 * @access private
49 * @var string $version The current version of this plugin.
50 */
51 private $version;
52
53 public $utils;
54 public $settings;
55 public $external_support;
56 public $unique_id;
57
58 /**
59 * Initialize the class and set its properties.
60 *
61 * @since 1.0.0
62 * @param string $plugin_name The name of the plugin.
63 * @param string $version The version of this plugin.
64 */
65 public function __construct($plugin_name, $version)
66 {
67 $this->plugin_name = $plugin_name;
68 $this->version = $version;
69
70 $this->utils = new DarkifyUtils($this);
71 $this->external_support = new DarkifyExternalSupport($this);
72
73 new DarkifyShortcode($this);
74
75 if (function_exists('wp_rand')) {
76 $this->unique_id = wp_rand();
77 }
78
79 $options = get_option('darkify');
80 $show_in_menu = isset($options['show_in_menu']) ? $options['show_in_menu'] : "";
81 $enable_switcher_in_menu = isset($show_in_menu["enable_switch_in_menu"]) ? $show_in_menu["enable_switch_in_menu"] : false;
82
83 if ($enable_switcher_in_menu) {
84 add_filter('wp_nav_menu_items', array($this, 'darkify_switch_in_menu'), 10, 2);
85 }
86
87 add_action('wp_enqueue_scripts', array($this, 'darkify_client_enqueue'), 100);
88 add_action('login_enqueue_scripts', array($this, 'darkify_client_enqueue'));
89 add_action('register_enqueue_scripts', array($this, 'darkify_client_enqueue'));
90 add_action('wp_head', array($this, 'darkify_client_header_script'), 1);
91 add_action('login_head', array($this, 'darkify_client_header_script'), 1);
92 add_action('register_head', array($this, 'darkify_client_header_script'), 1);
93 add_action('wp_footer', array($this, 'darkify_client_footer_script'));
94 add_action('login_footer', array($this, 'darkify_client_footer_script'));
95 add_action('register_footer', array($this, 'darkify_client_footer_script'));
96
97 add_action('init', array($this, 'darkify_register_switcher_styles'), 5);
98 add_filter('autoptimize_filter_js_exclude', array($this, 'darkify_exclude_js_from_cache_plugins'));
99 }
100
101 public function darkify_exclude_js_from_cache_plugins($excludes)
102 {
103 $excludes .= ',client_main.js,client_main.min.js';
104 return $excludes;
105 }
106
107 public function darkify_register_switcher_styles()
108 {
109 $min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
110 wp_register_style('darkify-client-main', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $min . '.css', array(), DARKIFY_VERSION);
111 wp_register_style('theme-classic', DARKIFY_DIR_URL . 'src/assets/css/switcher/classic' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
112 wp_register_style('theme-expand', DARKIFY_DIR_URL . 'src/assets/css/switcher/expand' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
113 wp_register_style('theme-inner-moon', DARKIFY_DIR_URL . 'src/assets/css/switcher/inner-moon' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
114 wp_register_style('theme-within', DARKIFY_DIR_URL . 'src/assets/css/switcher/within' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
115 wp_register_style('theme-orbit', DARKIFY_DIR_URL . 'src/assets/css/switcher/orbit' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
116 }
117
118 function darkify_client_enqueue()
119 {
120 if ($this->darkify_is_dark_mode_allowed()) {
121 $options = get_option('darkify');
122 $min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
123
124 wp_enqueue_style('darkify-client-main', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $min . '.css', array(), DARKIFY_VERSION);
125 wp_enqueue_script('darkify-client-main', DARKIFY_DIR_URL . 'src/assets/js/client_main' . $min . '.js', array('jquery'), DARKIFY_VERSION);
126
127 // When "Frontend Iframe Dark Mode" is OFF, use a two-stage inline script
128 // strategy that covers all scenarios — source JS, minified JS,
129 // cross-origin iframes, and same-origin WP pages embedded as iframes.
130 //
131 // ROOT CAUSE: a same-origin WP page loaded inside an <iframe> has its
132 // OWN copy of client_main[.min].js running. That copy calls
133 // darkify_check_preloading() → localStorage.darkify_last_state="1" →
134 // adds darkify_dark_mode_enabled to the iframe's <html> → goes dark.
135 // The parent's darkify_process_iframes guard is irrelevant here.
136 //
137 // Stage 1 — 'before' (runs BEFORE client_main[.min].js on EVERY page):
138 // If this page is rendering inside a frontend iframe, intercept the
139 // localStorage.darkify_last_state property via Object.defineProperty so
140 // that darkify_check_preloading() always reads "0" and returns false.
141 // This prevents the iframe page's own Darkify from initialising dark
142 // mode at all — works with both source and minified JS because
143 // darkify_check_preloading() always reads the property directly.
144 // The source JS also has a _dkf_iframe_disabled guard for belt-and-
145 // suspenders coverage.
146 //
147 // Stage 2 — 'after' (runs AFTER client_main[.min].js on EVERY page):
148 // • Cleans up the localStorage property intercept from Stage 1.
149 // • Overrides the five injection helpers so the PARENT page's
150 // darkify_apply_dark_to_iframe() applyDirect() closures (which look
151 // up these functions by name in the global scope at call time) become
152 // no-ops for cross-origin and not-yet-loaded iframes.
153 $enable_frontend_iframe = isset($options['enable_frontend_iframe_dark_mode']) ? $options['enable_frontend_iframe_dark_mode'] : true;
154 if (!$enable_frontend_iframe) {
155
156 // Stage 1 — intercept localStorage.darkify_last_state BEFORE client_main.
157 wp_add_inline_script(
158 'darkify-client-main',
159 '(function(){' .
160 // Only apply when this window is a child frame, the setting
161 // exists and is OFF, and it is not the admin panel context.
162 'if(window===window.top)return;' .
163 'if(typeof darkify_enable_frontend_iframe_dark_mode==="undefined"' .
164 '||darkify_enable_frontend_iframe_dark_mode==="1")return;' .
165 'if(typeof darkify_is_this_admin_panel!=="undefined"' .
166 '&&darkify_is_this_admin_panel==="1")return;' .
167 // Define an own property on localStorage that intercepts reads
168 // of darkify_last_state. darkify_check_preloading() uses
169 // localStorage.darkify_last_state (property access, not getItem),
170 // so this getter is hit every time the function runs.
171 // Returning "0" makes the function take the "was explicitly set
172 // to 0 → not preloaded" branch and return false.
173 // The setter is silently ignored so state writes in the iframe
174 // don't pollute the shared localStorage for other tabs/pages.
175 'try{' .
176 'Object.defineProperty(localStorage,"darkify_last_state",{' .
177 'get:function(){return"0";},set:function(){},configurable:true' .
178 '});' .
179 '}catch(e){' .
180 // Fallback for environments where Object.defineProperty on
181 // Storage objects is not supported: temporarily set the value.
182 'try{window._dkf_ls_bk=localStorage.darkify_last_state;' .
183 'localStorage.darkify_last_state="0";}catch(e2){}' .
184 '}' .
185 '})();',
186 'before'
187 );
188
189 // Stage 2 — cleanup + injection helper overrides AFTER client_main.
190 wp_add_inline_script(
191 'darkify-client-main',
192 '(function(){' .
193 // Remove the own-property intercept so subsequent reads of
194 // localStorage.darkify_last_state return the real stored value.
195 // "delete" removes the own property, restoring prototype lookup.
196 'if(window!==window.top){' .
197 'try{delete localStorage.darkify_last_state;}catch(e){' .
198 // Fallback: restore saved value
199 'try{if(typeof window._dkf_ls_bk!=="undefined"){' .
200 'if(window._dkf_ls_bk)localStorage.darkify_last_state=window._dkf_ls_bk;' .
201 'else localStorage.removeItem("darkify_last_state");' .
202 '}delete window._dkf_ls_bk;}catch(e2){}' .
203 '}' .
204 '}' .
205 // Override injection helpers so the PARENT page's applyDirect()
206 // closures (which already hold references to these global
207 // functions) become no-ops for frontend iframes.
208 'function _dkfAdmin(){' .
209 'return typeof darkify_is_this_admin_panel!=="undefined"&&darkify_is_this_admin_panel==="1";' .
210 '}' .
211 'if(typeof darkify_process_iframes==="function"){' .
212 'var _op=darkify_process_iframes;' .
213 'darkify_process_iframes=function(){if(_dkfAdmin())return _op();};' .
214 '}' .
215 'if(typeof darkify_inject_css_into_iframe==="function"){' .
216 'var _oi=darkify_inject_css_into_iframe;' .
217 'darkify_inject_css_into_iframe=function(d){if(_dkfAdmin())return _oi(d);};' .
218 '}' .
219 'if(typeof darkify_sync_iframe_vars==="function"){' .
220 'var _ov=darkify_sync_iframe_vars;' .
221 'darkify_sync_iframe_vars=function(d){if(_dkfAdmin())return _ov(d);};' .
222 '}' .
223 'if(typeof darkify_run_engine_on_iframe==="function"){' .
224 'var _oe=darkify_run_engine_on_iframe;' .
225 'darkify_run_engine_on_iframe=function(d){if(_dkfAdmin())return _oe(d);};' .
226 '}' .
227 'if(typeof darkify_inject_block_editor_css_into_iframe==="function"){' .
228 'var _ob=darkify_inject_block_editor_css_into_iframe;' .
229 'darkify_inject_block_editor_css_into_iframe=function(d){if(_dkfAdmin())return _ob(d);};' .
230 '}' .
231 '})();',
232 'after'
233 );
234 }
235
236 // Enqueue the global floating switcher style from settings
237 $enable_dark_mode_switch = isset($options['enable_dark_mode_switch']) ? $options['enable_dark_mode_switch'] : true;
238 if ($enable_dark_mode_switch) {
239 $global_variant = isset($options['enable_dark_switcher']) ? $options['enable_dark_switcher'] : 'classic';
240 wp_enqueue_style('theme-' . $global_variant);
241
242 // Separate mobile floating switcher (Different Switch in Mobile) — its toggler
243 // style may differ from the default, so enqueue that variant's CSS as well.
244 if (!empty($options['dark_switcher_switch_in_mobile'])) {
245 $mobile_variant = isset($options['enable_dark_switcher_in_mobile']) ? $options['enable_dark_switcher_in_mobile'] : 'classic';
246 wp_enqueue_style('theme-' . $this->darkify_switch_to_variant($mobile_variant));
247 }
248 }
249
250 // Detect [darkify] shortcode usage in current post and enqueue its variant
251 $post = get_post();
252 if ($post && !empty($post->post_content)) {
253 if (has_shortcode($post->post_content, 'darkify')) {
254 preg_match_all('/\[darkify\b[^\]]*\bswitch=["\']([^"\']+)["\'][^\]]*\]/i', $post->post_content, $matches);
255 $used_switches = !empty($matches[1]) ? $matches[1] : [];
256 foreach ($used_switches as $switch_val) {
257 wp_enqueue_style('theme-' . $this->darkify_switch_to_variant($switch_val));
258 }
259 // Shortcode without switch= attribute defaults to classic
260 preg_match_all('/\[darkify\b/i', $post->post_content, $all_sc);
261 if (count($all_sc[0]) > count($used_switches)) {
262 wp_enqueue_style('theme-classic');
263 }
264 }
265
266 // Detect darkify/switcher Gutenberg block usage and enqueue its variant
267 if (function_exists('has_block') && has_block('darkify/switcher', $post)) {
268 foreach ($this->darkify_get_block_variants($post->post_content) as $variant) {
269 wp_enqueue_style('theme-' . $variant);
270 }
271 }
272 }
273
274 // Detect switcher used in nav menus and enqueue its variant
275 $show_in_menu = isset($options['show_in_menu']) ? $options['show_in_menu'] : [];
276 $enable_in_menu = isset($show_in_menu['enable_switch_in_menu']) ? $show_in_menu['enable_switch_in_menu'] : false;
277 if ($enable_in_menu) {
278 $select_menus = isset($show_in_menu['select_menus']) ? $show_in_menu['select_menus'] : [];
279 foreach ($select_menus as $select_menu) {
280 $shortcode = $select_menu['darkify_menu_shortcode_helper'] ?? '';
281 if ($shortcode) {
282 preg_match('/\bswitch=["\']([^"\']+)["\']/', $shortcode, $m);
283 $switch_val = $m[1] ?? '1';
284 wp_enqueue_style('theme-' . $this->darkify_switch_to_variant($switch_val));
285 }
286 }
287 }
288 }
289 }
290
291 private function darkify_switch_to_variant($switch_value)
292 {
293 $map = [
294 '1' => 'classic', '2' => 'expand', '3' => 'inner-moon',
295 '4' => 'within', '5' => 'orbit',
296 ];
297 if (is_numeric($switch_value)) {
298 return $map[(string) $switch_value] ?? 'classic';
299 }
300 return $switch_value ?: 'classic';
301 }
302
303 private function darkify_get_block_variants($content)
304 {
305 $variants = [];
306 $this->darkify_collect_block_variants(parse_blocks($content), $variants);
307 return array_unique($variants);
308 }
309
310 private function darkify_collect_block_variants($blocks, &$variants)
311 {
312 foreach ($blocks as $block) {
313 if ('darkify/switcher' === $block['blockName']) {
314 $variants[] = $block['attrs']['style'] ?? 'classic';
315 }
316 if (!empty($block['innerBlocks'])) {
317 $this->darkify_collect_block_variants($block['innerBlocks'], $variants);
318 }
319 }
320
321 $options = get_option('darkify');
322 $min = (apply_filters('darkify_dev_mode', false) || WP_DEBUG) ? '' : '.min';
323
324 // Always register all variant styles so page builders (Elementor, etc.) can reference them.
325 wp_register_style('darkify-client-main', DARKIFY_DIR_URL . 'src/assets/css/client_main' . $min . '.css', array(), DARKIFY_VERSION);
326 wp_register_style('theme-classic', DARKIFY_DIR_URL . 'src/assets/css/switcher/classic' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
327 wp_register_style('theme-expand', DARKIFY_DIR_URL . 'src/assets/css/switcher/expand' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
328 wp_register_style('theme-inner-moon', DARKIFY_DIR_URL . 'src/assets/css/switcher/inner-moon' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
329 wp_register_style('theme-orbit', DARKIFY_DIR_URL . 'src/assets/css/switcher/orbit' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
330 wp_register_style('theme-within', DARKIFY_DIR_URL . 'src/assets/css/switcher/within' . $min . '.css', array('darkify-client-main'), DARKIFY_VERSION, 'all');
331
332 // Determine which variants are actually needed on this page.
333 $variants = $this->darkify_collect_page_variants($options);
334
335 if (empty($variants)) {
336 return;
337 }
338
339 // Enqueue main CSS + only the required variant CSS — all resolved before wp_head fires.
340 wp_enqueue_style('darkify-client-main');
341 foreach ($variants as $variant) {
342 wp_enqueue_style("theme-{$variant}");
343 }
344 wp_enqueue_script('darkify-client-main', DARKIFY_DIR_URL . 'src/assets/js/client_main' . $min . '.js', array('jquery'), DARKIFY_VERSION);
345 }
346
347 /**
348 * Collects all switcher variants needed on the current page so they can be
349 * enqueued during wp_enqueue_scripts (before wp_head), guaranteeing CSS loads
350 * in the <head> regardless of page builder or theme.
351 */
352 private function darkify_collect_page_variants($options)
353 {
354 $all_variants = array('classic', 'expand', 'inner-moon', 'orbit', 'within');
355 $num_map = array('1' => 'classic', '2' => 'expand', '3' => 'inner-moon', '4' => 'within', '5' => 'orbit');
356 $variants = array();
357
358 // 1. Global / floating switcher variant (always present when dark mode is active).
359 $global_variant = isset($options['enable_dark_switcher']) ? $options['enable_dark_switcher'] : 'classic';
360 if (in_array($global_variant, $all_variants, true)) {
361 $variants[] = $global_variant;
362 }
363
364 // 2. Menu switcher — parse variant from each saved shortcode helper string.
365 $show_in_menu = isset($options['show_in_menu']) ? $options['show_in_menu'] : array();
366 if (!empty($show_in_menu['enable_switch_in_menu']) && !empty($show_in_menu['select_menus'])) {
367 foreach ($show_in_menu['select_menus'] as $menu_item) {
368 $helper = isset($menu_item['darkify_menu_shortcode_helper']) ? $menu_item['darkify_menu_shortcode_helper'] : '';
369 if ($helper && preg_match('/switch=["\']?([^"\'>\s\]]+)["\']?/', $helper, $m)) {
370 $v = isset($num_map[$m[1]]) ? $num_map[$m[1]] : $m[1];
371 if (in_array($v, $all_variants, true)) {
372 $variants[] = $v;
373 }
374 }
375 }
376 }
377
378 // 3. Page-level detection — shortcodes, Gutenberg blocks, Elementor widget.
379 global $post;
380 if ($post) {
381 $content = $post->post_content;
382
383 // Shortcodes: parse switch attribute from every [darkify] tag.
384 if (has_shortcode($content, 'darkify')) {
385 preg_match_all('/\[darkify[^\]]*switch=["\']?([^"\'>\s\]]+)/', $content, $sc_matches);
386 foreach ($sc_matches[1] as $switch_val) {
387 $v = isset($num_map[$switch_val]) ? $num_map[$switch_val] : $switch_val;
388 if (in_array($v, $all_variants, true)) {
389 $variants[] = $v;
390 } else {
391 $variants[] = 'classic'; // default when no valid variant found
392 }
393 }
394 // If [darkify] exists but has no switch attribute, default to classic.
395 if (empty($sc_matches[1]) && has_shortcode($content, 'darkify')) {
396 $variants[] = 'classic';
397 }
398 }
399
400 // Gutenberg blocks: walk the block tree and collect style attributes.
401 if (function_exists('has_block') && has_block('darkify/switcher', $post)) {
402 $blocks = parse_blocks($content);
403 $variants = array_merge($variants, $this->darkify_extract_block_variants($blocks, $all_variants));
404 }
405
406 // Elementor widget: if Darkify widget appears in the page's Elementor data,
407 // load all variants — the specific style is stored as a control value that
408 // would require deep JSON parsing to extract reliably.
409 if (class_exists('\Elementor\Plugin')) {
410 $elementor_data = get_post_meta($post->ID, '_elementor_data', true);
411 if ($elementor_data && strpos($elementor_data, '"widgetType":"darkify"') !== false) {
412 return $all_variants;
413 }
414 }
415 }
416
417 return array_values(array_unique(array_filter($variants)));
418 }
419
420 /**
421 * Recursively walks a parsed block tree and collects darkify/darkify style values.
422 */
423 private function darkify_extract_block_variants($blocks, $all_variants)
424 {
425 $variants = array();
426 foreach ($blocks as $block) {
427 if ('darkify/switcher' === $block['blockName']) {
428 $style = isset($block['attrs']['style']) ? $block['attrs']['style'] : 'classic';
429 if (in_array($style, $all_variants, true)) {
430 $variants[] = $style;
431 }
432 }
433 if (!empty($block['innerBlocks'])) {
434 $variants = array_merge($variants, $this->darkify_extract_block_variants($block['innerBlocks'], $all_variants));
435 }
436 }
437 return $variants;
438 }
439 public function darkify_is_dark_mode_allowed()
440 {
441 $options = get_option('darkify');
442 /* Disable if Oxygen Builder is Opened */
443 if (isset($_GET['ct_builder'])) {
444 if ($_GET['ct_builder'] == "true") {
445 if (!isset($_GET['oxygen_iframe'])) {
446 return False;
447 }
448 }
449 }
450
451 return True;
452 }
453
454
455 public function darkify_client_header_script()
456 {
457 if ($this->darkify_is_dark_mode_allowed()) {
458 include_once DarkifyUtils::darkify_locate_template('header_script.php');
459 }
460 }
461
462 public function darkify_client_footer_script()
463 {
464 if ($this->darkify_is_dark_mode_allowed()) {
465 include_once DarkifyUtils::darkify_locate_template('footer_script.php');
466 }
467 }
468
469 function darkify_switch_in_menu($items, $args)
470 {
471 $options = get_option('darkify');
472 $show_in_menu = $options['show_in_menu'];
473 $select_menus = isset($show_in_menu['select_menus']) ? $show_in_menu['select_menus'] : [];
474
475 $last_shortcode = '';
476
477 if ($this->darkify_is_dark_mode_allowed() && isset($args->menu->term_id)) {
478 foreach ($select_menus as $select_menu) {
479
480 $switch_in_menu_location = $select_menu['switch_in_menu_location'] ?? '';
481 $darkify_menu_shortcode_helper = $select_menu['darkify_menu_shortcode_helper'] ?? '';
482 $menu_position = isset($select_menu['select_menu_position']) ? $select_menu['select_menu_position'] : 'after';
483 if ($args->menu->term_id == $switch_in_menu_location) {
484 $last_shortcode = $darkify_menu_shortcode_helper;
485 }
486 }
487 if ($last_shortcode) {
488 $items = ($menu_position == 'before') ? '<li class="menu-item">' . do_shortcode($last_shortcode) . '</li>' . $items : $items . '<li class="menu-item">' . do_shortcode($last_shortcode) . '</li>';
489 }
490 }
491
492 return $items;
493 }
494 }
495