PluginProbe
RabbitLoader / 4.0.2
RabbitLoader v4.0.2
4.0.2 4.0.1 4.0 3.2.0 3.1.1 3.1.0 3.0.5 3.0.3 3.0.4 2.17.1 2.17.2 2.17.3 2.17.4 2.17.5 2.17.6 2.17.7 2.18.0 2.18.1 2.18.2 2.18.3 2.18.4 2.18.5 2.18.6 2.18.7 2.18.8 All 129 releases
rabbit-loader / inc / class-rl-admin.php

class-rl-admin.php in RabbitLoader 4.0.2, at inc/class-rl-admin.php

428 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 final class RL5_Admin
8 {
9
10
11
12 /**
13 * Sidebar menu icon: the bunny SVG as a data URI (crisp at any DPI).
14 * Uses the brand-blue file directly — simple and always visible.
15 */
16 private static function menu_icon()
17 {
18 // White 20x20 bunny — correct for the dark admin menu background.
19 $file = RL5_DIR . 'assets/menu-icon.png';
20 if (is_readable($file)) {
21 return RL5_URL . 'assets/menu-icon.png';
22 }
23 return 'dashicons-performance';
24 }
25
26 private const TABS = [
27 'dashboard' => 'Dashboard',
28 'optimization' => 'Optimization',
29 'usage' => 'Usage',
30 'settings' => 'Settings',
31 'help' => 'Help',
32 ];
33
34 public static function init()
35 {
36 add_action('admin_menu', [__CLASS__, 'menu']);
37 add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue']);
38
39 add_action('wp_ajax_rabbitloader_connect_proof', [__CLASS__, 'ajax_connect_proof']);
40 add_action('wp_ajax_rl_connect_proof', [__CLASS__, 'ajax_connect_proof']);
41
42 add_action('wp_ajax_rabbitloader_save_keys', [__CLASS__, 'ajax_save_keys']);
43 add_action('wp_ajax_rl_save_keys', [__CLASS__, 'ajax_save_keys']);
44
45 add_action('wp_ajax_rabbitloader_disconnect', [__CLASS__, 'ajax_disconnect']);
46 add_action('wp_ajax_rl_disconnect', [__CLASS__, 'ajax_disconnect']);
47
48 add_action('wp_ajax_rabbitloader_purge', [__CLASS__, 'ajax_purge']);
49 add_action('wp_ajax_rl_purge', [__CLASS__, 'ajax_purge']);
50 }
51
52 public static function menu()
53 {
54 add_menu_page(
55 'RabbitLoader',
56 'RabbitLoader',
57 'manage_options',
58 'rabbitloader',
59 [__CLASS__, 'render'],
60 self::menu_icon(),
61 20
62 );
63 }
64
65 public static function enqueue($hook)
66 {
67 if ($hook !== 'toplevel_page_rabbitloader') {
68 return;
69 }
70
71 wp_enqueue_style(
72 'rl6-manrope',
73 'https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap',
74 [],
75 null
76 );
77
78 wp_enqueue_style(
79 'rl5-admin',
80 RL5_URL . 'admin/css/admin.css',
81 ['rl6-manrope'],
82 RL5_VERSION
83 );
84
85 wp_enqueue_script(
86 'rl5-connect',
87 RL5_URL . 'admin/js/connect.js',
88 [],
89 RL5_VERSION,
90 true
91 );
92
93 wp_enqueue_script(
94 'rl5-connect-proof',
95 RL5_URL . 'admin/js/connect-proof.js',
96 ['rl5-connect'],
97 RL5_VERSION,
98 true
99 );
100
101 wp_enqueue_script(
102 'rl5-admin',
103 RL5_URL . 'admin/js/admin.js',
104 ['rl5-connect-proof'],
105 RL5_VERSION,
106 true
107 );
108
109 $allowed_origins = ['https://dash.rabbitloader.com'];
110
111 if (defined('WP_DEBUG') && WP_DEBUG) {
112 $allowed_origins[] = 'https://dash.rabbitloader.local';
113 $allowed_origins[] = 'http://localhost:3000';
114 }
115
116 wp_localize_script('rl5-connect', 'RL5Config', [
117 'ajaxUrl' => admin_url('admin-ajax.php'),
118 'nonce' => wp_create_nonce('rl-ajax-nonce'),
119 'homeUrl' => home_url(),
120 'dashboardUrl' => admin_url('admin.php?page=rabbitloader'),
121 'connectBaseUrl' => 'https://dash.rabbitloader.com/connect',
122 'allowedOrigins' => $allowed_origins,
123 'connected' => RL5_Settings::is_connected(),
124 'debug' => defined('WP_DEBUG') && WP_DEBUG,
125 ]);
126
127 // admin.js (dashboard actions like Purge) uses its OWN config object.
128 // MUST NOT reuse 'RL5Config' — that would overwrite connect's allowedOrigins
129 // and silently break the connect handshake.
130 wp_localize_script('rl5-admin', 'RL5AdminConfig', [
131 'ajaxUrl' => admin_url('admin-ajax.php'),
132 'nonce' => wp_create_nonce('rl-ajax-nonce'),
133 'homeUrl' => home_url(),
134 ]);
135 }
136
137 public static function render()
138 {
139 if (!current_user_can('manage_options')) {
140 return;
141 }
142
143 if (!RL5_Settings::is_connected()) {
144 include RL5_DIR . 'admin/views/connect.php';
145 return;
146 }
147
148 $tab = isset($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'dashboard';
149 if (!isset(self::TABS[$tab])) {
150 $tab = 'dashboard';
151 }
152
153 $tabs = self::TABS;
154 $domain = RL5_Settings::domain();
155 $did = RL5_Settings::did();
156
157 // Keep using the already-working overview data for screens that need account/performance context.
158 $dashboard = in_array($tab, ['dashboard', 'optimization', 'usage'], true)
159 ? RL5_Entitlement::dashboard_data()
160 : [];
161
162 $view_file = RL5_DIR . 'admin/views/' . $tab . '.php';
163
164 include RL5_DIR . 'admin/views/partials/header.php';
165
166 if (is_readable($view_file)) {
167 include $view_file;
168 }
169
170 include RL5_DIR . 'admin/views/partials/footer.php';
171 }
172
173 public static function ajax_connect_proof()
174 {
175 self::require_admin_nonce();
176
177 $challenge_id = self::post_text(['challenge_id', 'challengeId']);
178 $challenge_nonce = self::post_text(['challenge_nonce', 'challengeNonce']);
179 $site_url = self::post_url(['site_url', 'siteUrl']);
180
181 if ($challenge_id === '' || $challenge_nonce === '' || $site_url === '') {
182 self::json_error('Missing reconnect challenge data.', 400, [
183 'received' => [
184 'challenge_id' => $challenge_id !== '',
185 'challenge_nonce' => $challenge_nonce !== '',
186 'site_url' => $site_url !== '',
187 ],
188 ]);
189 }
190
191 $site_host = self::normalize_host($site_url);
192 $home_host = self::normalize_host(home_url());
193
194 if ($site_host === '' || $home_host === '' || $site_host !== $home_host) {
195 self::json_error('Reconnect challenge site mismatch.', 400, [
196 'site_host' => $site_host,
197 'home_host' => $home_host,
198 ]);
199 }
200
201 $api = new RL5_API();
202 $response = $api->redeem_connect_proof($challenge_id, $challenge_nonce, home_url());
203
204 if (is_wp_error($response)) {
205 $error_data = $response->get_error_data();
206 $status = is_array($error_data) && !empty($error_data['status']) ? (int) $error_data['status'] : 502;
207
208 self::json_error(
209 $response->get_error_message(),
210 ($status >= 400 && $status <= 599) ? $status : 502,
211 ['backend_status' => $status]
212 );
213 }
214
215 if (empty($response['result']) || empty($response['redeem_token'])) {
216 self::json_error('Reconnect challenge was not accepted.', 400);
217 }
218
219 wp_send_json([
220 'result' => true,
221 'challenge_id' => $challenge_id,
222 'site_url' => home_url(),
223 'redeem_token' => sanitize_text_field((string) $response['redeem_token']),
224 'expires_at' => !empty($response['expires_at']) ? (int) $response['expires_at'] : 0,
225 ], 200);
226 }
227
228 public static function ajax_save_keys()
229 {
230 self::merge_json_body_into_post();
231 self::require_admin_nonce();
232
233 $encoded = self::post_text(['rl-token', 'token']);
234
235 if ($encoded === '') {
236 self::json_error('RabbitLoader connection token is missing.', 400);
237 }
238
239 $decoded = self::decode_connection_token($encoded);
240
241 if (!is_array($decoded)) {
242 self::json_error('RabbitLoader connection token could not be decoded.', 400);
243 }
244
245 $api_token = !empty($decoded['api_token']) ? (string) $decoded['api_token'] : '';
246 $did = !empty($decoded['did']) ? (string) $decoded['did'] : (!empty($decoded['domain_id']) ? (string) $decoded['domain_id'] : '');
247 $cdn_prefix = !empty($decoded['cdn_prefix']) ? (string) $decoded['cdn_prefix'] : '';
248
249 $host = wp_parse_url(home_url(), PHP_URL_HOST);
250 $host = is_string($host) ? strtolower($host) : '';
251
252 $saved = RL5_Settings::save_connection($api_token, $host, $did, $cdn_prefix);
253
254 if (is_wp_error($saved)) {
255 self::json_error($saved->get_error_message(), 400, [
256 'token_fields' => array_values(array_map('sanitize_key', array_keys($decoded))),
257 ]);
258 }
259
260 delete_transient('rl5_dashboard_' . md5($did));
261 delete_transient('rabbitloader_trans_overview_data');
262
263 do_action('rl_site_connected');
264
265 wp_send_json([
266 'result' => true,
267 'is_connected' => true,
268 'domain' => $host,
269 'did' => $did,
270 'redirect_url' => admin_url('admin.php?page=rabbitloader'),
271 ], 200);
272 }
273
274 public static function ajax_disconnect()
275 {
276 self::require_admin_nonce();
277
278 do_action('rl_site_disconnecting');
279 RL5_Settings::clear_connection();
280
281 wp_send_json([
282 'result' => true,
283 'is_connected' => false,
284 'redirect_url' => admin_url('admin.php?page=rabbitloader'),
285 ], 200);
286 }
287
288 /**
289 * Purge all pages: tells the backend to clear Cloudflare cache + rebuild.
290 * Plugin just fires the request; backend does the work.
291 */
292 public static function ajax_purge()
293 {
294 self::require_admin_nonce();
295
296 $api = new RL5_API();
297 $response = $api->purge();
298
299 if (is_wp_error($response)) {
300 $error_data = $response->get_error_data();
301 $status = is_array($error_data) && !empty($error_data['status']) ? (int) $error_data['status'] : 502;
302 self::json_error(
303 $response->get_error_message(),
304 ($status >= 400 && $status <= 599) ? $status : 502
305 );
306 }
307
308 // Backend accepted the purge. Clear local overview cache so the
309 // dashboard reflects the fresh state on next load.
310 delete_transient('rl5_dashboard_' . md5(RL5_Settings::did()));
311 delete_transient('rabbitloader_trans_overview_data');
312
313 wp_send_json([
314 'result' => true,
315 'message' => 'Purge started. Your pages will be cleared and rebuilt shortly.',
316 ], 200);
317 }
318
319 private static function require_admin_nonce()
320 {
321 if (!current_user_can('manage_options')) {
322 self::json_error('You are not allowed to manage RabbitLoader.', 403);
323 }
324
325 $nonce = self::post_text(['rl_nonce', '_ajax_nonce']);
326
327 if ($nonce === '' || !wp_verify_nonce($nonce, 'rl-ajax-nonce')) {
328 self::json_error('RabbitLoader security nonce is invalid or expired.', 403);
329 }
330 }
331
332 private static function merge_json_body_into_post()
333 {
334 $raw = file_get_contents('php://input');
335 if (!$raw) {
336 return;
337 }
338
339 $json = json_decode($raw, true);
340 if (!is_array($json)) {
341 return;
342 }
343
344 foreach ($json as $key => $value) {
345 if (!array_key_exists($key, $_POST)) {
346 $_POST[$key] = $value;
347 }
348 }
349 }
350
351 private static function post_text($keys)
352 {
353 foreach ($keys as $key) {
354 if (isset($_POST[$key]) && !is_array($_POST[$key])) {
355 return sanitize_text_field(wp_unslash($_POST[$key]));
356 }
357 }
358
359 return '';
360 }
361
362 private static function post_url($keys)
363 {
364 foreach ($keys as $key) {
365 if (isset($_POST[$key]) && !is_array($_POST[$key])) {
366 return esc_url_raw(wp_unslash($_POST[$key]));
367 }
368 }
369
370 return '';
371 }
372
373 private static function normalize_host($url)
374 {
375 $host = wp_parse_url($url, PHP_URL_HOST);
376
377 if (!$host && is_string($url) && strpos($url, '://') === false) {
378 $host = wp_parse_url('https://' . ltrim($url, '/'), PHP_URL_HOST);
379 }
380
381 if (!$host) {
382 return '';
383 }
384
385 $host = strtolower(trim((string) $host));
386 $host = preg_replace('/^www\./i', '', $host);
387 return rtrim($host, '.');
388 }
389
390 private static function decode_connection_token($encoded)
391 {
392 $encoded = trim((string) $encoded);
393
394 $decoded = base64_decode($encoded, true);
395
396 if ($decoded === false) {
397 $normalized = strtr($encoded, '-_', '+/');
398 $padding = strlen($normalized) % 4;
399 if ($padding) {
400 $normalized .= str_repeat('=', 4 - $padding);
401 }
402 $decoded = base64_decode($normalized, true);
403 }
404
405 if ($decoded === false || $decoded === '') {
406 return null;
407 }
408
409 $payload = json_decode($decoded, true);
410 return is_array($payload) ? $payload : null;
411 }
412
413 private static function json_error($message, $status = 400, $extra = [])
414 {
415 $body = [
416 'result' => false,
417 'message' => sanitize_text_field((string) $message),
418 ];
419
420 if (!empty($extra) && is_array($extra)) {
421 $body['debug'] = $extra;
422 }
423
424 wp_send_json($body, $status);
425 exit;
426 }
427 }
428