PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
timetics / core / admin / theme-mode.php

theme-mode.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.64, at core/admin/theme-mode.php

180 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Per-user light/dark preference for the Timetics admin.
4 *
5 * @package Timetics
6 */
7 namespace Timetics\Core\Admin;
8
9 defined( 'ABSPATH' ) || exit;
10
11 use Timetics\Utils\Singleton;
12
13 /**
14 * The preference is stored against the WordPress user, not in plugin options,
15 * so one admin choosing dark does not impose it on everyone else who logs in.
16 *
17 * It is printed on <body> by PHP, before any script runs, so a dark mode user
18 * gets no white flash while React mounts. PHP cannot resolve 'system' on the
19 * server, so it prints the preference and a CSS media query settles it; once
20 * React mounts it swaps in the resolved value.
21 *
22 * Saving needs no controller of our own: register_meta() with show_in_rest
23 * makes core's POST /wp/v2/users/me accept the field, and a logged-in user may
24 * always edit their own profile.
25 */
26 class Theme_Mode {
27 use Singleton;
28
29 /**
30 * User meta key holding the preference.
31 */
32 const META_KEY = 'timetics_theme_mode';
33
34 /**
35 * The only values that may ever be stored or printed.
36 */
37 const MODES = array( 'light', 'dark', 'system' );
38
39 /**
40 * Used when nothing is stored, and when anything invalid is.
41 */
42 const DEFAULT_MODE = 'system';
43
44 /**
45 * The one admin screen Timetics owns.
46 */
47 const SCREEN = 'toplevel_page_timetics';
48
49 /**
50 * Register hooks.
51 *
52 * Base::init() runs on plugins_loaded, so hooking init here is on time.
53 *
54 * @return void
55 */
56 public function init() {
57 add_action( 'init', array( $this, 'register_meta' ) );
58 add_filter( 'admin_body_class', array( $this, 'body_class' ) );
59 }
60
61 /**
62 * Expose the preference on core's user REST endpoint.
63 *
64 * @return void
65 */
66 public function register_meta() {
67 register_meta(
68 'user',
69 self::META_KEY,
70 array(
71 'type' => 'string',
72 'single' => true,
73 'default' => self::DEFAULT_MODE,
74 'show_in_rest' => true,
75 'sanitize_callback' => array( __CLASS__, 'sanitize' ),
76 'auth_callback' => function ( $allowed, $meta_key, $object_id ) {
77 return current_user_can( 'edit_user', $object_id );
78 },
79 )
80 );
81 }
82
83 /**
84 * Force any value into one of the three modes.
85 *
86 * This value ends up inside a class attribute on <body>, so it is a trust
87 * boundary. The REST endpoint belongs to core, so the payload shape is not
88 * ours to control: arrays and nulls fall through quietly.
89 *
90 * @param mixed $value Raw value from REST, the database, or WP-CLI.
91 *
92 * @return string One of self::MODES.
93 */
94 public static function sanitize( $value ) {
95 if ( ! is_string( $value ) ) {
96 return self::DEFAULT_MODE;
97 }
98
99 return in_array( $value, self::MODES, true ) ? $value : self::DEFAULT_MODE;
100 }
101
102 /**
103 * The preference for a user.
104 *
105 * Sanitises on the way out as well as in: meta can be written by WP-CLI or
106 * other code, so reading it is just as much a trust boundary.
107 *
108 * @param int $user_id 0 for the current user.
109 *
110 * @return string One of self::MODES.
111 */
112 public static function get_for_user( $user_id = 0 ) {
113 $user_id = $user_id ? $user_id : get_current_user_id();
114
115 if ( ! $user_id ) {
116 return self::DEFAULT_MODE;
117 }
118
119 return self::sanitize( get_user_meta( $user_id, self::META_KEY, true ) );
120 }
121
122 /**
123 * Whether the feature is visible to anyone.
124 *
125 * On by default. The filter stays so a site owner who does not want dark
126 * mode can turn the whole thing off in one line:
127 *
128 * add_filter( 'timetics_enable_dark_mode', '__return_false' );
129 *
130 * @return bool
131 */
132 public static function is_enabled() {
133 return (bool) apply_filters( 'timetics_enable_dark_mode', true );
134 }
135
136 /**
137 * The mode handed to JavaScript.
138 *
139 * While the feature is off this must be 'light', never the stored value:
140 * JS resolves 'system' itself and would turn a dark OS into a dark admin.
141 *
142 * @return string One of self::MODES.
143 */
144 public static function get_for_localize() {
145 return self::is_enabled() ? self::get_for_user() : 'light';
146 }
147
148 /**
149 * Add the theme class to <body> on the Timetics screen only.
150 *
151 * @param string $classes Space separated classes from WordPress.
152 *
153 * @return string
154 */
155 public function body_class( $classes ) {
156 if ( ! self::is_enabled() || ! $this->is_timetics_screen() ) {
157 return $classes;
158 }
159
160 return $classes . ' timetics-theme-' . self::get_for_user();
161 }
162
163 /**
164 * Whether the current request is rendering the Timetics admin screen.
165 *
166 * get_current_screen() is unavailable early in the request.
167 *
168 * @return bool
169 */
170 private function is_timetics_screen() {
171 if ( ! function_exists( 'get_current_screen' ) ) {
172 return false;
173 }
174
175 $screen = get_current_screen();
176
177 return $screen && self::SCREEN === $screen->base;
178 }
179 }
180