PluginProbe
RabbitLoader / 3.1.0
RabbitLoader v3.1.0
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 / admin.php

admin.php in RabbitLoader 3.1.0, at inc/admin.php

1,182 lines 48.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class RabbitLoader_21_Admin
4 {
5
6 public static $rabbitloader_cache_warnings = false;
7 public static $admin_notice_shown = false;
8
9 const PURGE_POST_CHANGE = "PURGE_POST";
10 const PURGE_THEME_CHANGE = "PURGE_THEME";
11 const PURGE_PLUG_CHANGE = "PURGE_PLUGIN";
12 const PURGE_MANUAL_USER = "PURGE_USER";
13 const SURVEY_DIS_PERMA = 999;
14
15 public static function addActions()
16 {
17 add_action('admin_notices', 'RabbitLoader_21_Admin::admin_notices');
18 add_action('admin_init', 'RabbitLoader_21_Admin::admin_init');
19 add_action('network_admin_notices', 'RabbitLoader_21_Admin::admin_notices');
20 add_action('admin_menu', 'RabbitLoader_21_Admin::leftMenuOption');
21 add_action('admin_enqueue_scripts', function () {
22 $is_rl_page = RabbitLoader_21_Util_Core::isRLPage();
23 $dependencies = ['jquery'];
24 if ($is_rl_page) {
25 wp_enqueue_script('rabbitloader-react', 'no', [], false);
26 wp_enqueue_script('rabbitloader-react-dom', 'no', [], false);
27 $dependencies[] = 'rabbitloader-react';
28 $dependencies[] = 'rabbitloader-react-dom';
29 }
30
31 wp_enqueue_script('rabbitloader-index', RABBITLOADER_PLUG_URL . 'admin/js/index.js', $dependencies, RABBITLOADER_PLUG_VERSION);
32 if ($is_rl_page) {
33 $guard_config = wp_json_encode([
34 'adminAjax' => admin_url('admin-ajax.php'),
35 'isConnected' => self::isPluginActivated(),
36 ], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
37 if ($guard_config !== false) {
38 wp_add_inline_script('rabbitloader-index', 'window.RLConflictGuardConfig = ' . $guard_config . ';', 'before');
39 }
40
41 wp_enqueue_script('rabbitloader-conflict-guard', RABBITLOADER_PLUG_URL . 'admin/js/conflict-guard.js', ['rabbitloader-index'], RABBITLOADER_PLUG_VERSION, true);
42 $proof_config = wp_json_encode(self::getConnectProofConfig(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
43 if ($proof_config !== false) {
44 wp_add_inline_script('rabbitloader-index', 'window.RLConnectProofConfig = ' . $proof_config . ';', 'before');
45 }
46 wp_enqueue_script('rabbitloader-connect-proof', RABBITLOADER_PLUG_URL . 'admin/js/connect-proof.js', ['rabbitloader-index'], RABBITLOADER_PLUG_VERSION, true);
47 }
48 if ($is_rl_page && self::isPluginActivated()) {
49 $boot_data = wp_json_encode(self::getAdminBootData(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
50 if ($boot_data !== false) {
51 wp_add_inline_script('rabbitloader-index', 'window.rabbitloader_local_vars = ' . $boot_data . ';', 'before');
52 }
53 wp_enqueue_script(
54 'rabbitloader-admin-enhancements',
55 RABBITLOADER_PLUG_URL . 'admin/js/admin-enhancements.js',
56 [],
57 RABBITLOADER_PLUG_VERSION,
58 true
59 );
60 }
61 });
62
63 $purge_func = function () {
64 RL21UtilWP::verifyAjaxNonce();
65 $response = [
66 'result' => false,
67 'lpc' => 0 //local purge count
68 ];
69 if (!empty($_POST['post_id'])) {
70 $post_id = RabbitLoader_21_Util_Core::get_param('post_id', true);
71 RL21UtilWP::onPostChange($post_id);
72 $response['result'] = true;
73 } else {
74 RL21UtilWP::onPostChange(RL21UtilWP::POST_ID_ALL);
75 $response['result'] = true;
76 }
77 RL21UtilWP::execute_purge($response['lpc']);
78 delete_transient('rabbitloader_trans_overview_data');
79 RabbitLoader_21_Core::sendJsonResponse($response);
80 };
81
82 add_action('wp_ajax_rabbitloader_ajax_purge', $purge_func);
83 add_action('wp_ajax_nopriv_rabbitloader_ajax_purge', $purge_func);
84
85 $mode_change_func = function () {
86 RL21UtilWP::verifyAjaxNonce();
87
88 if (!RabbitLoader_21_Admin::canAccessModeAjax()) {
89 wp_send_json_error(null, 403);
90 return;
91 }
92
93 $private_mode = !empty($_POST['private_mode']);
94 RabbitLoader_21_Admin::updateModeValue($private_mode);
95 $response = [
96 'result' => true,
97 'private_mode' => $private_mode,
98 'me_mode' => $private_mode,
99 'mode' => $private_mode ? 'me' : 'everyone'
100 ];
101
102 RabbitLoader_21_Core::sendJsonResponse($response);
103 };
104
105 add_action('wp_ajax_rabbitloader_mode_change', $mode_change_func);
106 add_action('wp_ajax_nopriv_rabbitloader_mode_change', $mode_change_func);
107
108 $mode_get_func = function () {
109 if (empty($_POST['rl_nonce'])) {
110 $request_nonce = RabbitLoader_21_Util_Core::get_param('rl_nonce', true);
111 if (!empty($request_nonce)) {
112 $_POST['rl_nonce'] = $request_nonce;
113 }
114 }
115
116 if (empty($_POST['token'])) {
117 $request_token = RabbitLoader_21_Util_Core::get_param('token', true);
118 if (!empty($request_token)) {
119 $_POST['token'] = $request_token;
120 }
121 }
122
123 RL21UtilWP::verifyAjaxNonce();
124
125 if (!RabbitLoader_21_Admin::canAccessModeAjax()) {
126 wp_send_json_error(null, 403);
127 return;
128 }
129
130 RabbitLoader_21_Core::getWpUserOption($user_options);
131 $private_mode = !empty($user_options['private_mode_val']);
132 $response = [
133 'result' => true,
134 'private_mode' => $private_mode,
135 'me_mode' => $private_mode,
136 'mode' => $private_mode ? 'me' : 'everyone'
137 ];
138
139 RabbitLoader_21_Core::sendJsonResponse($response);
140 };
141
142 add_action('wp_ajax_rabbitloader_mode_get', $mode_get_func);
143 add_action('wp_ajax_nopriv_rabbitloader_mode_get', $mode_get_func);
144 add_action('wp_ajax_rabbitloader_get_exclusions', function () {
145 RL21UtilWP::verifyAjaxNonce();
146
147 if (!current_user_can('manage_options')) {
148 wp_send_json_error(null, 403);
149 return;
150 }
151
152 $response = self::getExcludePatternsState();
153 RabbitLoader_21_Core::sendJsonResponse($response);
154 });
155 add_action('wp_ajax_rabbitloader_add_exclusion', function () {
156 $json_body = json_decode(file_get_contents('php://input'), true);
157 if (is_array($json_body)) {
158 foreach ($json_body as $key => $value) {
159 $_POST[$key] = $value;
160 }
161 }
162
163 RL21UtilWP::verifyAjaxNonce();
164
165 if (!current_user_can('manage_options')) {
166 wp_send_json_error(null, 403);
167 return;
168 }
169
170 $incoming_patterns = [];
171 $single_pattern = '';
172
173 if (isset($_POST['pattern'])) {
174 $single_pattern = wp_unslash($_POST['pattern']);
175 } elseif (isset($_GET['pattern'])) {
176 $single_pattern = wp_unslash($_GET['pattern']);
177 }
178
179 if (!empty($single_pattern)) {
180 $incoming_patterns[] = $single_pattern;
181 }
182
183 if (!empty($_POST['patterns']) && is_array($_POST['patterns'])) {
184 foreach ($_POST['patterns'] as $pattern) {
185 $incoming_patterns[] = $pattern;
186 }
187 }
188
189 $normalized_patterns = self::normalizeExcludePatterns($incoming_patterns);
190 if (empty($normalized_patterns)) {
191 wp_send_json_error(['message' => 'At least one valid exclusion pattern is required.'], 400);
192 return;
193 }
194
195 $response = self::addExcludePatterns($normalized_patterns);
196 RabbitLoader_21_Core::sendJsonResponse($response);
197 });
198 add_action('wp_ajax_rabbitloader_remove_exclusion', function () {
199 $json_body = json_decode(file_get_contents('php://input'), true);
200 if (is_array($json_body)) {
201 foreach ($json_body as $key => $value) {
202 $_POST[$key] = $value;
203 }
204 }
205
206 RL21UtilWP::verifyAjaxNonce();
207
208 if (!current_user_can('manage_options')) {
209 wp_send_json_error(null, 403);
210 return;
211 }
212
213 $incoming_patterns = [];
214 $single_pattern = '';
215
216 if (isset($_POST['pattern'])) {
217 $single_pattern = wp_unslash($_POST['pattern']);
218 } elseif (isset($_GET['pattern'])) {
219 $single_pattern = wp_unslash($_GET['pattern']);
220 }
221
222 if (!empty($single_pattern)) {
223 $incoming_patterns[] = $single_pattern;
224 }
225
226 if (!empty($_POST['patterns']) && is_array($_POST['patterns'])) {
227 foreach ($_POST['patterns'] as $pattern) {
228 $incoming_patterns[] = $pattern;
229 }
230 }
231
232 $normalized_patterns = self::normalizeExcludePatterns($incoming_patterns);
233 if (empty($normalized_patterns)) {
234 wp_send_json_error(['message' => 'At least one valid exclusion pattern is required.'], 400);
235 return;
236 }
237
238 $response = self::removeExcludePatterns($normalized_patterns);
239 RabbitLoader_21_Core::sendJsonResponse($response);
240 });
241 add_action('wp_ajax_rabbitloader_conflicts', function () {
242 if (!current_user_can('manage_options')) {
243 wp_send_json_error(null, 403);
244 return;
245 }
246
247 $context = sanitize_text_field(RabbitLoader_21_Util_Core::get_param('context'));
248 $isActivationFlow = strcmp($context, 'connected') !== 0;
249 $response = RabbitLoader_21_Conflicts::getConflictState($isActivationFlow);
250 RabbitLoader_21_Core::sendJsonResponse($response);
251 });
252 add_action('wp_ajax_rabbitloader_admin_boot', function () {
253 if (!current_user_can('manage_options')) {
254 wp_send_json_error(null, 403);
255 return;
256 }
257
258 RabbitLoader_21_Core::sendJsonResponse(self::getAdminBootData());
259 });
260 add_action('wp_ajax_rabbitloader_impact_data', function () {
261 RL21UtilWP::verifyAjaxNonce();
262
263 if (!current_user_can('manage_options')) {
264 wp_send_json_error(null, 403);
265 return;
266 }
267
268 $response = self::getImpactDataResponse();
269 RabbitLoader_21_Core::sendJsonResponse($response);
270 });
271 add_action('wp_ajax_rabbitloader_impact_refresh', function () {
272 RL21UtilWP::verifyAjaxNonce();
273
274 if (!current_user_can('manage_options')) {
275 wp_send_json_error(null, 403);
276 return;
277 }
278
279 $response = self::refreshImpactDataResponse();
280 RabbitLoader_21_Core::sendJsonResponse($response);
281 });
282 add_action('wp_ajax_rabbitloader_connect_proof', function () {
283 RL21UtilWP::verifyAjaxNonce();
284
285 if (!current_user_can('manage_options')) {
286 wp_send_json_error(null, 403);
287 return;
288 }
289
290 $challenge_id = isset($_POST['challenge_id']) ? sanitize_text_field(wp_unslash($_POST['challenge_id'])) : '';
291 $challenge_nonce = isset($_POST['challenge_nonce']) ? sanitize_text_field(wp_unslash($_POST['challenge_nonce'])) : '';
292 $site_url = isset($_POST['site_url']) ? esc_url_raw(wp_unslash($_POST['site_url'])) : '';
293
294 if (empty($challenge_id) || empty($challenge_nonce) || empty($site_url)) {
295 wp_send_json_error(['message' => 'Missing reconnect challenge data.'], 400);
296 return;
297 }
298
299 $site_host = self::normalizeConnectProofHost($site_url);
300 $home_host = self::normalizeConnectProofHost(home_url());
301 if (empty($site_host) || empty($home_host) || $site_host !== $home_host) {
302 wp_send_json_error(['message' => 'Reconnect challenge site mismatch.'], 400);
303 return;
304 }
305
306 $response = wp_remote_post(RabbitLoader_21_Core::getRLDomainV2() . 'domain/v2/reconnect-challenge/redeem', [
307 'timeout' => 15,
308 'headers' => [
309 'Content-Type' => 'application/json',
310 'Accept' => 'application/json',
311 ],
312 'body' => wp_json_encode([
313 'challenge_id' => $challenge_id,
314 'challenge_nonce' => $challenge_nonce,
315 'site_url' => home_url(),
316 'plugin_version' => RABBITLOADER_PLUG_VERSION,
317 'wp_admin_url' => admin_url('admin.php?page=rabbitloader'),
318 ]),
319 ]);
320
321 if (is_wp_error($response)) {
322 wp_send_json_error(['message' => 'Could not redeem reconnect challenge.'], 502);
323 return;
324 }
325
326 $status = (int) wp_remote_retrieve_response_code($response);
327 $body = json_decode(wp_remote_retrieve_body($response), true);
328 if ($status !== 200 || empty($body['result']) || empty($body['redeem_token'])) {
329 wp_send_json_error(['message' => 'Reconnect challenge was not accepted.'], $status >= 400 ? $status : 400);
330 return;
331 }
332
333 $result = [
334 'result' => true,
335 'challenge_id' => $challenge_id,
336 'redeem_token' => sanitize_text_field($body['redeem_token']),
337 'expires_at' => isset($body['expires_at']) ? intval($body['expires_at']) : 0,
338 ];
339 RabbitLoader_21_Core::sendJsonResponse($result);
340 });
341 add_action('wp_ajax_rabbitloader_save_keys', function () {
342 $json_body = json_decode(file_get_contents('php://input'), true);
343 if (is_array($json_body)) {
344 foreach ($json_body as $key => $value) {
345 $_POST[$key] = $value;
346 }
347 }
348
349 RL21UtilWP::verifyAjaxNonce();
350
351 if (!current_user_can('manage_options')) {
352 wp_send_json_error(null, 403);
353 return;
354 }
355
356 $conflict_state = RabbitLoader_21_Conflicts::getConflictState(true);
357 if (!empty($conflict_state['activation_blocked'])) {
358 $conflict_state['result'] = false;
359 if (empty($conflict_state['message'])) {
360 $conflict_state['message'] = RL21UtilWP::__('Resolve the blocking RabbitLoader conflict(s) before connecting this site.');
361 }
362
363 status_header(409);
364 RabbitLoader_21_Core::sendJsonResponse($conflict_state);
365 }
366
367 $encoded_token = RabbitLoader_21_Util_Core::get_param('rl-token', true);
368 $decoded_token = !empty($encoded_token) ? base64_decode($encoded_token, true) : false;
369 if (empty($decoded_token)) {
370 wp_send_json_error(['message' => 'Invalid token'], 400);
371 return;
372 }
373
374 $tokens = json_decode($decoded_token, true);
375 if (empty($tokens['api_token'])) {
376 wp_send_json_error(['message' => 'Invalid token'], 400);
377 return;
378 }
379
380 $urlparts = parse_url(home_url());
381 RabbitLoader_21_Core::update_api_tokens($tokens['api_token'], $urlparts['host'], $tokens['did'], '');
382 if (isset($tokens['cdn_prefix'])) {
383 update_option('rabbitloader_cdn_prefix', $tokens['cdn_prefix'], true);
384 }
385
386 do_action('rl_site_connected');
387
388 $response = self::getAdminBootData();
389 $response['result'] = true;
390 RabbitLoader_21_Core::sendJsonResponse($response);
391 });
392 add_action('wp_ajax_rabbitloader_disconnect', function () {
393 RL21UtilWP::verifyAjaxNonce();
394
395 if (!current_user_can('manage_options')) {
396 wp_send_json_error(null, 403);
397 return;
398 }
399
400 self::disconnectPlugin();
401
402 $response = [
403 'result' => true,
404 'admin_ajax' => admin_url('admin-ajax.php'),
405 'rl_nonce' => wp_create_nonce('rl-ajax-nonce'),
406 'rl_acct' => false,
407 'is_connected' => false,
408 'api_token' => '',
409 'did' => '',
410 'domain' => '',
411 'plan_title' => '',
412 'home_page_url_id' => ''
413 ];
414 RabbitLoader_21_Core::sendJsonResponse($response);
415 });
416 add_action('wp_ajax_rabbitloader_ajax_cron', function () {
417 //self::deferred_exe();
418 RL21UtilWP::verifyAjaxNonce();
419 });
420 add_action('wp_ajax_rabbitloader_warmup_urls', function () {
421 RL21UtilWP::verifyAjaxNonce();
422 $response = [
423 'poffset' => intval($_POST['poffset']),
424 'posts_per_page' => intval($_POST['posts_per_page']),
425 ];
426 RabbitLoader_21_Core::get_recent_posts($response['poffset'], $response['posts_per_page'], $response['published_count'], $response['permalinks']);
427 RabbitLoader_21_Core::sendJsonResponse($response);
428 });
429 // add_action('wp_ajax_rabbitloader_ajax_survey_dismissed', function () {
430 // RL21UtilWP::verifyAjaxNonce();
431 // self::survey_dismissed(self::SURVEY_DIS_PERMA);
432 // });
433 add_action('rl_site_connected', function () {
434 self::rl_site_connected();
435 });
436 add_action('plugins_loaded', 'RabbitLoader_AD_AD::on_plugins_loaded');
437 //listeners for taxonomy changes
438
439 add_action('wp_print_scripts', function () {
440 if (RabbitLoader_21_Util_Core::isRLPage()) {
441 wp_dequeue_script('monsterinsights-vue-frontend'); //vue JS conflict with apexcharts window.SVG().addTo fails
442 }
443 }, 100);
444 }
445
446 public static function init() {}
447
448 private static function canAccessModeAjax()
449 {
450 if (!empty($_POST['token'])) {
451 return true;
452 }
453
454 return current_user_can('manage_options');
455 }
456
457 private static function normalizeExcludePatterns($patterns)
458 {
459 if (!is_array($patterns)) {
460 $patterns = [$patterns];
461 }
462
463 $normalized = [];
464 foreach ($patterns as $pattern) {
465 if (!is_scalar($pattern)) {
466 continue;
467 }
468
469 $pattern = sanitize_text_field((string) $pattern);
470 $pattern = preg_replace('/[\r\n\t]+/', '', $pattern);
471 $pattern = trim($pattern);
472
473 if ($pattern === '' || strpos($pattern, "\0") !== false) {
474 continue;
475 }
476
477 $normalized[$pattern] = true;
478 }
479
480 return array_keys($normalized);
481 }
482
483 private static function getExcludePatternsState()
484 {
485 RabbitLoader_21_Core::getWpUserOption($user_options);
486 $patterns = self::normalizeExcludePatterns(explode("\n", $user_options['exclude_patterns']));
487
488 return [
489 'result' => true,
490 'patterns' => $patterns,
491 'exclude_patterns' => implode("\n", $patterns),
492 'count' => count($patterns),
493 ];
494 }
495
496 private static function addExcludePatterns($patterns)
497 {
498 RabbitLoader_21_Core::getWpUserOption($user_options);
499 $existing_patterns = self::normalizeExcludePatterns(explode("\n", $user_options['exclude_patterns']));
500 $existing_map = [];
501 foreach ($existing_patterns as $pattern) {
502 $existing_map[$pattern] = true;
503 }
504
505 $added_patterns = [];
506 foreach ($patterns as $pattern) {
507 if (isset($existing_map[$pattern])) {
508 continue;
509 }
510
511 $existing_map[$pattern] = true;
512 $existing_patterns[] = $pattern;
513 $added_patterns[] = $pattern;
514 }
515
516 $user_options['exclude_patterns'] = implode("\n", $existing_patterns);
517 RabbitLoader_21_Core::updateUserOption($user_options);
518
519 if (!empty($added_patterns)) {
520 RL21UtilWP::onPostChange(RL21UtilWP::POST_ID_ALL);
521 }
522
523 return [
524 'result' => true,
525 'message' => empty($added_patterns) ? 'Exclusion pattern already exists.' : 'Exclusion pattern(s) added successfully.',
526 'added_patterns' => $added_patterns,
527 'patterns' => $existing_patterns,
528 'exclude_patterns' => $user_options['exclude_patterns'],
529 'count' => count($existing_patterns),
530 ];
531 }
532
533 private static function removeExcludePatterns($patterns)
534 {
535 RabbitLoader_21_Core::getWpUserOption($user_options);
536 $existing_patterns = self::normalizeExcludePatterns(explode("\n", $user_options['exclude_patterns']));
537 $remove_map = [];
538 foreach ($patterns as $pattern) {
539 $remove_map[$pattern] = true;
540 }
541
542 $remaining_patterns = [];
543 $removed_patterns = [];
544 foreach ($existing_patterns as $pattern) {
545 if (isset($remove_map[$pattern])) {
546 $removed_patterns[] = $pattern;
547 continue;
548 }
549
550 $remaining_patterns[] = $pattern;
551 }
552
553 $user_options['exclude_patterns'] = implode("\n", $remaining_patterns);
554 RabbitLoader_21_Core::updateUserOption($user_options);
555
556 if (!empty($removed_patterns)) {
557 RL21UtilWP::onPostChange(RL21UtilWP::POST_ID_ALL);
558 }
559
560 return [
561 'result' => true,
562 'message' => empty($removed_patterns) ? 'Exclusion pattern not found.' : 'Exclusion pattern(s) removed successfully.',
563 'removed_patterns' => $removed_patterns,
564 'patterns' => $remaining_patterns,
565 'exclude_patterns' => $user_options['exclude_patterns'],
566 'count' => count($remaining_patterns),
567 ];
568 }
569
570 private static function updateModeValue($private_mode)
571 {
572 RabbitLoader_21_Core::getWpUserOption($user_options);
573 $user_options['private_mode_val'] = !empty($private_mode);
574 $user_options['private_mode_ts'] = date('c');
575 RabbitLoader_21_Core::updateUserOption($user_options);
576
577 try {
578 // remove public pages cache, main purpose is to purge TPV
579 RabbitLoader_21_TP::purge_all($tp_purge_count);
580 } catch (\Throwable $e) {
581 RabbitLoader_21_Core::on_exception($e);
582 }
583 }
584
585 protected static function disconnectPlugin()
586 {
587 $disconnect_context = [
588 'did' => RabbitLoader_21_Core::getWpOptVal('did'),
589 'domain' => RabbitLoader_21_Core::getWpOptVal('domain'),
590 'token_present' => !empty(RabbitLoader_21_Core::getWpOptVal('api_token')),
591 ];
592 error_log('RabbitLoader manual disconnect test: sending heartbeat ' . wp_json_encode($disconnect_context));
593
594 $post_data = ['disconnect' => 1];
595 $http = RabbitLoader_21_Core::callPOSTAPI('domain/heartbeat', $post_data, $apiError, $apiMessage);
596 $response_body = !empty($http['body']) && is_array($http['body']) ? $http['body'] : [];
597 $response_context = [
598 'status' => empty($http['response']['code']) ? 0 : (int) $http['response']['code'],
599 'result' => array_key_exists('result', $response_body) ? (bool) $response_body['result'] : null,
600 'message' => empty($response_body['message']) ? (isset($apiMessage) ? (string) $apiMessage : '') : (string) $response_body['message'],
601 'api_error' => (bool) $apiError,
602 'token_present_after_call' => !empty(RabbitLoader_21_Core::getWpOptVal('api_token')),
603 ];
604 error_log('RabbitLoader manual disconnect test: heartbeat response ' . wp_json_encode($response_context));
605
606 if ($apiError) {
607 error_log('RabbitLoader: backend manual disconnect notification failed.');
608 }
609 error_log('RabbitLoader manual disconnect test: continuing to clear local credentials');
610 RabbitLoader_21_Core::update_api_tokens('', '', '', 'user action disconnect');
611 delete_transient('rabbitloader_trans_overview_data');
612 }
613
614 public static function leftMenuOption()
615 {
616 self::get_warnings($notification_count, false);
617 add_menu_page(
618 'RabbitLoader',
619 $notification_count ? sprintf('RabbitLoader <span class="awaiting-mod">%d</span>', $notification_count) : 'RabbitLoader',
620 'manage_options',
621 'rabbitloader',
622 'RabbitLoader_21_Tab_Init::echoPluginPage',
623 dirname(plugin_dir_url(__FILE__)) . '/images/icon_16.png',
624 //'',
625 20
626 );
627
628 if (RabbitLoader_21_Util_Core::isRLPage()) {
629 add_action('admin_head', 'admin_styles', 10, 1);
630 function admin_styles($a)
631 {
632 echo '<link rel="stylesheet" href="' . RABBITLOADER_PLUG_URL . 'admin/css/bootstrap.v5.1.3.min.css' . '" type="text/css" media="all" />
633 <link rel="stylesheet" href="' . RABBITLOADER_PLUG_URL . 'admin/css/style.css?v=' . RABBITLOADER_PLUG_VERSION . '" type="text/css" media="all" />
634 <link rel="stylesheet" href="' . RABBITLOADER_PLUG_URL . 'admin/css/mfe-patches.css?v=' . RABBITLOADER_PLUG_VERSION . '" type="text/css" media="all" />';
635 }
636 }
637 add_action('admin_head', function () {
638 echo '<link rel="stylesheet" href="' . RABBITLOADER_PLUG_URL . 'admin/css/style-common.css?v=' . RABBITLOADER_PLUG_VERSION . '" type="text/css" media="all" />';
639 }, 10, 1);
640 }
641
642
643 protected static function isPluginActivated()
644 {
645 $api_token = RabbitLoader_21_Core::getWpOptVal('api_token');
646 $did = RabbitLoader_21_Core::getWpOptVal('did');
647
648 return !empty($api_token) && preg_match('/^[a-f0-9]{24}$/i', $did) === 1;
649 }
650
651 protected static function getAdminBootData()
652 {
653 $overview = RabbitLoader_21_Tab_Init::getOverviewData();
654 $is_connected = self::isPluginActivated();
655
656 return [
657 'admin_ajax' => admin_url('admin-ajax.php'),
658 'rl_nonce' => wp_create_nonce('rl-ajax-nonce'),
659 'rl_acct' => $is_connected,
660 'is_connected' => $is_connected,
661 'api_token' => RabbitLoader_21_Core::getWpOptVal('api_token'),
662 'did' => RabbitLoader_21_Core::getWpOptVal('did'),
663 'domain' => RabbitLoader_21_Core::getWpOptVal('domain'),
664 'plan_title' => isset($overview['plan_title']) ? $overview['plan_title'] : '',
665 'home_page_url_id' => isset($overview['home_page_url_id']) ? $overview['home_page_url_id'] : '',
666 'published_url_count' => isset($overview['published_url_count']) ? intval($overview['published_url_count']) : 0,
667 'canonical_url_count' => isset($overview['canonical_url_count']) ? intval($overview['canonical_url_count']) : 0,
668 'optimized_url_count' => isset($overview['optimized_url_count']) ? intval($overview['optimized_url_count']) : 0,
669 'impact_actions' => [
670 'fetch' => 'rabbitloader_impact_data',
671 'refresh' => 'rabbitloader_impact_refresh',
672 ],
673 ];
674 }
675
676 protected static function getImpactDataResponse()
677 {
678 $response = [
679 'result' => false,
680 'data' => [
681 'last_checked' => '',
682 'metrics' => [],
683 ],
684 'message' => '',
685 ];
686
687 $http = RabbitLoader_21_Core::callGETAPIV2('billing/{domain_id}/impact', $apiError, $apiMessage);
688 if ($apiError) {
689 $response['message'] = empty($apiMessage) ? 'Unable to fetch impact data.' : $apiMessage;
690 return $response;
691 }
692
693 $response['result'] = true;
694 $response['data'] = self::normalizeImpactPayload(isset($http['body']['data']) ? $http['body']['data'] : []);
695 return $response;
696 }
697
698 protected static function refreshImpactDataResponse()
699 {
700 $response = [
701 'result' => false,
702 'data' => [
703 'last_checked' => '',
704 'metrics' => [],
705 ],
706 'message' => '',
707 ];
708
709 $http = RabbitLoader_21_Core::callPOSTAPIV2('billing/{domain_id}/impact/refresh', [], $apiError, $apiMessage);
710 if ($apiError) {
711 $response['message'] = empty($apiMessage) ? 'Unable to refresh impact data.' : $apiMessage;
712 return $response;
713 }
714
715 $response['result'] = true;
716 $response['data'] = self::normalizeImpactPayload(isset($http['body']['data']) ? $http['body']['data'] : []);
717 $response['message'] = empty($apiMessage) ? 'Impact refresh requested.' : $apiMessage;
718 return $response;
719 }
720
721 protected static function normalizeImpactPayload($data)
722 {
723 $normalized = [
724 'last_checked' => '',
725 'metrics' => [],
726 ];
727
728 if (!is_array($data)) {
729 return $normalized;
730 }
731
732 $normalized['last_checked'] = self::extractImpactLastChecked($data);
733
734 foreach (['lcp', 'tbt', 'speed_index'] as $metric_key) {
735 $normalized['metrics'][$metric_key] = self::extractImpactMetric($data, $metric_key);
736 }
737
738 return $normalized;
739 }
740
741 protected static function extractImpactLastChecked($data)
742 {
743 $candidates = [
744 'last_checked',
745 'lastChecked',
746 'checked_at',
747 'checkedAt',
748 'updated_at',
749 'updatedAt',
750 'generated_at',
751 'generatedAt',
752 ];
753
754 foreach ($candidates as $candidate) {
755 if (!empty($data[$candidate]) && is_scalar($data[$candidate])) {
756 return (string) $data[$candidate];
757 }
758 }
759
760 if (!empty($data['meta']) && is_array($data['meta'])) {
761 return self::extractImpactLastChecked($data['meta']);
762 }
763
764 return '';
765 }
766
767 protected static function extractImpactMetric($data, $metric_key)
768 {
769 $metric = [
770 'before' => null,
771 'after' => null,
772 ];
773
774 $sources = [];
775 if (!empty($data[$metric_key]) && is_array($data[$metric_key])) {
776 $sources[] = $data[$metric_key];
777 }
778 if (!empty($data['metrics']) && is_array($data['metrics']) && !empty($data['metrics'][$metric_key]) && is_array($data['metrics'][$metric_key])) {
779 $sources[] = $data['metrics'][$metric_key];
780 }
781 if (!empty($data['impact']) && is_array($data['impact']) && !empty($data['impact'][$metric_key]) && is_array($data['impact'][$metric_key])) {
782 $sources[] = $data['impact'][$metric_key];
783 }
784
785 foreach ($sources as $source) {
786 $before = self::extractImpactValue($source, ['before', 'without', 'without_rabbitloader', 'baseline', 'original']);
787 $after = self::extractImpactValue($source, ['after', 'with', 'with_rabbitloader', 'optimized']);
788
789 if ($before !== null) {
790 $metric['before'] = $before;
791 }
792 if ($after !== null) {
793 $metric['after'] = $after;
794 }
795 if ($metric['before'] !== null || $metric['after'] !== null) {
796 break;
797 }
798 }
799
800 return $metric;
801 }
802
803 protected static function extractImpactValue($source, $candidate_keys)
804 {
805 foreach ($candidate_keys as $key) {
806 if (array_key_exists($key, $source)) {
807 return self::sanitizeImpactNumber($source[$key]);
808 }
809 }
810
811 foreach ($candidate_keys as $key) {
812 if (!empty($source[$key]) && is_array($source[$key])) {
813 $nested = self::extractImpactValue($source[$key], ['value', 'metric', 'ms']);
814 if ($nested !== null) {
815 return $nested;
816 }
817 }
818 }
819
820 return null;
821 }
822
823 protected static function sanitizeImpactNumber($value)
824 {
825 if ($value === null || $value === '') {
826 return null;
827 }
828
829 if (is_numeric($value)) {
830 return floatval($value);
831 }
832
833 if (is_string($value)) {
834 $clean = preg_replace('/[^0-9.\-]/', '', $value);
835 if ($clean !== '' && is_numeric($clean)) {
836 return floatval($clean);
837 }
838 }
839
840 return null;
841 }
842
843 protected static function getConnectProofConfig()
844 {
845 $allowed_origins = RabbitLoader_21_Util_Core::isDev()
846 ? ['https://dash.rabbitloader.local', 'http://localhost:3000', 'https://dash.rabbitloader.com']
847 : ['https://dash.rabbitloader.com'];
848
849 return [
850 'admin_ajax' => admin_url('admin-ajax.php'),
851 'rl_nonce' => wp_create_nonce('rl-ajax-nonce'),
852 'home_url' => home_url(),
853 'wp_admin_url' => admin_url('admin.php?page=rabbitloader'),
854 'plugin_version' => RABBITLOADER_PLUG_VERSION,
855 'can_manage_options' => current_user_can('manage_options'),
856 'allowed_dashboard_origins' => $allowed_origins,
857 ];
858 }
859
860 protected static function normalizeConnectProofHost($url)
861 {
862 $host = wp_parse_url($url, PHP_URL_HOST);
863 if (empty($host)) {
864 return '';
865 }
866
867 $host = strtolower($host);
868 return preg_replace('/^www\./', '', $host);
869 }
870
871 public static function admin_notices()
872 {
873
874 //self::survey();
875
876 try {
877 $page = RabbitLoader_21_Util_Core::get_param('page');
878
879 if (self::$admin_notice_shown || ($page == 'rabbitloader')) {
880 return;
881 }
882
883 self::$admin_notice_shown = true;
884
885 $plug_url = admin_url("admin.php?page=rabbitloader");
886
887
888 if (!self::isPluginActivated()) {
889 echo '
890 <div class="notice notice-error is-dismissible"><p>';
891 printf(RL21UtilWP::__('RabbitLoader is disconnected. Your pages are not optimized. <a href="%s">Click here to connect</a>'), $plug_url);
892 echo '</p></div>';
893 } else {
894 self::get_warnings($notification_count, false);
895
896 if ($notification_count > 0) {
897 echo '<div class="notice notice-error is-dismissible"><p>';
898 printf(RL21UtilWP::_n('RabbitLoader has %d warning which is affecting your website\'s optimizations. <a href="%s">check details</a>', 'RabbitLoader has %d warnings which may affect your website\'s optimizations. <a href="%s">check details</a>', $notification_count), $notification_count, $plug_url);
899 echo '</p></div>';
900 }
901 }
902 } catch (Throwable $e) {
903 RabbitLoader_21_Core::on_exception($e);
904 } catch (Exception $e) {
905 RabbitLoader_21_Core::on_exception($e);
906 }
907 }
908
909 private static function survey()
910 {
911 $showSurvey = false;
912 $integration_start_time = RabbitLoader_21_Core::getWpOptVal('token_update_ts');
913 if (!empty($integration_start_time)) {
914 $maxTimeSec = 1 * 3600; //after 1hour
915 $elapsedTimeSec = time() - $integration_start_time;
916 $showSurvey = $elapsedTimeSec > $maxTimeSec;
917 }
918
919 if (!$showSurvey) {
920 return;
921 }
922
923 $user_id = get_current_user_id();
924 if (empty($user_id)) {
925 return;
926 }
927 //delete_user_meta($user_id, 'rl_survey_dismissed');
928 $dismiss_time = intval(get_user_meta($user_id, 'rl_survey_dismissed', true));
929 if (self::SURVEY_DIS_PERMA === $dismiss_time) {
930 //permanently dismissed
931 return;
932 } else if (!empty($dismiss_time) && ($dismiss_time > strtotime('-7 days'))) {
933 //dismiss within last 7 days
934 return;
935 }
936
937 $remindLaterURL = esc_url(add_query_arg('rl_survey_dismissed', time()));
938 $remindNeverURL = esc_url(add_query_arg('rl_survey_dismissed', self::SURVEY_DIS_PERMA));
939 echo '<div class="notice notice-info is-dismissible rl_survey_notice" style="background: #f4f4f4; color: #1d2327; border-width: 1px; border-style: solid; border-color: #1d2327; padding: 1rem 1rem; border-radius: 5px;"><div style="float:left; padding-right:1rem;"><img src="' . RABBITLOADER_PLUG_URL . '/assets/icon-dark.svg" width="100" /></div>';
940 echo '<p class="p1">';
941 RL21UtilWP::_e("Enjoying RabbitLoader? 🚀");
942 echo '</p>';
943 echo '<p>';
944 RL21UtilWP::_e('Share your thoughts in a quick 10-second anonymous survey. Your feedback helps us hop forward! 🐇');
945 echo '</p>';
946 echo '<p class="p" style="margin-top: 1.5rem;"><button id="rl_show_survey" class="rl-btn rl-btn-primary mt-2 mb-sm-0">';
947 RL21UtilWP::_e('Yes, Continue');
948 echo '</button> <a href="' . $remindLaterURL . '" class="rl-btn" style="color:#0076CE;">';
949 RL21UtilWP::_e('Ask me later');
950 echo '</a><a href="' . $remindNeverURL . '" class="rl-btn" style="color:#0076CE;">';
951 RL21UtilWP::_e('I already did');
952 echo '</a></p>';
953 echo '</div>';
954 }
955
956 // private static function survey_dismissed($forceTime)
957 // {
958 // $user_id = get_current_user_id();
959 // if (empty($user_id)) {
960 // wp_send_json_error(null, 403);
961 // }
962 // if (isset($_GET['rl_survey_dismissed'])) {
963 // delete_user_meta($user_id, 'rl_survey_dismissed');
964 // update_user_meta($user_id, 'rl_survey_dismissed', intval($_GET['rl_survey_dismissed']), false);
965 // }
966 // if ($forceTime) {
967 // delete_user_meta($user_id, 'rl_survey_dismissed');
968 // update_user_meta($user_id, 'rl_survey_dismissed', $forceTime, false);
969 // }
970 // }
971 public static function admin_init()
972 {
973 // if (isset($_GET['rl_survey_dismissed'])) {
974 // self::survey_dismissed(0);
975 // }
976 }
977
978 protected static function get_warnings(&$count, $print)
979 {
980
981 if (self::$rabbitloader_cache_warnings === false) {
982
983 self::$rabbitloader_cache_warnings = [];
984
985 $otherConflictPluginMessages = RabbitLoader_21_Conflicts::getMessages(false);
986 foreach ($otherConflictPluginMessages as $plugMessage) {
987 self::$rabbitloader_cache_warnings[] = $plugMessage;
988 }
989
990 $adv_cache_msg = RL21UtilWP::__("The file /wp-content/advanced-cache.php is not writable. Please make sure that the PHP script has write access to the /wp-content/ directory and refresh this page to make RabbitLoader work efficiently.");
991
992 if (!defined('RABBITLOADER_AC_ACTIVE') || (RABBITLOADER_PLUG_VERSION != RABBITLOADER_AC_PLUG_VERSION) || (LOGGED_IN_COOKIE != RABBITLOADER_AC_LOGGED_IN_COOKIE)) {
993 $aac_code = self::activate_advanced_cache();
994 if ($aac_code === 4) {
995 self::$rabbitloader_cache_warnings[] = $adv_cache_msg;
996 }
997 }
998
999 if ((!defined("WP_CACHE") || !WP_CACHE)) {
1000 if (RL21UtilWP::is_flywheel()) {
1001 self::$rabbitloader_cache_warnings[] = sprintf(RL21UtilWP::__('Please enable WP_CACHE from the Flywheel settings <a href="%s">check details</a>'), "https://rabbitloader.com/kb/settings-for-flywheel/");
1002 } else if (!self::update_wp_config_const('WP_CACHE', 'true')) {
1003 self::$rabbitloader_cache_warnings[] = RL21UtilWP::__("The file /wp-config.php is not writable. Please make sure the file is writable or set WP_CACHE value to true to make RabbitLoader work efficiently.");
1004 }
1005 }
1006
1007 RabbitLoader_21_Core::getWpOption($rl_wp_options);
1008 if (!empty($rl_wp_options['rl_hb_messages'])) {
1009 foreach ($rl_wp_options['rl_hb_messages'] as $message) {
1010 if (!empty($message['fd'])) {
1011 self::$rabbitloader_cache_warnings[] = RL21UtilWP::__($message['fd']);
1012 }
1013 }
1014 }
1015 if (!empty($rl_wp_options['rl_latest_plugin_v'])) {
1016 if (version_compare(RABBITLOADER_PLUG_VERSION, $rl_wp_options['rl_latest_plugin_v']) == -1) {
1017 self::$rabbitloader_cache_warnings[] = RL21UtilWP::__("You are using an outdated version of RabbitLoader plugin. Please update it for a better experience.");
1018 }
1019 }
1020 }
1021
1022 $count = count(self::$rabbitloader_cache_warnings);
1023
1024 if ($print) {
1025 foreach (self::$rabbitloader_cache_warnings as $message) {
1026 // echo '<div class="alert alert-danger" role="alert">';
1027 // _e($message, RABBITLOADER_TEXT_DOMAIN);
1028 // echo '</div>';
1029 echo '<div class="notice notice-error"><p><b>';
1030 RL21UtilWP::_e('Warning');
1031 echo ': </b>';
1032 _e($message, RABBITLOADER_TEXT_DOMAIN);
1033 echo '</div>';
1034 }
1035 }
1036 }
1037
1038 public static function activate_advanced_cache()
1039 {
1040
1041 try {
1042 if (!empty(RabbitLoader_21_Conflicts::getMessages(false))) {
1043 return 1;
1044 }
1045
1046 // if (!RabbitLoader_21_Core::htaccessExists()) {
1047 // return 2;
1048 // }
1049
1050 $adv_cache_sample = RABBITLOADER_PLUG_DIR . "advanced-cache.php";
1051 $file_updated = false;
1052 if (file_exists($adv_cache_sample)) {
1053 $adv_cache_contents = file_get_contents($adv_cache_sample);
1054 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_ABSPATH%%", ABSPATH, $adv_cache_contents);
1055 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_PLUG_DIR%%", RABBITLOADER_PLUG_DIR, $adv_cache_contents);
1056 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_LOGGED_IN_COOKIE%%", LOGGED_IN_COOKIE, $adv_cache_contents);
1057 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_CACHE_DIR%%", RABBITLOADER_CACHE_DIR, $adv_cache_contents);
1058 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_PLUG_VERSION%%", RABBITLOADER_PLUG_VERSION, $adv_cache_contents);
1059 $adv_cache_contents = str_replace("%%RABBITLOADER_AC_PLUG_ENV%%", RABBITLOADER_PLUG_ENV, $adv_cache_contents);
1060
1061 $advanced_cache_file = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'advanced-cache.php';
1062 $file_updated = RabbitLoader_21_Util_Core::fpc($advanced_cache_file, $adv_cache_contents, WP_DEBUG);
1063 }
1064
1065 if ($file_updated) {
1066 self::update_wp_config_const('WP_CACHE', 'true');
1067 return 3;
1068 } else {
1069 return 4;
1070 }
1071 } catch (Throwable $e) {
1072 RabbitLoader_21_Core::on_exception($e);
1073 }
1074 }
1075
1076 private static function update_wp_config_const($const_name, $const_val)
1077 {
1078 $wp_config_path = RL21UtilWP::get_wp_config();
1079 //check if config file is writable
1080 if (empty($wp_config_path) || !is_writable($wp_config_path)) {
1081 //echo 'rl_not_writable__';
1082 return;
1083 }
1084
1085 $lines = file($wp_config_path);
1086 $last_line = count($lines) - 1;
1087
1088 $new_file = array();
1089 $const_added = false;
1090 foreach ($lines as $current_line => $line_content) {
1091 //check if constant is already defined
1092 if (preg_match("/define\(\s*'{$const_name}'/i", $line_content)) {
1093 $const_added = true;
1094 $new_file[] = "if (!defined('{$const_name}')) { define( '{$const_name}', {$const_val} );}\n\n";
1095 continue; //dont't break here, its a complete file rewrite
1096 }
1097
1098 $thatsAllLine = (preg_match("/\/\* That's all, stop editing!.*/i", $line_content)); //constants should be before this line.
1099 $isLast = ($thatsAllLine && !defined($const_name)) || ($last_line == $current_line);
1100
1101 // If we reach the end and no define - add it.
1102 if (empty($const_added) && $isLast) {
1103 $const_added = true;
1104 $new_file[] = "if (!defined('{$const_name}')) { define( '{$const_name}', {$const_val} );}\n\n";
1105 }
1106
1107 $new_file[] = $line_content;
1108 }
1109 $file_contents = implode("", $new_file);
1110 return RabbitLoader_21_Util_Core::fpc($wp_config_path, $file_contents, WP_DEBUG);
1111 }
1112
1113 public static function plugin_deactivate($network_wide = false, $notifyBackend = true)
1114 {
1115 if ($notifyBackend) {
1116 RabbitLoader_21_Core::notify_backend_disconnect('plugin deactivate', $apiError, $apiMessage);
1117 }
1118
1119 try {
1120 self::update_wp_config_const('WP_CACHE', 'false');
1121 $advanced_cache_file = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'advanced-cache.php';
1122 if (file_exists($advanced_cache_file)) { //during uninstall RABBITLOADER_AC_ACTIVE will not be there
1123 $adv_cache_contents = "";
1124 $file_updated = RabbitLoader_21_Util_Core::fpc($advanced_cache_file, $adv_cache_contents, WP_DEBUG);
1125 }
1126 } catch (Throwable $e) {
1127 RabbitLoader_21_Core::on_exception($e);
1128 }
1129 }
1130
1131 public static function plugin_uninstall()
1132 {
1133 self::plugin_deactivate(false, false);
1134
1135 RabbitLoader_21_Core::notify_backend_disconnect('plugin uninstall', $apiError, $apiMessage);
1136
1137 try {
1138 RabbitLoader_21_Core::cleanAllCachedFiles();
1139 RabbitLoader_21_Core::delete_log_file(RabbitLoader_21_Core::LOCAL_CONFIG_FILE);
1140 } catch (Exception $e) {
1141 }
1142 $ourOptions = array('rabbitloader_field_apikey', 'rabbitloader_field_apisecret', 'rabbitloader_field_domain', 'rl_optimizer_engine_version', 'rabbit_loader_wp_options', 'rabbit_loader_user_options', 'rabbitloader_cdn_prefix');
1143
1144 foreach ($ourOptions as $optionName) {
1145 delete_option($optionName);
1146 }
1147 }
1148
1149 private static function rl_site_connected()
1150 {
1151 $api_token = RabbitLoader_21_Core::getWpOptVal('api_token');
1152 $domain = RabbitLoader_21_Core::getWpOptVal('domain');
1153 $did = RabbitLoader_21_Core::getWpOptVal('did');
1154 RabbitLoader_21_Core::callPOSTAPI('domain/heartbeat', [], $apiError, $apiMessage);
1155 if ($apiError) {
1156 RabbitLoader_21_Core::update_api_tokens($api_token, $domain, $did, 'connected; heartbeat notification failed');
1157 error_log('RabbitLoader: backend connection heartbeat failed after saving credentials.');
1158 }
1159
1160 try {
1161 RabbitLoader_21_TP::purge_all($tp_purge_count);
1162 } catch (\Throwable $e) {
1163 RabbitLoader_21_Core::on_exception($e);
1164 }
1165 }
1166
1167 public static function settings_link($links)
1168 {
1169 $url_settings = esc_url(add_query_arg(
1170 'page',
1171 'rabbitloader',
1172 get_admin_url() . 'admin.php'
1173 ));
1174 $link = "<a href='$url_settings'>" . __('Settings') . '</a>';
1175 array_push($links, $link);
1176 $url_kb = esc_url('https://rabbitloader.com/kb/');
1177 $link = "<a target='_blank' href='$url_kb'>" . __('Knowledge Base') . '</a>';
1178 array_push($links, $link);
1179 return $links;
1180 }
1181 }
1182