1<?php
2/**
3 * Copyright © 2019-2026 Rhubarb Tech Inc. All Rights Reserved.
4 *
5 * The Object Cache Pro Software and its related materials are property and confidential
6 * information of Rhubarb Tech Inc. Any reproduction, use, distribution, or exploitation
7 * of the Object Cache Pro Software and its related materials, in whole or in part,
8 * is strictly forbidden unless prior permission is obtained from Rhubarb Tech Inc.
9 *
10 * In addition, any reproduction, use, distribution, or exploitation of the Object Cache Pro
11 * Software and its related materials, in whole or in part, is subject to the End-User License
12 * Agreement accessible in the included `LICENSE` file, or at: https://objectcache.pro/eula
13 */
14
15declare(strict_types=1);
16
17namespace RedisCachePro\Plugin;
18
19use WP_Screen;
20
21use RedisCachePro\Plugin;
22
23use RedisCachePro\Metrics\RedisMetrics;
24use RedisCachePro\Metrics\RelayMetrics;
25use RedisCachePro\Metrics\WordPressMetrics;
26
27/**
28 * @mixin \RedisCachePro\Plugin
29 */
30trait Widget
31{
32 /**
33 * Whitelist of widget actions.
34 *
35 * @var array<string>
36 */
37 protected $widgetActions = [
38 'flush-cache',
39 'flush-site-cache',
40 'flush-network-cache',
41 'flush-relay',
42 'reset-relay-ratios',
43 'enable-dropin',
44 'update-dropin',
45 'disable-dropin',
46 ];
47
48 /**
49 * Whitelist of widget action statuses.
50 *
51 * @var array<string>
52 */
53 protected $widgetActionStatuses = [
54 'cache-flushed',
55 'cache-not-flushed',
56 'site-cache-flushed',
57 'site-cache-not-flushed',
58 'network-cache-flushed',
59 'network-cache-not-flushed',
60 'relay-flushed',
61 'relay-not-flushed',
62 'relay-ratios-reset',
63 'relay-ratios-not-reset',
64 'dropin-enabled',
65 'dropin-not-enabled',
66 'dropin-updated',
67 'dropin-not-updated',
68 'dropin-disabled',
69 'dropin-not-disabled',
70 ];
71
72 /**
73 * Boot widget component.
74 *
75 * @return void
76 */
77 public function bootWidget()
78 {
79 add_action('current_screen', [$this, 'registerWidget']);
80 }
81
82 /**
83 * Register the dashboard widgets.
84 *
85 * @param \WP_Screen $screen
86 * @return void
87 */
88 public function registerWidget(WP_Screen $screen)
89 {
90 if (! in_array($screen->id, ['dashboard', 'dashboard-network', $this->screenId])) {
91 return;
92 }
93
94 if (! current_user_can(Plugin::Capability)) {
95 return;
96 }
97
98 $pageHook = str_replace('-network', '', $this->screenId);
99
100 add_action('load-index.php', [$this, 'handleWidgetActions']);
101 add_action("load-{$pageHook}", [$this, 'handleWidgetActions']);
102
103 add_action('admin_notices', [$this, 'displayWidgetNotice'], 0);
104 add_action('network_admin_notices', [$this, 'displayWidgetNotice'], 0);
105
106 add_action('admin_enqueue_scripts', [$this, 'addWidgetStyles']);
107
108 if ($screen->id !== $this->screenId) {
109 $this->enqueueWidgetScripts();
110 }
111
112 /**
113 * Filters whether to add the dashboard widget.
114 *
115 * @param bool $add_widget Whether to add the dashboard widget. Default `true`.
116 */
117 if ((bool) apply_filters('objectcache_dashboard_widget', true)) {
118 add_action('wp_dashboard_setup', function () {
119 wp_add_dashboard_widget('dashboard_objectcache', 'Object Cache Pro', [$this, 'renderWidget'], null, null, 'normal', 'high');
120 });
121 }
122
123 /**
124 * Filters whether to add the network dashboard widget.
125 *
126 * @param bool $add_widget Whether to add the network dashboard widget. Default `true`.
127 */
128 if ((bool) apply_filters('objectcache_network_dashboard_widget', true)) {
129 add_action('wp_network_dashboard_setup', function () {
130 wp_add_dashboard_widget('dashboard_objectcache', 'Object Cache Pro', [$this, 'renderWidget'], null, null, 'normal', 'high');
131 });
132 }
133 }
134
135 /**
136 * Render the dashboard widget.
137 *
138 * @return void
139 */
140 public function renderWidget()
141 {
142 global $wp_object_cache_errors;
143
144 require __DIR__ . '/templates/widgets/overview.phtml';
145 }
146
147 /**
148 * Handle widget actions and redirect back to dashboard.
149 *
150 * @return void
151 */
152 public function handleWidgetActions()
153 {
154 global $wp_object_cache;
155
156 $screenId = get_current_screen()->id ?? null;
157 $actionParameter = $screenId === $this->screenId ? 'action' : 'objectcache-action';
158
159 $nonce = $_GET['_wpnonce'] ?? false;
160 $action = $_GET[$actionParameter] ?? false;
161
162 if (! $action || ! $nonce) {
163 return;
164 }
165
166 if (! in_array($action, $this->widgetActions)) {
167 wp_die('Invalid action.', 400);
168 }
169
170 if (! \wp_verify_nonce($nonce, $action)) {
171 wp_die("Invalid nonce for {$action} action.", 400);
172 }
173
174 if (is_multisite() && ! is_network_admin() && ! in_array($action, ['flush-cache', 'flush-site-cache'])) {
175 wp_die("Sorry, you are not allowed to perform the {$action} action.", 403);
176 }
177
178 $status = null;
179
180 switch ($action) {
181 case 'flush-cache':
182 $this->logFlush();
183 $status = $wp_object_cache->flush() ? 'cache-flushed' : 'cache-not-flushed';
184 break;
185 case 'flush-site-cache':
186 $status = $wp_object_cache->flushBlog() ? 'site-cache-flushed' : 'site-cache-not-flushed';
187 break;
188 case 'flush-network-cache':
189 $this->logFlush();
190 $status = $wp_object_cache->flush() ? 'network-cache-flushed' : 'network-cache-not-flushed';
191 break;
192 case 'flush-relay':
193 $status = $this->flushRelay() ? 'relay-flushed' : 'relay-not-flushed';
194 break;
195 case 'reset-relay-ratios':
196 $status = $this->resetRelayRatios() ? 'relay-ratios-reset' : 'relay-ratios-not-reset';
197 break;
198 case 'enable-dropin':
199 $status = $this->enableDropin() ? 'dropin-enabled' : 'dropin-not-enabled';
200 break;
201 case 'update-dropin':
202 $status = $this->updateDropin() ? 'dropin-updated' : 'dropin-not-updated';
203 break;
204 case 'disable-dropin':
205 $status = $this->disableDropin() ? 'dropin-disabled' : 'dropin-not-disabled';
206 break;
207 }
208
209 if ($screenId === $this->screenId) {
210 $url = add_query_arg('status', $status, $this->baseurl);
211 } else {
212 $url = add_query_arg('objectcache-status', $status, is_network_admin() ? network_admin_url() : admin_url());
213 }
214
215 wp_safe_redirect($url, 302, 'Object Cache Pro');
216 exit;
217 }
218
219 /**
220 * Print the widget styles inlines to support non-standard installs.
221 *
222 * @return void
223 */
224 public function addWidgetStyles()
225 {
226 wp_add_inline_style('dashboard', $this->inlineAsset('css/widget.css'));
227 }
228
229 /**
230 * Enqueue the widget scripts.
231 *
232 * @return void
233 */
234 protected function enqueueWidgetScripts()
235 {
236 if (! $this->analyticsEnabled()) {
237 return;
238 }
239
240 \wp_register_script('objectcache', false);
241 \wp_enqueue_script('objectcache');
242
243 \wp_localize_script('objectcache', 'objectcache', [
244 'rest' => [
245 'nonce' => \wp_create_nonce('wp_rest'),
246 'url' => \rest_url(),
247 ],
248 'gmt_offset' => \get_option('gmt_offset'),
249 'refresh' => 30,
250 'interval' => 60,
251 'series' => [
252 ['field' => 'median', 'name' => 'Median'],
253 ],
254 'comboCharts' => array_map(static function ($metric) {
255 return [
256 'containers' => array_keys($metric['type']),
257 'labels' => $metric['labels'],
258 ];
259 }, $this->comboMetrics()),
260 ]);
261
262 $this->enqueueAnalyticsAssets();
263 }
264
265 /**
266 * Display status notices for widget actions.
267 *
268 * @return void
269 */
270 public function displayWidgetNotice()
271 {
272 $status = $_GET['status'] ?? $_GET['objectcache-status'] ?? false;
273
274 if (! $status || ! in_array($status, $this->widgetActionStatuses)) {
275 return;
276 }
277
278 $notice = function ($type, $text) {
279 return sprintf('<div class="notice notice-%s"><p>%s</p></div>', $type, $text);
280 };
281
282 switch ($status) {
283 case 'cache-flushed':
284 echo $notice('success', 'The object cache was flushed.');
285 break;
286 case 'cache-not-flushed':
287 echo $notice('error', 'The object cache could not be flushed.');
288 break;
289 case 'site-cache-flushed':
290 echo $notice('success', 'This site’s object cache was flushed.');
291 break;
292 case 'site-cache-not-flushed':
293 echo $notice('error', 'This site’s object cache could not be flushed.');
294 break;
295 case 'network-cache-flushed':
296 echo $notice('success', 'The network’s object cache was flushed.');
297 break;
298 case 'network-cache-not-flushed':
299 echo $notice('error', 'The network’s object cache could not be flushed.');
300 break;
301 case 'relay-flushed':
302 echo $notice('success', 'The Relay memory was flushed.');
303 break;
304 case 'relay-not-flushed':
305 echo $notice('error', 'The Relay memory could not be flushed.');
306 break;
307 case 'relay-ratios-reset':
308 echo $notice('success', 'The adaptive cache ratios have been reset.');
309 break;
310 case 'relay-ratios-not-reset':
311 echo $notice('error', 'The adaptive cache rations could not be reset.');
312 break;
313 case 'dropin-enabled':
314 echo $notice('success', 'The object cache drop-in was enabled.');
315 break;
316 case 'dropin-not-enabled':
317 echo $notice('error', 'The object cache drop-in could not be enabled.');
318 break;
319 case 'dropin-updated':
320 echo $notice('success', 'The object cache drop-in was updated.');
321 break;
322 case 'dropin-not-updated':
323 echo $notice('error', 'The object cache drop-in could not be updated.');
324 break;
325 case 'dropin-disabled':
326 echo $notice('success', 'The object cache drop-in was disabled.');
327 break;
328 case 'dropin-not-disabled':
329 echo $notice('error', 'The object cache drop-in could not be disabled.');
330 break;
331 }
332 }
333
334 /**
335 * Add the all metrics as widgets.
336 *
337 * @return array<string, mixed>
338 */
339 protected function widgetCharts()
340 {
341 $charts = [
342 'response-times',
343 'requests',
344 ];
345
346 $metrics = array_merge(
347 WordPressMetrics::schema(),
348 RedisMetrics::schema(),
349 $this->comboMetrics()
350 );
351
352 if ($this->diagnostics()->usingRelayCache()) {
353 $charts[] = 'relay-requests';
354 $metrics = array_merge($metrics, RelayMetrics::schema());
355 }
356
357 /**
358 * Filters the default order and available metrics on the dashboard widget.
359 *
360 * @param array $metrics The available metrics.
361 */
362 $charts = (array) apply_filters('objectcache_widget_metrics', $charts);
363
364 return array_combine($charts, array_map(function ($id) use ($metrics) {
365 return $metrics[$id];
366 }, $charts));
367 }
368
369 /**
370 * Updates the flush timestamp in the cache metadata.
371 *
372 * @return bool
373 */
374 protected function flushRelay()
375 {
376 global $wp_object_cache;
377
378 if (! $this->diagnostics()->usingRelay()) {
379 return false;
380 }
381
382 $wp_object_cache->metadata('relay.flushed_at', microtime(true));
383 $wp_object_cache->writeMetadata();
384
385 return true;
386 }
387
388 /**
389 * Updates the ratio reset timestamp in the cache metadata.
390 *
391 * @return bool
392 */
393 protected function resetRelayRatios()
394 {
395 global $wp_object_cache;
396
397 if (! $this->diagnostics()->usingRelay()) {
398 return false;
399 }
400
401 $wp_object_cache->metadata('relay.ratios_reset_at', microtime(true));
402 $wp_object_cache->writeMetadata();
403
404 return true;
405 }
406}
407