PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.11
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.11
2.7.1 2.7.2 2.7.0 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 All 141 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.11, at includes/class-metasync-admin-ajax.php

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