PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.7
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.7
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-admin-ajax.php

class-metasync-admin-ajax.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.7, at includes/class-metasync-admin-ajax.php

1,351 lines 54.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 class Metasync_Admin_Ajax
7 {
8 private static $instance = null;
9
10 private $db_redirection = null;
11
12 public static function instance()
13 {
14 if (null === self::$instance) {
15 self::$instance = new self();
16 }
17 return self::$instance;
18 }
19
20 private function __construct() {}
21
22 private function get_db_redirection()
23 {
24 if (null === $this->db_redirection) {
25 $this->db_redirection = new Metasync_Redirection_Database();
26 }
27 return $this->db_redirection;
28 }
29
30 public function ajax_import_external_data()
31 {
32 $execution_time = Metasync_Settings_Fields::instance()->get_execution_setting('max_execution_time');
33 if (function_exists('set_time_limit')) {
34 @set_time_limit($execution_time);
35 }
36
37 Metasync_Settings_Fields::instance()->apply_memory_limit();
38
39 check_ajax_referer('metasync_import_external_data', 'nonce');
40
41 if (!Metasync::current_user_has_plugin_access()) {
42 wp_send_json_error(['message' => 'Insufficient permissions.']);
43 }
44
45 $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : '';
46 $plugin = isset($_POST['plugin']) ? sanitize_text_field($_POST['plugin']) : '';
47
48 if (empty($type) || empty($plugin)) {
49 wp_send_json_error(['message' => 'Missing required parameters.']);
50 }
51
52 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-external-importer.php';
53 $importer = new Metasync_External_Importer($this->get_db_redirection());
54 $result = ['success' => false, 'message' => 'Unknown import type.'];
55
56 switch ($type) {
57 case 'redirections':
58 $result = $importer->import_redirections($plugin);
59 break;
60 case 'sitemap':
61 $result = $importer->import_sitemap($plugin);
62 break;
63 case 'robots':
64 $result = $importer->import_robots($plugin);
65 break;
66 case 'indexation':
67 $result = $importer->import_indexation($plugin);
68 break;
69 case 'schema':
70 $result = $importer->import_schema($plugin);
71 break;
72 }
73
74 if ($result['success']) {
75 wp_send_json_success($result);
76 } else {
77 wp_send_json_error($result);
78 }
79 }
80
81 public function ajax_import_seo_metadata()
82 {
83 $execution_time = Metasync_Settings_Fields::instance()->get_execution_setting('max_execution_time');
84 if (function_exists('set_time_limit')) {
85 @set_time_limit($execution_time);
86 }
87
88 Metasync_Settings_Fields::instance()->apply_memory_limit();
89
90 check_ajax_referer('metasync_import_seo_metadata', 'nonce');
91
92 if (!Metasync::current_user_has_plugin_access()) {
93 wp_send_json_error(['message' => 'Insufficient permissions.']);
94 }
95
96 $plugin = isset($_POST['plugin']) ? sanitize_text_field($_POST['plugin']) : '';
97 $import_titles = isset($_POST['import_titles']) ? (bool) intval($_POST['import_titles']) : true;
98 $import_descriptions = isset($_POST['import_descriptions']) ? (bool) intval($_POST['import_descriptions']) : true;
99 $overwrite_existing = isset($_POST['overwrite_existing']) ? (bool) intval($_POST['overwrite_existing']) : false;
100 $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
101
102 if (empty($plugin)) {
103 wp_send_json_error(['message' => 'Missing required plugin parameter.']);
104 }
105
106 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-external-importer.php';
107 $importer = new Metasync_External_Importer($this->get_db_redirection());
108
109 $options = [
110 'import_titles' => $import_titles,
111 'import_descriptions' => $import_descriptions,
112 'overwrite_existing' => $overwrite_existing,
113 'batch_size' => 50,
114 'offset' => $offset
115 ];
116
117 $result = $importer->import_seo_metadata($plugin, $options);
118
119 if ($result['success']) {
120 wp_send_json_success($result);
121 } else {
122 wp_send_json_error($result);
123 }
124 }
125
126 public function lgSendCustomerParams()
127 {
128 check_ajax_referer('metasync_nonce', 'nonce');
129 if (!current_user_can('manage_options')) {
130 wp_send_json_error('Unauthorized');
131 return;
132 }
133 $sync_request = new Metasync_Sync_Requests();
134
135 # use the existing apikey for backward compatibility
136 $general_options = Metasync::get_option('general') ?? [];
137 $token = $general_options['apikey'] ?? null;
138
139 # get the response
140 $response = $sync_request->SyncCustomerParams($token);
141
142 // Check if response is a throttling error object
143 if (is_object($response) && isset($response->throttled) && $response->throttled === true) {
144 wp_send_json($response);
145 wp_die();
146 }
147
148 // Check if response is null/false (other error cases)
149 if ($response === null || $response === false) {
150 wp_send_json(['error' => 'Sync failed - no response from sync method', 'detail' => 'The sync method returned null or false']);
151 wp_die();
152 }
153
154 $responseBody = wp_remote_retrieve_body($response);
155 $responseCode = wp_remote_retrieve_response_code($response);
156
157 if ($responseCode == 200) {
158 $dt = new DateTime();
159 $send_auth_token_timestamp = Metasync::get_option();
160 $send_auth_token_timestamp['general']['send_auth_token_timestamp'] = $dt->format('M d, Y h:i:s A');;
161 Metasync::set_option($send_auth_token_timestamp);
162
163 Metasync_Heartbeat_Manager::instance()->update_heartbeat_cache_after_sync(true, 'Sync Now - successful data sync');
164
165 $result = json_decode($responseBody);
166 if ( ! is_object( $result ) ) {
167 $result = new stdClass();
168 }
169 $timestamp = Metasync::get_option('general')['send_auth_token_timestamp'] ?? '';
170 $result->send_auth_token_timestamp = $timestamp;
171 $result->send_auth_token_diffrence = Metasync_Settings_Fields::instance()->time_elapsed_string($timestamp);
172 wp_send_json($result);
173 wp_die();
174 } else {
175 Metasync_Heartbeat_Manager::instance()->update_heartbeat_cache_after_sync(false, 'Sync Now - failed data sync');
176 }
177
178 $result = json_decode($responseBody);
179 wp_send_json($result);
180 wp_die();
181 }
182
183 public function ajax_update_db_structure()
184 {
185 if (!Metasync::current_user_has_plugin_access()) {
186 wp_die('Insufficient permissions');
187 }
188
189 if (empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_update_db_nonce')) {
190 wp_die('Security check failed');
191 }
192
193 try {
194 $this->get_db_redirection()->force_table_update();
195
196 wp_send_json_success('Database structure updated successfully');
197 } catch (Exception $e) {
198 wp_send_json_error('Database update failed: ' . $e->getMessage());
199 }
200 }
201
202 public function ajax_save_wizard_progress()
203 {
204 check_ajax_referer('metasync_wizard', 'nonce');
205
206 if (!Metasync::current_user_has_plugin_access()) {
207 wp_send_json_error(array('message' => 'Insufficient permissions'));
208 }
209
210 $step = isset($_POST['step']) ? intval($_POST['step']) : 0;
211 $raw_data = isset($_POST['data']) ? $_POST['data'] : array();
212
213 // Allowlist top-level wizard data keys to prevent mass assignment
214 $allowed_wizard_keys = array('verification', 'seo_settings', 'schema');
215 $data = is_array($raw_data) ? array_intersect_key($raw_data, array_flip($allowed_wizard_keys)) : array();
216
217 $options = get_option('metasync_options', array());
218
219 if (isset($data['verification'])) {
220 if (!isset($options['general'])) {
221 $options['general'] = array();
222 }
223 $options['general']['google_verification'] = sanitize_text_field($data['verification']['google']);
224 $options['general']['bing_verification'] = sanitize_text_field($data['verification']['bing']);
225 }
226
227 if (isset($data['seo_settings'])) {
228 if (!isset($options['seo_controls'])) {
229 $options['seo_controls'] = array();
230 }
231
232 $options['seo_controls']['index_date_archives'] = $data['seo_settings']['date_archives'] ? 'false' : 'true';
233 $options['seo_controls']['index_author_archives'] = $data['seo_settings']['author_archives'] ? 'false' : 'true';
234 $options['seo_controls']['index_category_archives'] = $data['seo_settings']['category_archives'] ? 'false' : 'true';
235 $options['seo_controls']['index_tag_archives'] = $data['seo_settings']['tag_archives'] ? 'false' : 'true';
236 }
237
238 if (isset($data['schema'])) {
239 if (!isset($options['general'])) {
240 $options['general'] = array();
241 }
242 $options['general']['enable_schema_markup'] = $data['schema']['enabled'];
243 $options['general']['default_schema_type'] = sanitize_text_field($data['schema']['default_type']);
244 }
245
246 update_option('metasync_options', $options);
247
248 wp_send_json_success(array('message' => 'Progress saved'));
249 }
250
251 public function ajax_complete_wizard()
252 {
253 check_ajax_referer('metasync_wizard', 'nonce');
254
255 if (!Metasync::current_user_has_plugin_access()) {
256 wp_send_json_error(array('message' => 'Insufficient permissions'));
257 }
258
259 update_option('metasync_wizard_completed', array(
260 'completed' => true,
261 'completed_at' => current_time('mysql'),
262 'completed_by' => get_current_user_id(),
263 'version' => METASYNC_VERSION
264 ));
265
266 $user_id = get_current_user_id();
267 delete_transient("metasync_wizard_state_{$user_id}");
268
269 wp_send_json_success(array('message' => 'Wizard completed'));
270 }
271
272 public function ajax_validate_robots()
273 {
274 check_ajax_referer('metasync_nonce', 'nonce');
275 if (!Metasync::current_user_has_plugin_access()) {
276 wp_send_json_error('Insufficient permissions');
277 return;
278 }
279
280 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
281 $robots_txt = Metasync_Robots_Txt::get_instance();
282
283 $content = isset($_POST['content']) ? wp_unslash($_POST['content']) : '';
284 $validation = $robots_txt->validate_content($content);
285
286 wp_send_json_success($validation);
287 }
288
289 public function ajax_get_default_robots()
290 {
291 check_ajax_referer('metasync_nonce', 'nonce');
292 if (!Metasync::current_user_has_plugin_access()) {
293 wp_send_json_error('Insufficient permissions');
294 return;
295 }
296
297 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
298 $robots_txt = Metasync_Robots_Txt::get_instance();
299
300 wp_send_json_success(array(
301 'content' => $robots_txt->get_default_robots_content()
302 ));
303 }
304
305 public function ajax_preview_robots_backup()
306 {
307 check_ajax_referer('metasync_nonce', 'nonce');
308 if (!Metasync::current_user_has_plugin_access()) {
309 wp_send_json_error('Insufficient permissions');
310 return;
311 }
312
313 $backup_id = isset($_POST['backup_id']) ? intval($_POST['backup_id']) : 0;
314
315 if (!$backup_id) {
316 wp_send_json_error('Invalid backup ID');
317 return;
318 }
319
320 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt-database.php';
321 $database = Metasync_Robots_Txt_Database::get_instance();
322
323 $backup = $database->get_backup($backup_id);
324
325 if (!$backup) {
326 wp_send_json_error('Backup not found');
327 return;
328 }
329
330 wp_send_json_success(array(
331 'content' => $backup['content'],
332 'created_at' => $backup['created_at'],
333 'created_by_name' => isset($backup['created_by_name']) ? $backup['created_by_name'] : ''
334 ));
335 }
336
337 public function ajax_delete_robots_backup()
338 {
339 if (!Metasync::current_user_has_plugin_access()) {
340 wp_send_json_error(array('message' => esc_html__('Insufficient permissions', 'metasync')));
341 return;
342 }
343
344 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'metasync_delete_robots_backup')) {
345 wp_send_json_error(array('message' => esc_html__('Security check failed', 'metasync')));
346 return;
347 }
348
349 $backup_id = isset($_POST['backup_id']) ? intval($_POST['backup_id']) : 0;
350
351 if (!$backup_id) {
352 wp_send_json_error(array('message' => esc_html__('Invalid backup ID', 'metasync')));
353 return;
354 }
355
356 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
357 $robots_txt = Metasync_Robots_Txt::get_instance();
358
359 $result = $robots_txt->delete_backup($backup_id);
360
361 if ($result) {
362 wp_send_json_success(array('message' => esc_html__('Backup deleted successfully!', 'metasync')));
363 } else {
364 wp_send_json_error(array('message' => esc_html__('Failed to delete backup.', 'metasync')));
365 }
366 }
367
368 public function ajax_restore_robots_backup()
369 {
370 if (!Metasync::current_user_has_plugin_access()) {
371 wp_send_json_error(array('message' => esc_html__('Insufficient permissions', 'metasync')));
372 return;
373 }
374
375 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'metasync_restore_robots_backup')) {
376 wp_send_json_error(array('message' => esc_html__('Security check failed', 'metasync')));
377 return;
378 }
379
380 $backup_id = isset($_POST['backup_id']) ? intval($_POST['backup_id']) : 0;
381
382 if (!$backup_id) {
383 wp_send_json_error(array('message' => esc_html__('Invalid backup ID', 'metasync')));
384 return;
385 }
386
387 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
388 $robots_txt = Metasync_Robots_Txt::get_instance();
389
390 $result = $robots_txt->restore_backup($backup_id);
391
392 if (is_wp_error($result)) {
393 wp_send_json_error(array('message' => $result->get_error_message()));
394 } else {
395 $current_content = $robots_txt->read_robots_file();
396
397 if (is_wp_error($current_content)) {
398 wp_send_json_error(array('message' => $current_content->get_error_message()));
399 } else {
400 wp_send_json_success(array(
401 'message' => esc_html__('robots.txt restored from backup successfully!', 'metasync'),
402 'content' => $current_content
403 ));
404 }
405 }
406 }
407
408 public function ajax_create_redirect_from_404()
409 {
410 if (empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_404_redirect')) {
411 wp_die('Security check failed');
412 }
413
414 if (!Metasync::current_user_has_plugin_access()) {
415 wp_die('Insufficient permissions');
416 }
417
418 $uri = sanitize_text_field($_POST['uri']);
419 $redirect_url = sanitize_url($_POST['redirect_url']);
420
421 if (empty($uri) || empty($redirect_url)) {
422 wp_send_json_error('Missing required parameters');
423 }
424
425 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
426 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
427 $db_404 = new Metasync_Error_Monitor_Database();
428 $monitor_404 = new Metasync_Error_Monitor($db_404);
429
430 $result = $monitor_404->create_redirection_from_404($uri, $redirect_url, 'Created from 404 suggestion');
431
432 if ($result) {
433 wp_send_json_success('Redirect created successfully');
434 } else {
435 wp_send_json_error('Failed to create redirect');
436 }
437 }
438
439 public function ajax_test_host_blocking_get()
440 {
441 check_ajax_referer('metasync_nonce', 'nonce');
442 if (!Metasync::current_user_has_plugin_access()) {
443 wp_send_json_error('Insufficient permissions');
444 return;
445 }
446
447 $endpoint = 'https://wp-check.searchatlas.com/ping';
448 $start_time = microtime(true);
449
450 $response = wp_remote_get($endpoint, array(
451 'timeout' => 30,
452 'user-agent' => 'MetaSync Plugin Host Test',
453 'headers' => array(
454 'Accept' => 'application/json',
455 'Content-Type' => 'application/json',
456 'Origin' => home_url(),
457 'Referer' => admin_url(),
458 'X-WordPress-Site' => home_url()
459 )
460 ));
461
462 $end_time = microtime(true);
463 $response_time = round(($end_time - $start_time) * 1000, 2);
464
465 if (is_wp_error($response)) {
466 wp_send_json_success(array(
467 'method' => 'GET',
468 'status' => 'error',
469 'response_time' => $response_time . 'ms',
470 'error' => $response->get_error_message(),
471 'blocked' => true,
472 'details' => 'Request failed - possible blocking detected'
473 ));
474 } else {
475 $status_code = wp_remote_retrieve_response_code($response);
476 $body = wp_remote_retrieve_body($response);
477 $headers = wp_remote_retrieve_headers($response);
478
479 $is_blocked = false;
480 $status_text = 'success';
481 $details = 'GET request completed successfully';
482 $parsed_response = null;
483
484 if ($status_code === 200) {
485 $parsed_response = json_decode($body, true);
486
487 if ($parsed_response && isset($parsed_response['results']['get'])) {
488 $get_result = $parsed_response['results']['get'];
489 $get_status_code = isset($get_result['statusCode']) ? $get_result['statusCode'] : null;
490
491 if ($get_status_code !== 200) {
492 $is_blocked = true;
493 $status_text = 'error';
494 $details = "GET request to target site returned status code {$get_status_code} - host blocking detected";
495 }
496 } else {
497 $is_blocked = true;
498 $status_text = 'error';
499 $details = 'Unable to parse response structure - possible blocking or endpoint issue';
500 }
501 } else {
502 $is_blocked = true;
503 $status_text = 'error';
504 $details = "External endpoint returned status code {$status_code} - possible blocking detected";
505 }
506
507 wp_send_json_success(array(
508 'method' => 'GET',
509 'status' => $status_text,
510 'response_time' => $response_time . 'ms',
511 'status_code' => $status_code,
512 'body' => $body,
513 'headers' => $headers->getAll(),
514 'blocked' => $is_blocked,
515 'details' => $details,
516 'parsed_response' => $parsed_response
517 ));
518 }
519 }
520
521 public function ajax_test_host_blocking_post()
522 {
523 check_ajax_referer('metasync_nonce', 'nonce');
524 if (!Metasync::current_user_has_plugin_access()) {
525 wp_send_json_error('Insufficient permissions');
526 return;
527 }
528
529 $endpoint = 'https://wp-check.searchatlas.com/ping';
530 $start_time = microtime(true);
531
532 $test_data = array(
533 'test' => 'host_blocking_test',
534 'timestamp' => current_time('mysql'),
535 'source' => 'metasync_plugin',
536 'method' => 'POST'
537 );
538
539 $response = wp_remote_post($endpoint, array(
540 'timeout' => 30,
541 'user-agent' => 'MetaSync Plugin Host Test',
542 'headers' => array(
543 'Accept' => 'application/json',
544 'Content-Type' => 'application/json',
545 'Origin' => home_url(),
546 'Referer' => admin_url(),
547 'X-WordPress-Site' => home_url()
548 ),
549 'body' => json_encode($test_data)
550 ));
551
552 $end_time = microtime(true);
553 $response_time = round(($end_time - $start_time) * 1000, 2);
554
555 if (is_wp_error($response)) {
556 wp_send_json_success(array(
557 'method' => 'POST',
558 'status' => 'error',
559 'response_time' => $response_time . 'ms',
560 'error' => $response->get_error_message(),
561 'blocked' => true,
562 'details' => 'Request failed - possible blocking detected',
563 'sent_data' => $test_data
564 ));
565 } else {
566 $status_code = wp_remote_retrieve_response_code($response);
567 $body = wp_remote_retrieve_body($response);
568 $headers = wp_remote_retrieve_headers($response);
569
570 $is_blocked = false;
571 $status_text = 'success';
572 $details = 'POST request completed successfully';
573 $parsed_response = null;
574
575 if ($status_code === 200) {
576 $parsed_response = json_decode($body, true);
577
578 if ($parsed_response && isset($parsed_response['results']['post'])) {
579 $post_result = $parsed_response['results']['post'];
580 $post_status_code = isset($post_result['statusCode']) ? $post_result['statusCode'] : null;
581
582 if ($post_status_code !== 200) {
583 $is_blocked = true;
584 $status_text = 'error';
585 $details = "POST request to target site returned status code {$post_status_code} - host blocking detected";
586 }
587 } else {
588 $is_blocked = true;
589 $status_text = 'error';
590 $details = 'Unable to parse response structure - possible blocking or endpoint issue';
591 }
592 } else {
593 $is_blocked = true;
594 $status_text = 'error';
595 $details = "External endpoint returned status code {$status_code} - possible blocking detected";
596 }
597
598 wp_send_json_success(array(
599 'method' => 'POST',
600 'status' => $status_text,
601 'response_time' => $response_time . 'ms',
602 'status_code' => $status_code,
603 'body' => $body,
604 'headers' => $headers->getAll(),
605 'blocked' => $is_blocked,
606 'details' => $details,
607 'sent_data' => $test_data,
608 'parsed_response' => $parsed_response
609 ));
610 }
611 }
612
613 public function execute_transient_cleanup()
614 {
615 global $wpdb;
616
617 $cleanup_stats = array(
618 'expired_transients' => 0,
619 'plugin_transients' => 0,
620 'rate_limit_transients' => 0,
621 'telemetry_transients' => 0,
622 'start_time' => microtime(true)
623 );
624
625 try {
626 delete_expired_transients(true);
627 $cleanup_stats['expired_transients'] = 'cleaned_by_wordpress';
628
629 $plugin_transients = $wpdb->get_results(
630 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_%'",
631 ARRAY_A
632 );
633
634 foreach ($plugin_transients as $transient) {
635 $transient_name = str_replace('_transient_', '', $transient['option_name']);
636 delete_transient($transient_name);
637 $cleanup_stats['plugin_transients']++;
638 }
639
640 $rate_limit_transients = $wpdb->get_results(
641 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
642 ARRAY_A
643 );
644
645 foreach ($rate_limit_transients as $transient) {
646 $transient_name = str_replace('_transient_', '', $transient['option_name']);
647 delete_transient($transient_name);
648 $cleanup_stats['rate_limit_transients']++;
649 }
650
651 $telemetry_transients = $wpdb->get_results(
652 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_telemetry_%'",
653 ARRAY_A
654 );
655
656 foreach ($telemetry_transients as $transient) {
657 $transient_name = str_replace('_transient_', '', $transient['option_name']);
658 delete_transient($transient_name);
659 $cleanup_stats['telemetry_transients']++;
660 }
661
662 $sa_connect_success_transients = $wpdb->get_results(
663 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_sa_connect_success_%'",
664 ARRAY_A
665 );
666
667 foreach ($sa_connect_success_transients as $transient) {
668 $transient_name = str_replace('_transient_', '', $transient['option_name']);
669 delete_transient($transient_name);
670 $cleanup_stats['sa_connect_success_transients'] = ($cleanup_stats['sa_connect_success_transients'] ?? 0) + 1;
671 }
672
673 $cleanup_stats['execution_time'] = round((microtime(true) - $cleanup_stats['start_time']) * 1000, 2);
674 $cleanup_stats['next_run'] = wp_next_scheduled('metasync_cleanup_transients') ?
675 date('Y-m-d H:i:s T', wp_next_scheduled('metasync_cleanup_transients')) : 'N/A';
676
677 error_log('MetaSync: Transient cleanup completed - ' . json_encode($cleanup_stats));
678
679 } catch (Exception $e) {
680 error_log('MetaSync: Transient cleanup failed - ' . $e->getMessage());
681 }
682 }
683
684 public function ajax_submit_issue_report()
685 {
686 try {
687 # Verify nonce
688 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_report_issue')) {
689 wp_send_json_error(array('message' => 'Security verification failed.'));
690 return;
691 }
692
693 # Check user capabilities
694 if (!Metasync::current_user_has_plugin_access()) {
695 wp_send_json_error(array('message' => 'Insufficient permissions.'));
696 return;
697 }
698
699 # Get and validate form data
700 $issue_message = isset($_POST['issue_message']) ? sanitize_textarea_field(wp_unslash($_POST['issue_message'])) : '';
701 $issue_severity = isset($_POST['issue_severity']) ? sanitize_text_field(wp_unslash($_POST['issue_severity'])) : 'warning';
702 $include_user_info = isset($_POST['include_user_info']) && sanitize_text_field(wp_unslash($_POST['include_user_info'])) === 'true';
703
704 # Validate severity level
705 $valid_severity_levels = array('info', 'warning', 'error', 'fatal');
706 if (!in_array($issue_severity, $valid_severity_levels, true)) {
707 $issue_severity = 'warning';
708 }
709
710 # Validate message length
711 if (empty($issue_message) || strlen($issue_message) < 10) {
712 wp_send_json_error(array('message' => 'Please provide a more detailed description (at least 10 characters).'));
713 return;
714 }
715 if (strlen($issue_message) > 1000) {
716 wp_send_json_error(array('message' => 'Message is too long. Please limit to 1000 characters.'));
717 return;
718 }
719
720 # Handle file upload if present
721 $attachment = null;
722 if (!empty($_FILES['issue_attachment']['tmp_name'])) {
723 # Validate file type using server-side MIME detection (not client-supplied type)
724 $tmp_name = $_FILES['issue_attachment']['tmp_name'];
725 $filename = sanitize_file_name($_FILES['issue_attachment']['name']);
726 $file_info = wp_check_filetype_and_ext($tmp_name, $filename);
727 if (!$file_info['ext']) {
728 wp_send_json_error(array('message' => 'Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.'));
729 return;
730 }
731 $allowed_extensions = array('jpg', 'jpeg', 'png', 'gif', 'webp');
732 if (!in_array($file_info['ext'], $allowed_extensions, true)) {
733 wp_send_json_error(array('message' => 'Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.'));
734 return;
735 }
736 $file_type = $file_info['type'];
737
738 # Validate file size (5MB max)
739 $max_size = 5 * 1024 * 1024; // 5MB
740 if ($_FILES['issue_attachment']['size'] > $max_size) {
741 wp_send_json_error(array('message' => 'File size exceeds 5MB. Please choose a smaller file.'));
742 return;
743 }
744
745 # Read file contents
746 $file_contents = file_get_contents($tmp_name);
747 if ($file_contents !== false) {
748 $attachment = array(
749 'filename' => $filename,
750 'data' => $file_contents,
751 'content_type' => $file_type
752 );
753 }
754 }
755
756 # Get general options (same way as used throughout the plugin)
757 $general_options = Metasync::get_option('general');
758 if (!is_array($general_options)) {
759 $general_options = array();
760 }
761
762 $project_uuid = isset($general_options['otto_pixel_uuid']) ? sanitize_text_field($general_options['otto_pixel_uuid']) : '';
763
764 # Always use standardized title format for Sentry prioritization
765 $issue_title = !empty($project_uuid) ? 'Client Report ' . $project_uuid : 'Client Report (UUID Not Configured)';
766
767 # Collect system information with error handling
768 $active_plugins = get_option('active_plugins');
769 $plugin_count = is_array($active_plugins) ? count($active_plugins) : 0;
770
771 $active_theme = wp_get_theme();
772 $theme_name = is_object($active_theme) ? $active_theme->get('Name') : get_template();
773
774 $system_context = array(
775 'report_type' => 'manual_client_report',
776 'website_url' => esc_url_raw(home_url()),
777 'site_title' => sanitize_text_field(get_bloginfo('name')),
778 'admin_email' => sanitize_email(get_bloginfo('admin_email')),
779 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0',
780 'plugin_name' => 'Search Engine Labs SEO (MetaSync)',
781 'wordpress_version' => get_bloginfo('version'),
782 'php_version' => PHP_VERSION,
783 'active_theme' => $theme_name,
784 'memory_limit' => ini_get('memory_limit'),
785 'multisite' => is_multisite(),
786 'project_uuid' => $project_uuid,
787 'active_plugins' => $plugin_count,
788 'report_timestamp' => current_time('mysql'),
789 'severity_level' => $issue_severity
790 );
791
792 # Add user information if requested
793 if ($include_user_info) {
794 $current_user = wp_get_current_user();
795 if ($current_user && $current_user->ID > 0) {
796 $system_context['reporter'] = array(
797 'username' => sanitize_user($current_user->user_login),
798 'email' => sanitize_email($current_user->user_email),
799 'display_name' => sanitize_text_field($current_user->display_name),
800 'roles' => is_array($current_user->roles) ? $current_user->roles : array()
801 );
802 }
803 }
804
805 # Send to Sentry using User Feedback API
806 $sent_to_sentry = false;
807
808 # Check if Sentry feedback function exists
809 if (!function_exists('metasync_sentry_capture_feedback')) {
810 # Log warning if function doesn't exist
811 if (defined('WP_DEBUG') && WP_DEBUG) {
812 error_log('MetaSync: Sentry feedback function not available for report submission.');
813 }
814 } else {
815 $feedback_data = array(
816 'message' => $issue_message,
817 'severity' => $issue_severity
818 );
819
820 # Add user information if requested
821 if ($include_user_info) {
822 $current_user = wp_get_current_user();
823 if ($current_user && $current_user->ID > 0) {
824 $feedback_data['name'] = sanitize_text_field($current_user->display_name);
825 $feedback_data['email'] = sanitize_email($current_user->user_email);
826 }
827 }
828
829 $sent_to_sentry = metasync_sentry_capture_feedback($feedback_data, $attachment);
830 }
831
832 if ($sent_to_sentry) {
833 wp_send_json_success(array(
834 'message' => 'Report submitted successfully! Our team will review it shortly.',
835 'project_uuid' => $project_uuid,
836 'report_title' => esc_html($issue_title)
837 ));
838 } else {
839 # Fallback: Log locally if Sentry fails or is unavailable
840 if (defined('WP_DEBUG') && WP_DEBUG) {
841 error_log(sprintf(
842 'MetaSync Client Report (Fallback): UUID: %s | Title: %s | Message: %s | Severity: %s',
843 $project_uuid,
844 $issue_title,
845 $issue_message,
846 $issue_severity
847 ));
848 }
849
850 wp_send_json_success(array(
851 'message' => 'Report logged locally. Note: Remote reporting may be unavailable.',
852 'project_uuid' => $project_uuid,
853 'report_title' => esc_html($issue_title),
854 'fallback' => true
855 ));
856 }
857
858 } catch (Exception $e) {
859 # Log the error securely (only in debug mode)
860 if (defined('WP_DEBUG') && WP_DEBUG) {
861 error_log(sprintf(
862 'MetaSync Report Submission Error: %s in %s on line %d',
863 $e->getMessage(),
864 $e->getFile(),
865 $e->getLine()
866 ));
867 }
868
869 # Send generic error message to client
870 wp_send_json_error(array('message' => 'Failed to submit report. Please try again later.'));
871 }
872 }
873
874 public function ajax_recover_password()
875 {
876 try {
877 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_recover_password_nonce')) {
878 wp_send_json_error(array('message' => 'Security verification failed.'));
879 return;
880 }
881
882 $whitelabel_settings = Metasync::get_whitelabel_settings();
883 $password = $whitelabel_settings['settings_password'] ?? '';
884 $recovery_email = $whitelabel_settings['recovery_email'] ?? '';
885
886 if (empty($password)) {
887 wp_send_json_error(array('message' => 'No password is configured for recovery.'));
888 return;
889 }
890
891 if (empty($recovery_email) || !is_email($recovery_email)) {
892 wp_send_json_error(array('message' => 'No valid recovery email is configured. Please contact your administrator.'));
893 return;
894 }
895
896 $site_name = get_bloginfo('name');
897 $site_url = home_url();
898 $plugin_name = Metasync::get_effective_plugin_name('');
899 $settings_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=whitelabel');
900 $to = $recovery_email;
901
902 $subject = sprintf('[%s] Settings Password Recovery', $site_name);
903
904 $message = sprintf(
905 "Hello,\n\n" .
906 "A password recovery request was made for the %s settings on %s.\n\n" .
907 "Your Settings Password is:\n%s\n\n" .
908 "You can use this password to access the protected settings at:\n%s\n\n" .
909 "If you did not request this password recovery, please secure your WordPress admin account immediately.\n\n" .
910 "---\n" .
911 "This is an automated message from %s\n%s",
912 $plugin_name,
913 $site_name,
914 $password,
915 $settings_url,
916 $site_name,
917 $site_url
918 );
919
920 $from_name = !empty($whitelabel_settings['company_name'])
921 ? $whitelabel_settings['company_name']
922 : $site_name;
923
924 $headers = array(
925 'Content-Type: text/plain; charset=UTF-8',
926 sprintf('From: %s <%s>', $from_name, get_option('admin_email'))
927 );
928
929 $mail_error = '';
930 add_action('wp_mail_failed', function($error) use (&$mail_error) {
931 $mail_error = $error->get_error_message();
932 });
933
934 $sent = wp_mail($to, $subject, $message, $headers);
935
936 if ($sent) {
937 wp_send_json_success(array(
938 'message' => sprintf('Password recovery email sent to %s', esc_html($recovery_email))
939 ));
940 } else {
941 $error_message = 'Failed to send recovery email. ';
942 if (!empty($mail_error)) {
943 $error_message .= 'Error: ' . $mail_error;
944 } else {
945 $error_message .= 'Your server may not be configured to send emails. Please check your email configuration or contact your administrator.';
946 }
947
948 wp_send_json_error(array('message' => $error_message));
949 }
950
951 } catch (Exception $e) {
952 if (defined('WP_DEBUG') && WP_DEBUG) {
953 error_log(sprintf(
954 'MetaSync Password Recovery Error: %s in %s on line %d',
955 $e->getMessage(),
956 $e->getFile(),
957 $e->getLine()
958 ));
959 }
960
961 wp_send_json_error(array('message' => 'An error occurred while processing your request. Please try again later.'));
962 }
963 }
964
965 public function ajax_save_theme()
966 {
967 try {
968 # Verify nonce for security
969 if (!isset($_POST['_ajax_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_ajax_nonce'])), 'metasync_theme_nonce')) {
970 wp_send_json_error(array('message' => 'Security verification failed.'));
971 return;
972 }
973
974 # Check user capabilities
975 if (!Metasync::current_user_has_plugin_access()) {
976 wp_send_json_error(array('message' => 'Insufficient permissions.'));
977 return;
978 }
979
980 # Get and validate theme value
981 $theme = isset($_POST['theme']) ? sanitize_text_field(wp_unslash($_POST['theme'])) : '';
982
983 # Validate theme is either 'light' or 'dark'
984 if (!in_array($theme, array('light', 'dark'), true)) {
985 wp_send_json_error(array('message' => 'Invalid theme value.'));
986 return;
987 }
988
989 # Save theme preference to WordPress options
990 update_option('metasync_theme', $theme, true);
991
992 # Send success response
993 wp_send_json_success(array(
994 'message' => 'Theme preference saved successfully.',
995 'theme' => $theme
996 ));
997
998 } catch (Exception $e) {
999 # Log error if debug is enabled
1000 if (defined('WP_DEBUG') && WP_DEBUG) {
1001 error_log('MetaSync Theme Save Error: ' . $e->getMessage());
1002 }
1003
1004 wp_send_json_error(array('message' => 'Failed to save theme preference.'));
1005 }
1006 }
1007
1008 public function ajax_track_one_click_activation()
1009 {
1010 check_ajax_referer('metasync_nonce', 'nonce');
1011 # Check user capabilities
1012 if (!Metasync::current_user_has_plugin_access()) {
1013 wp_send_json_error(['message' => 'Insufficient permissions']);
1014 return;
1015 }
1016
1017 # Get parameters
1018 $auth_method = isset($_POST['auth_method']) ? sanitize_text_field(wp_unslash($_POST['auth_method'])) : 'searchatlas_connect';
1019 $is_reconnection = isset($_POST['is_reconnection']) ? filter_var($_POST['is_reconnection'], FILTER_VALIDATE_BOOLEAN) : false;
1020
1021 # Track the event in GA4
1022 try {
1023 Metasync_GA4::get_instance()->track_one_click_activation($auth_method, $is_reconnection);
1024
1025 wp_send_json_success([
1026 'message' => '1-click activation tracked successfully',
1027 'auth_method' => $auth_method,
1028 'is_reconnection' => $is_reconnection
1029 ]);
1030 } catch (Exception $e) {
1031 # Still return success to avoid breaking the auth flow
1032 wp_send_json_success([
1033 'message' => 'Authentication successful'
1034 ]);
1035 }
1036 }
1037
1038 public function handle_export_whitelabel_settings()
1039 {
1040 # Check user capabilities
1041 if (!Metasync::current_user_has_plugin_access()) {
1042 wp_die('Insufficient permissions');
1043 }
1044
1045 # Verify nonce for security (check both GET and POST)
1046 $nonce = '';
1047 if (isset($_POST['_wpnonce'])) {
1048 $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce']));
1049 } elseif (isset($_GET['_wpnonce'])) {
1050 $nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce']));
1051 }
1052
1053 if (empty($nonce) || !wp_verify_nonce($nonce, 'metasync_export_whitelabel')) {
1054 wp_die('Security verification failed.');
1055 }
1056
1057 try {
1058 # Get all whitelabel settings
1059 $whitelabel_settings = Metasync::get_whitelabel_settings();
1060
1061 # Get general settings that relate to whitelabel
1062 $general_settings = Metasync::get_option('general');
1063 $whitelabel_related_general = array();
1064
1065 # Include ALL whitelabel-related general settings
1066 $whitelabel_keys = array(
1067 'white_label_plugin_name',
1068 'white_label_plugin_description',
1069 'white_label_plugin_author',
1070 'white_label_plugin_author_uri',
1071 'white_label_plugin_uri',
1072 'white_label_plugin_menu_slug',
1073 'white_label_plugin_menu_icon',
1074 'whitelabel_otto_name'
1075 );
1076
1077 foreach ($whitelabel_keys as $key) {
1078 if (isset($general_settings[$key])) {
1079 $whitelabel_related_general[$key] = $general_settings[$key];
1080 }
1081 }
1082
1083 # Bundle the menu icon file so it survives import on a different site.
1084 # If the icon is a local URL (media library), replace it with a special
1085 # marker and include the actual file in the ZIP under the plugin folder.
1086 $bundled_icon_filename = null;
1087 $icon_url = $whitelabel_related_general['white_label_plugin_menu_icon'] ?? '';
1088 if (!empty($icon_url) && filter_var($icon_url, FILTER_VALIDATE_URL)) {
1089 $site_url = trailingslashit(site_url());
1090 if (strpos($icon_url, $site_url) === 0) {
1091 // Resolve URL to an absolute filesystem path
1092 $relative_path = str_replace($site_url, ABSPATH, $icon_url);
1093 $icon_abs_path = realpath($relative_path);
1094 if ($icon_abs_path && file_exists($icon_abs_path)) {
1095 $ext = strtolower(pathinfo($icon_abs_path, PATHINFO_EXTENSION));
1096 $bundled_icon_filename = 'whitelabel-icon.' . $ext;
1097 // Replace the URL with a marker so the importer knows to restore it
1098 $whitelabel_related_general['white_label_plugin_menu_icon'] = '__bundled_icon__' . $ext;
1099 }
1100 }
1101 }
1102
1103 # Prepare export data
1104 $export_data = array(
1105 'version' => '1.0',
1106 'exported_at' => current_time('mysql'),
1107 'whitelabel_settings' => $whitelabel_settings,
1108 'general_settings' => $whitelabel_related_general
1109 );
1110
1111 # Convert to JSON
1112 $json_data = wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1113
1114 if ($json_data === false) {
1115 wp_die('Failed to encode settings to JSON.');
1116 }
1117
1118 # Check if ZipArchive is available
1119 if (!class_exists('ZipArchive')) {
1120 wp_die('ZipArchive class is not available. Please enable PHP zip extension.');
1121 }
1122
1123 # Get plugin directory path (remove trailing slash for basename)
1124 $plugin_dir = rtrim(plugin_dir_path(dirname(__FILE__)), '/');
1125 $plugin_folder_name = basename($plugin_dir);
1126
1127 # Create temporary directory for zip file
1128 $upload_dir = wp_upload_dir();
1129 $temp_dir = $upload_dir['basedir'] . '/metasync-export-temp';
1130
1131 # Create temp directory if it doesn't exist
1132 if (!file_exists($temp_dir)) {
1133 wp_mkdir_p($temp_dir);
1134 }
1135
1136 # Generate unique filename
1137 $timestamp = date('Y-m-d_H-i-s');
1138 $zip_filename = 'metasync-whitelabel-plugin-' . $timestamp . '.zip';
1139 $json_filename = 'whitelabel-settings.json';
1140 $zip_path = $temp_dir . '/' . $zip_filename;
1141
1142 # Create zip file
1143 $zip = new ZipArchive();
1144 if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
1145 wp_die('Failed to create zip file.');
1146 }
1147
1148 # Add whitelabel settings JSON file to zip (inside plugin folder)
1149 $zip->addFromString($plugin_folder_name . '/' . $json_filename, $json_data);
1150
1151 # Bundle icon file if one was detected
1152 if ($bundled_icon_filename !== null && isset($icon_abs_path) && file_exists($icon_abs_path)) {
1153 $zip->addFile($icon_abs_path, $plugin_folder_name . '/' . $bundled_icon_filename);
1154 }
1155
1156
1157 # Files and directories to exclude from the zip
1158 $exclude_patterns = array(
1159 '.git',
1160 '.gitignore',
1161 '.gitattributes',
1162 'node_modules',
1163 '.DS_Store',
1164 'Thumbs.db',
1165 '.idea',
1166 '.vscode',
1167 'composer.lock',
1168 'package-lock.json',
1169 'yarn.lock',
1170 '.env',
1171 '.env.local',
1172 'docker-compose.yml',
1173 'Dockerfile',
1174 'Makefile',
1175 'renovate.json',
1176 'sonar-project.properties',
1177 'CODEOWNERS',
1178 'metasync-export-temp',
1179 'whitelabel-settings.json'
1180 );
1181
1182 # Recursively add plugin files to zip (with plugin folder as root in zip)
1183 self::add_directory_to_zip($zip, $plugin_dir . '/', $plugin_folder_name . '/', $exclude_patterns);
1184
1185 $zip->close();
1186
1187 # Check if file was created
1188 if (!file_exists($zip_path)) {
1189 wp_die('Zip file was not created successfully.');
1190 }
1191
1192 # Set headers for file download
1193 header('Content-Type: application/zip');
1194 header('Content-Disposition: attachment; filename="' . $zip_filename . '"');
1195 header('Content-Length: ' . filesize($zip_path));
1196 header('Pragma: no-cache');
1197 header('Expires: 0');
1198
1199 # Output file and clean up
1200 readfile($zip_path);
1201 unlink($zip_path);
1202
1203 # Clean up temp directory if empty
1204 if (is_dir($temp_dir) && count(scandir($temp_dir)) == 2) {
1205 rmdir($temp_dir);
1206 }
1207
1208 # Exit to prevent WordPress from adding anything to the response
1209 exit;
1210
1211 } catch (Exception $e) {
1212 # Log error if debug is enabled
1213 if (defined('WP_DEBUG') && WP_DEBUG) {
1214 error_log('MetaSync Whitelabel Export Error: ' . $e->getMessage());
1215 }
1216
1217 wp_die('Failed to export whitelabel settings: ' . $e->getMessage());
1218 }
1219 }
1220
1221 private static function add_directory_to_zip($zip, $dir, $zip_path = '', $exclude_patterns = array())
1222 {
1223 if (!is_dir($dir)) {
1224 return;
1225 }
1226
1227 $files = scandir($dir);
1228
1229 foreach ($files as $file) {
1230 if ($file === '.' || $file === '..') {
1231 continue;
1232 }
1233
1234 $file_path = $dir . $file;
1235 $zip_file_path = $zip_path . $file;
1236
1237 $should_exclude = false;
1238 foreach ($exclude_patterns as $pattern) {
1239 if (strpos($file, $pattern) !== false || strpos($file_path, $pattern) !== false) {
1240 $should_exclude = true;
1241 break;
1242 }
1243 }
1244
1245 if ($should_exclude) {
1246 continue;
1247 }
1248
1249 if (is_dir($file_path)) {
1250 $zip->addEmptyDir($zip_file_path);
1251 self::add_directory_to_zip($zip, $file_path . '/', $zip_file_path . '/', $exclude_patterns);
1252 } else {
1253 if (file_exists($file_path) && is_readable($file_path)) {
1254 $zip->addFile($file_path, $zip_file_path);
1255 }
1256 }
1257 }
1258 }
1259
1260 public function render_html_pages_dashboard_widget()
1261 {
1262 global $wpdb;
1263
1264 $query = "
1265 SELECT p.ID, p.post_title, p.post_modified, p.post_type
1266 FROM {$wpdb->posts} p
1267 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
1268 WHERE p.post_status = 'publish'
1269 AND p.post_type IN ('post', 'page')
1270 AND (pm.meta_key = '_metasync_raw_html_enabled' OR pm.meta_key = '_metasync_custom_css')
1271 GROUP BY p.ID
1272 ORDER BY p.post_modified DESC
1273 LIMIT 10
1274 ";
1275
1276 $html_pages = $wpdb->get_results($query);
1277 $total_count = count($html_pages);
1278
1279 $label = $this->get_html_source_label();
1280
1281 if (empty($html_pages)) {
1282 echo '<div class="metasync-dashboard-widget-empty">';
1283 echo '<span class="dashicons dashicons-admin-page" style="font-size: 48px; opacity: 0.3; display: block; margin: 20px auto;"></span>';
1284 echo '<p style="text-align: center; color: #666;">';
1285 echo sprintf(__('No pages created with %s yet.', 'metasync'), '<strong>' . esc_html($label) . '</strong>');
1286 echo '</p>';
1287 echo '<p style="text-align: center;">';
1288 echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-primary">';
1289 echo __('Get Started', 'metasync');
1290 echo '</a>';
1291 echo '</p>';
1292 echo '</div>';
1293 return;
1294 }
1295
1296 echo '<div class="metasync-dashboard-widget">';
1297
1298 echo '<div class="metasync-widget-stats">';
1299 echo '<div class="metasync-stat-box">';
1300 echo '<span class="metasync-stat-number">' . $total_count . '</span>';
1301 echo '<span class="metasync-stat-label">' . __('AI-Generated Pages', 'metasync') . '</span>';
1302 echo '</div>';
1303 echo '</div>';
1304
1305 echo '<div class="metasync-widget-list">';
1306 echo '<h4>' . __('Recent Pages', 'metasync') . '</h4>';
1307 echo '<ul>';
1308
1309 foreach ($html_pages as $page) {
1310 $edit_link = get_edit_post_link($page->ID);
1311 $view_link = get_permalink($page->ID);
1312 $time_ago = human_time_diff(strtotime($page->post_modified), current_time('timestamp'));
1313
1314 echo '<li class="metasync-widget-page-item">';
1315 echo '<span class="metasync-page-icon">⚡</span>';
1316 echo '<div class="metasync-page-details">';
1317 echo '<a href="' . esc_url($edit_link) . '" class="metasync-page-title">';
1318 echo esc_html($page->post_title ?: __('(no title)', 'metasync'));
1319 echo '</a>';
1320 echo '<span class="metasync-page-meta">';
1321 echo sprintf(__('Updated %s ago', 'metasync'), $time_ago);
1322 echo '';
1323 echo '<a href="' . esc_url($view_link) . '" target="_blank">' . __('View', 'metasync') . '</a>';
1324 echo '</span>';
1325 echo '</div>';
1326 echo '</li>';
1327 }
1328
1329 echo '</ul>';
1330 echo '</div>';
1331
1332 echo '<div class="metasync-widget-footer">';
1333 echo '<a href="' . admin_url('edit.php?post_type=page') . '">';
1334 echo __('View All Pages', 'metasync') . '';
1335 echo '</a>';
1336 echo '</div>';
1337
1338 echo '</div>';
1339 }
1340
1341 private function get_html_source_label()
1342 {
1343 $whitelabel_company = Metasync::get_whitelabel_company_name();
1344 if (!empty($whitelabel_company)) {
1345 return $whitelabel_company . ' AI';
1346 }
1347
1348 return Metasync::get_effective_plugin_name() . ' AI';
1349 }
1350 }
1351