| @@ -6,8 +6,33 @@ | ||
| 6 | 6 | if (!defined('ABSPATH')) { |
| 7 | 7 | exit; |
| 8 | 8 | } |
| 9 | 9 | |
| 10 | +if (!function_exists('darkify_js_string')) { | |
| 11 | + /** | |
| 12 | + * Emit a value as a JavaScript string literal (quotes included). | |
| 13 | + * | |
| 14 | + * For anything the browser parses rather than displays — CSS selectors, | |
| 15 | + * above all — esc_attr() is the wrong escaper inside a <script> block: it | |
| 16 | + * HTML-encodes `>`, `&` and `"`, and script bodies are raw text, so those | |
| 17 | + * entities reach the JS engine verbatim and corrupt the value. JSON is the | |
| 18 | + * correct escaping for a JS string; the HEX flags additionally keep `<`, | |
| 19 | + * `>`, `&`, `'` and `"` out of the raw output so the literal can never | |
| 20 | + * close the surrounding <script> tag. | |
| 21 | + * | |
| 22 | + * @param mixed $value Value to emit (cast to string). | |
| 23 | + * @return string A quoted JS string literal, e.g. '"#id, #id *"'. | |
| 24 | + */ | |
| 25 | + function darkify_js_string($value) | |
| 26 | + { | |
| 27 | + $json = wp_json_encode( | |
| 28 | + (string) $value, | |
| 29 | + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | |
| 30 | + ); | |
| 31 | + return false === $json ? '""' : $json; | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 10 | 35 | ob_start(); |
| 11 | 36 | $options = get_option('darkify'); |
| 12 | 37 | // Controls |
| 13 | 38 | $enable_default_dark_mode = isset($options['enable_default_dark_mode']) ? $options['enable_default_dark_mode'] : ''; |
| @@ -122,9 +147,54 @@ | ||
| 122 | 147 | } else { |
| 123 | 148 | $video_replacements = "[]"; |
| 124 | 149 | } |
| 125 | 150 | if (!is_admin()) { ?> |
| 126 | - <script type="text/javascript" class="darkify_inline_js">(function(){var s=localStorage.darkify_last_state,t=localStorage.darkify_selected_theme||'set1';if(s==='1'){var h=document.documentElement;h.classList.add('darkify_dark_mode_enabled');h.classList.add('darkify-'+t);h.classList.add('darkify_prepaint');setTimeout(function(){h.classList.remove('darkify_prepaint')},800);}}());</script> | |
| 151 | + <?php | |
| 152 | + /* | |
| 153 | + * Release the pre-paint guard when the engine is done, not on a fixed timer. | |
| 154 | + * | |
| 155 | + * This used to be a flat `setTimeout(..., 800)`. On a warm reload that is | |
| 156 | + * invisible — every asset is in cache, the engine's first walk lands early, | |
| 157 | + * and it drops both classes itself long before the timer. On a HARD reload | |
| 158 | + * it is the flicker: nothing is served from cache, so the engine script and | |
| 159 | + * the stylesheets it reads are re-fetched and the first walk lands well | |
| 160 | + * after 800ms. The timer fired first, un-hid `body` while the page still | |
| 161 | + * carried the theme's LIGHT colours, and the walk then flipped the whole | |
| 162 | + * page to dark a few hundred ms later. That flip is what the visitor sees | |
| 163 | + * flickering. | |
| 164 | + * | |
| 165 | + * The timeout's real job is the failsafe: if the engine never runs (script | |
| 166 | + * 404s, blocked by an optimiser, JS disabled mid-flight) the page must not | |
| 167 | + * stay hidden. So the guard now comes off on a condition rather than a | |
| 168 | + * duration: | |
| 169 | + * | |
| 170 | + * - engine still to paint -> keep waiting; it removes the classes itself | |
| 171 | + * the moment its first walk has real colours on the page. | |
| 172 | + * - parsing finished and `window.darkify_engine_loaded` still unset -> | |
| 173 | + * the engine is not on this page and never will be, so release at once. | |
| 174 | + * That is strictly quicker than the old fixed 800ms in the case the | |
| 175 | + * timer actually existed for. | |
| 176 | + * - 4000ms -> release regardless, so no failure mode can leave the page | |
| 177 | + * hidden. Comfortably above the slowest measured hard reload. | |
| 178 | + * | |
| 179 | + * Parse state is the gate, not the presence of the engine's script tag: on | |
| 180 | + * a large page the tag can sit hundreds of KB into <head> and simply not be | |
| 181 | + * parsed yet at the moment we look, which reads as "no engine" on exactly | |
| 182 | + * the slow load this is meant to protect. | |
| 183 | + * | |
| 184 | + * Only `darkify_fouc_guard` is polled. Once that is gone the engine has | |
| 185 | + * demonstrably painted, so `darkify_prepaint` can be left to it; the two are | |
| 186 | + * still dropped together if the failsafe ever has to fire, which preserves | |
| 187 | + * the original guarantee that neither class can outlive the page load. | |
| 188 | + * | |
| 189 | + * No `>`/`<`/`&` anywhere in the snippet, so the ceiling is counted in | |
| 190 | + * 100ms ticks (40 = 4s) and every test is an `===`/`!==`. wp_kses | |
| 191 | + * entity-encodes all three characters inside script text — `n>=40` reaches | |
| 192 | + * the browser as `n>=40` and dies with a SyntaxError — which is also why | |
| 193 | + * the gates are nested ifs rather than `&&`. | |
| 194 | + */ | |
| 195 | + ?> | |
| 196 | + <script type="text/javascript" class="darkify_inline_js">(function(){var s=localStorage.darkify_last_state,t=localStorage.darkify_selected_theme||'set1';if(s==='1'){var h=document.documentElement;h.classList.add('darkify_dark_mode_enabled');h.classList.add('darkify-'+t);h.classList.add('darkify_prepaint');h.classList.add('darkify_fouc_guard');var r=function(){h.classList.remove('darkify_prepaint');h.classList.remove('darkify_fouc_guard');};var n=0;var p=function(){if(!h.classList.contains('darkify_fouc_guard')){return;}n=n+1;if(n===40){r();return;}if(document.readyState!=='loading'){if(!window.darkify_engine_loaded){r();return;}}setTimeout(p,100);};setTimeout(p,100);}}());</script> | |
| 127 | 197 | <style type="text/css" class="darkify_inline_css"> |
| 128 | 198 | :root { |
| 129 | 199 | <?php |
| 130 | 200 | $dark_mode_color_palettes = [ |
| @@ -403,8 +473,31 @@ | ||
| 403 | 473 | |
| 404 | 474 | <script type="text/javascript" class="darkify_inline_js"> |
| 405 | 475 | var darkify_switch_unique_id = "<?php echo esc_attr($this->unique_id); ?>"; |
| 406 | 476 | var darkify_is_this_admin_panel = "<?php echo esc_attr(is_admin() ? "1" : "0"); ?>"; |
| 477 | + <?php | |
| 478 | + /* | |
| 479 | + * Admin Panel Dark Mode, exposed so the admin engine can refuse to darken | |
| 480 | + * while the option is off. Darkify's own SPA screens load the engine even | |
| 481 | + * then, so the admin-bar icon can be toggled without a reload, which means | |
| 482 | + * the engine has to honour the option itself rather than relying on simply | |
| 483 | + * not being enqueued. | |
| 484 | + */ | |
| 485 | + ?> | |
| 486 | + var darkify_admin_panel_dark_enabled = "<?php echo esc_attr(isset($darkify_effective_admin_dark) ? $darkify_effective_admin_dark : (!empty($options['enable_admin_panel_dark_mode']) ? '1' : '0')); ?>"; | |
| 487 | + <?php | |
| 488 | + /* | |
| 489 | + * Site-wide palette defaults for the Dark Reader admin engine, one for the | |
| 490 | + * block editor and one for everything else. "auto" means "let Dark Reader | |
| 491 | + * derive every colour", which is the default and the recommended value. | |
| 492 | + * | |
| 493 | + * These are defaults, not overrides: the editor's toolbar dropdown writes a | |
| 494 | + * per-user choice to localStorage and that still wins, which is how that | |
| 495 | + * control has always behaved. | |
| 496 | + */ | |
| 497 | + ?> | |
| 498 | + var darkify_admin_palette_default = "<?php echo esc_attr(!empty($options['admin_panel_palette']) ? $options['admin_panel_palette'] : 'auto'); ?>"; | |
| 499 | + var darkify_editor_palette_default = "<?php echo esc_attr(!empty($options['block_editor_palette']) ? $options['block_editor_palette'] : 'auto'); ?>"; | |
| 407 | 500 | var darkify_enable_default_dark_mode = "<?php echo esc_attr($enable_default_dark_mode); ?>"; |
| 408 | 501 | var darkify_enable_os_aware = "<?php echo esc_attr($enable_os_aware); ?>"; |
| 409 | 502 | var darkify_enable_keyboard_shortcut = "<?php echo esc_attr($enable_keyboard_shortcut); ?>"; |
| 410 | 503 | var darkify_keyboard_shortcut_keys = "<?php echo esc_attr($keyboard_shortcut_keys); ?>"; |
| @@ -433,14 +526,30 @@ | ||
| 433 | 526 | var darkify_video_brightness_to = "<?php echo esc_attr($low_video_brightness_label); ?>"; |
| 434 | 527 | var darkify_enable_video_grayscale = "<?php echo esc_attr($enable_video_grayscale); ?>"; |
| 435 | 528 | var darkify_video_grayscale_to = "<?php echo esc_attr($video_grayscale_label); ?>"; |
| 436 | 529 | var darkify_video_replacements = "<?PHP echo esc_attr($video_replacements); ?>"; |
| 437 | - var darkify_allowed_elements = "<?php echo esc_attr($this->utils->generateAllowedElementsStr($options)); ?>"; | |
| 438 | - var darkify_allowed_elements_raw = "<?php echo esc_attr($allowed_elements); ?>"; | |
| 530 | + <?php | |
| 531 | + /* | |
| 532 | + * These four carry CSS selectors, not display text, and they are read back | |
| 533 | + * by element.matches() in client_main.js. esc_attr() would HTML-encode the | |
| 534 | + * selector combinators (`>` becomes `>`, `&` becomes `&`), and a | |
| 535 | + * <script> body is raw text — the browser never decodes those back. One | |
| 536 | + * encoded combinator anywhere in the list makes the whole comma-joined | |
| 537 | + * selector a SyntaxError, matches() throws, and Disallowed Elements | |
| 538 | + * silently stops restricting anything. Custom CSS selectors are folded into | |
| 539 | + * the disallowed list automatically (see generateDisallowedElementsStr), so | |
| 540 | + * a perfectly ordinary `.card > .title` rule was enough to trigger it. | |
| 541 | + * darkify_js_string() emits a proper JSON string literal instead, which | |
| 542 | + * keeps the selector intact and is still safe to sit inside <script> | |
| 543 | + * (tags/ampersands are \u-escaped). | |
| 544 | + */ | |
| 545 | + ?> | |
| 546 | + var darkify_allowed_elements = <?php echo darkify_js_string($this->utils->generateAllowedElementsStr($options)); ?>; | |
| 547 | + var darkify_allowed_elements_raw = <?php echo darkify_js_string($allowed_elements); ?>; | |
| 439 | 548 | var darkify_allowed_elements_force_to_correct = "<?php echo esc_attr($allowed_elements_force_to_correct); ?>"; |
| 440 | 549 | |
| 441 | - var darkify_disallowed_elements = "<?php echo esc_attr($this->utils->generateDisallowedElementsStr($options, $this->external_support)); ?>"; | |
| 442 | - var darkify_disallowed_elements_raw = "<?php echo esc_attr($disallowed_elements); ?>"; | |
| 550 | + var darkify_disallowed_elements = <?php echo darkify_js_string($this->utils->generateDisallowedElementsStr($options, $this->external_support)); ?>; | |
| 551 | + var darkify_disallowed_elements_raw = <?php echo darkify_js_string($disallowed_elements); ?>; | |
| 443 | 552 | var darkify_disallowed_elements_force_to_correct = "<?php echo esc_attr($disallowed_elements_force_to_correct); ?>"; |
| 444 | 553 | |
| 445 | 554 | var darkify_allowed_btn_class = <?php echo json_encode($this->utils->addButtonClassByDarkify($options)); ?>; |
| 446 | 555 | |