PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.10
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.10
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.10, at includes/class-metasync-admin-ajax.php

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