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

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