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

1,411 lines 57.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 # 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_get_robots_backups()
421 {
422 if (!Metasync::current_user_has_plugin_access()) {
423 wp_send_json_error(array('message' => esc_html__('Insufficient permissions', 'metasync')));
424 return;
425 }
426
427 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'metasync_get_robots_backups')) {
428 wp_send_json_error(array('message' => esc_html__('Security check failed', 'metasync')));
429 return;
430 }
431
432 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
433 $robots_txt = Metasync_Robots_Txt::get_instance();
434
435 $backup_per_page = Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE;
436 $backup_total = $robots_txt->get_backup_count();
437 $backup_total_pages = max(1, (int) ceil($backup_total / $backup_per_page));
438
439 $backup_page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
440 if ($backup_page > $backup_total_pages) {
441 $backup_page = $backup_total_pages;
442 }
443
444 $offset = ($backup_page - 1) * $backup_per_page;
445 $backups = $robots_txt->get_backup_history($backup_per_page, $offset);
446
447 // Render the shared list + pagination partial to a string.
448 ob_start();
449 require plugin_dir_path(dirname(__FILE__)) . 'robots-txt/views/backup-history.php';
450 $html = ob_get_clean();
451
452 wp_send_json_success(array(
453 'html' => $html,
454 'total' => $backup_total,
455 'page' => $backup_page,
456 'total_pages' => $backup_total_pages,
457 ));
458 }
459
460 public function ajax_create_redirect_from_404()
461 {
462 if (empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_404_redirect')) {
463 wp_die('Security check failed');
464 }
465
466 if (!Metasync::current_user_has_plugin_access()) {
467 wp_die('Insufficient permissions');
468 }
469
470 $uri = sanitize_text_field($_POST['uri']);
471 $redirect_url = sanitize_url($_POST['redirect_url']);
472
473 if (empty($uri) || empty($redirect_url)) {
474 wp_send_json_error('Missing required parameters');
475 }
476
477 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
478 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
479 $db_404 = new Metasync_Error_Monitor_Database();
480 $monitor_404 = new Metasync_Error_Monitor($db_404);
481
482 $result = $monitor_404->create_redirection_from_404($uri, $redirect_url, 'Created from 404 suggestion');
483
484 if ($result) {
485 wp_send_json_success('Redirect created successfully');
486 } else {
487 wp_send_json_error('Failed to create redirect');
488 }
489 }
490
491 public function ajax_test_host_blocking_get()
492 {
493 check_ajax_referer('metasync_nonce', 'nonce');
494 if (!Metasync::current_user_has_plugin_access()) {
495 wp_send_json_error('Insufficient permissions');
496 return;
497 }
498
499 $endpoint = 'https://wp-check.searchatlas.com/ping';
500 $start_time = microtime(true);
501
502 $response = wp_remote_get($endpoint, array(
503 'timeout' => 30,
504 'user-agent' => 'MetaSync Plugin Host Test',
505 'headers' => array(
506 'Accept' => 'application/json',
507 'Content-Type' => 'application/json',
508 'Origin' => home_url(),
509 'Referer' => admin_url(),
510 'X-WordPress-Site' => home_url()
511 )
512 ));
513
514 $end_time = microtime(true);
515 $response_time = round(($end_time - $start_time) * 1000, 2);
516
517 if (is_wp_error($response)) {
518 wp_send_json_success(array(
519 'method' => 'GET',
520 'status' => 'error',
521 'response_time' => $response_time . 'ms',
522 'error' => $response->get_error_message(),
523 'blocked' => true,
524 'details' => 'Request failed - possible blocking detected'
525 ));
526 } else {
527 $status_code = wp_remote_retrieve_response_code($response);
528 $body = wp_remote_retrieve_body($response);
529 $headers = wp_remote_retrieve_headers($response);
530
531 $is_blocked = false;
532 $status_text = 'success';
533 $details = 'GET request completed successfully';
534 $parsed_response = null;
535
536 if ($status_code === 200) {
537 $parsed_response = json_decode($body, true);
538
539 if ($parsed_response && isset($parsed_response['results']['get'])) {
540 $get_result = $parsed_response['results']['get'];
541 $get_status_code = isset($get_result['statusCode']) ? $get_result['statusCode'] : null;
542
543 if ($get_status_code !== 200) {
544 $is_blocked = true;
545 $status_text = 'error';
546 $details = "GET request to target site returned status code {$get_status_code} - host blocking detected";
547 }
548 } else {
549 $is_blocked = true;
550 $status_text = 'error';
551 $details = 'Unable to parse response structure - possible blocking or endpoint issue';
552 }
553 } else {
554 $is_blocked = true;
555 $status_text = 'error';
556 $details = "External endpoint returned status code {$status_code} - possible blocking detected";
557 }
558
559 wp_send_json_success(array(
560 'method' => 'GET',
561 'status' => $status_text,
562 'response_time' => $response_time . 'ms',
563 'status_code' => $status_code,
564 'body' => $body,
565 'headers' => $headers->getAll(),
566 'blocked' => $is_blocked,
567 'details' => $details,
568 'parsed_response' => $parsed_response
569 ));
570 }
571 }
572
573 public function ajax_test_host_blocking_post()
574 {
575 check_ajax_referer('metasync_nonce', 'nonce');
576 if (!Metasync::current_user_has_plugin_access()) {
577 wp_send_json_error('Insufficient permissions');
578 return;
579 }
580
581 $endpoint = 'https://wp-check.searchatlas.com/ping';
582 $start_time = microtime(true);
583
584 $test_data = array(
585 'test' => 'host_blocking_test',
586 'timestamp' => current_time('mysql'),
587 'source' => 'metasync_plugin',
588 'method' => 'POST'
589 );
590
591 $response = wp_remote_post($endpoint, array(
592 'timeout' => 30,
593 'user-agent' => 'MetaSync Plugin Host Test',
594 'headers' => array(
595 'Accept' => 'application/json',
596 'Content-Type' => 'application/json',
597 'Origin' => home_url(),
598 'Referer' => admin_url(),
599 'X-WordPress-Site' => home_url()
600 ),
601 'body' => json_encode($test_data)
602 ));
603
604 $end_time = microtime(true);
605 $response_time = round(($end_time - $start_time) * 1000, 2);
606
607 if (is_wp_error($response)) {
608 wp_send_json_success(array(
609 'method' => 'POST',
610 'status' => 'error',
611 'response_time' => $response_time . 'ms',
612 'error' => $response->get_error_message(),
613 'blocked' => true,
614 'details' => 'Request failed - possible blocking detected',
615 'sent_data' => $test_data
616 ));
617 } else {
618 $status_code = wp_remote_retrieve_response_code($response);
619 $body = wp_remote_retrieve_body($response);
620 $headers = wp_remote_retrieve_headers($response);
621
622 $is_blocked = false;
623 $status_text = 'success';
624 $details = 'POST request completed successfully';
625 $parsed_response = null;
626
627 if ($status_code === 200) {
628 $parsed_response = json_decode($body, true);
629
630 if ($parsed_response && isset($parsed_response['results']['post'])) {
631 $post_result = $parsed_response['results']['post'];
632 $post_status_code = isset($post_result['statusCode']) ? $post_result['statusCode'] : null;
633
634 if ($post_status_code !== 200) {
635 $is_blocked = true;
636 $status_text = 'error';
637 $details = "POST request to target site returned status code {$post_status_code} - host blocking detected";
638 }
639 } else {
640 $is_blocked = true;
641 $status_text = 'error';
642 $details = 'Unable to parse response structure - possible blocking or endpoint issue';
643 }
644 } else {
645 $is_blocked = true;
646 $status_text = 'error';
647 $details = "External endpoint returned status code {$status_code} - possible blocking detected";
648 }
649
650 wp_send_json_success(array(
651 'method' => 'POST',
652 'status' => $status_text,
653 'response_time' => $response_time . 'ms',
654 'status_code' => $status_code,
655 'body' => $body,
656 'headers' => $headers->getAll(),
657 'blocked' => $is_blocked,
658 'details' => $details,
659 'sent_data' => $test_data,
660 'parsed_response' => $parsed_response
661 ));
662 }
663 }
664
665 public function execute_transient_cleanup()
666 {
667 global $wpdb;
668
669 $cleanup_stats = array(
670 'expired_transients' => 0,
671 'plugin_transients' => 0,
672 'rate_limit_transients' => 0,
673 'telemetry_transients' => 0,
674 'start_time' => microtime(true)
675 );
676
677 try {
678 delete_expired_transients(true);
679 $cleanup_stats['expired_transients'] = 'cleaned_by_wordpress';
680
681 $plugin_transients = $wpdb->get_results(
682 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_%'",
683 ARRAY_A
684 );
685
686 foreach ($plugin_transients as $transient) {
687 $transient_name = str_replace('_transient_', '', $transient['option_name']);
688 delete_transient($transient_name);
689 $cleanup_stats['plugin_transients']++;
690 }
691
692 $rate_limit_transients = $wpdb->get_results(
693 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
694 ARRAY_A
695 );
696
697 foreach ($rate_limit_transients as $transient) {
698 $transient_name = str_replace('_transient_', '', $transient['option_name']);
699 delete_transient($transient_name);
700 $cleanup_stats['rate_limit_transients']++;
701 }
702
703 $telemetry_transients = $wpdb->get_results(
704 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_telemetry_%'",
705 ARRAY_A
706 );
707
708 foreach ($telemetry_transients as $transient) {
709 $transient_name = str_replace('_transient_', '', $transient['option_name']);
710 delete_transient($transient_name);
711 $cleanup_stats['telemetry_transients']++;
712 }
713
714 $sa_connect_success_transients = $wpdb->get_results(
715 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_sa_connect_success_%'",
716 ARRAY_A
717 );
718
719 foreach ($sa_connect_success_transients as $transient) {
720 $transient_name = str_replace('_transient_', '', $transient['option_name']);
721 delete_transient($transient_name);
722 $cleanup_stats['sa_connect_success_transients'] = ($cleanup_stats['sa_connect_success_transients'] ?? 0) + 1;
723 }
724
725 $cleanup_stats['execution_time'] = round((microtime(true) - $cleanup_stats['start_time']) * 1000, 2);
726 $cleanup_stats['next_run'] = wp_next_scheduled('metasync_cleanup_transients') ?
727 date('Y-m-d H:i:s T', wp_next_scheduled('metasync_cleanup_transients')) : 'N/A';
728
729 error_log('MetaSync: Transient cleanup completed - ' . json_encode($cleanup_stats));
730
731 } catch (Exception $e) {
732 error_log('MetaSync: Transient cleanup failed - ' . $e->getMessage());
733 }
734 }
735
736 public function ajax_submit_issue_report()
737 {
738 try {
739 # Verify nonce
740 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_report_issue')) {
741 wp_send_json_error(array('message' => 'Security verification failed.'));
742 return;
743 }
744
745 # Check user capabilities
746 if (!Metasync::current_user_has_plugin_access()) {
747 wp_send_json_error(array('message' => 'Insufficient permissions.'));
748 return;
749 }
750
751 # Get and validate form data
752 $issue_message = isset($_POST['issue_message']) ? sanitize_textarea_field(wp_unslash($_POST['issue_message'])) : '';
753 $issue_severity = isset($_POST['issue_severity']) ? sanitize_text_field(wp_unslash($_POST['issue_severity'])) : 'warning';
754 $include_user_info = isset($_POST['include_user_info']) && sanitize_text_field(wp_unslash($_POST['include_user_info'])) === 'true';
755
756 # Validate severity level
757 $valid_severity_levels = array('info', 'warning', 'error', 'fatal');
758 if (!in_array($issue_severity, $valid_severity_levels, true)) {
759 $issue_severity = 'warning';
760 }
761
762 # Validate message length
763 if (empty($issue_message) || strlen($issue_message) < 10) {
764 wp_send_json_error(array('message' => 'Please provide a more detailed description (at least 10 characters).'));
765 return;
766 }
767 if (strlen($issue_message) > 1000) {
768 wp_send_json_error(array('message' => 'Message is too long. Please limit to 1000 characters.'));
769 return;
770 }
771
772 # Handle file upload if present
773 $attachment = null;
774 if (!empty($_FILES['issue_attachment']['tmp_name'])) {
775 # Validate file type using server-side MIME detection (not client-supplied type)
776 $tmp_name = $_FILES['issue_attachment']['tmp_name'];
777 $filename = sanitize_file_name($_FILES['issue_attachment']['name']);
778 $file_info = wp_check_filetype_and_ext($tmp_name, $filename);
779 if (!$file_info['ext']) {
780 wp_send_json_error(array('message' => 'Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.'));
781 return;
782 }
783 $allowed_extensions = array('jpg', 'jpeg', 'png', 'gif', 'webp');
784 if (!in_array($file_info['ext'], $allowed_extensions, true)) {
785 wp_send_json_error(array('message' => 'Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.'));
786 return;
787 }
788 $file_type = $file_info['type'];
789
790 # Validate file size (5MB max)
791 $max_size = 5 * 1024 * 1024; // 5MB
792 if ($_FILES['issue_attachment']['size'] > $max_size) {
793 wp_send_json_error(array('message' => 'File size exceeds 5MB. Please choose a smaller file.'));
794 return;
795 }
796
797 # Read file contents
798 $file_contents = file_get_contents($tmp_name);
799 if ($file_contents !== false) {
800 $attachment = array(
801 'filename' => $filename,
802 'data' => $file_contents,
803 'content_type' => $file_type
804 );
805 }
806 }
807
808 # Get general options (same way as used throughout the plugin)
809 $general_options = Metasync::get_option('general');
810 if (!is_array($general_options)) {
811 $general_options = array();
812 }
813
814 $project_uuid = isset($general_options['otto_pixel_uuid']) ? sanitize_text_field($general_options['otto_pixel_uuid']) : '';
815
816 # Always use standardized title format for Sentry prioritization
817 $issue_title = !empty($project_uuid) ? 'Client Report ' . $project_uuid : 'Client Report (UUID Not Configured)';
818
819 # Collect system information with error handling
820 $active_plugins = get_option('active_plugins');
821 $plugin_count = is_array($active_plugins) ? count($active_plugins) : 0;
822
823 $active_theme = wp_get_theme();
824 $theme_name = is_object($active_theme) ? $active_theme->get('Name') : get_template();
825
826 $system_context = array(
827 'report_type' => 'manual_client_report',
828 'website_url' => esc_url_raw(home_url()),
829 'site_title' => sanitize_text_field(get_bloginfo('name')),
830 'admin_email' => sanitize_email(get_bloginfo('admin_email')),
831 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0',
832 'plugin_name' => 'Search Engine Labs SEO (MetaSync)',
833 'wordpress_version' => get_bloginfo('version'),
834 'php_version' => PHP_VERSION,
835 'active_theme' => $theme_name,
836 'memory_limit' => ini_get('memory_limit'),
837 'multisite' => is_multisite(),
838 'project_uuid' => $project_uuid,
839 'active_plugins' => $plugin_count,
840 'report_timestamp' => current_time('mysql'),
841 'severity_level' => $issue_severity
842 );
843
844 # Add user information if requested
845 if ($include_user_info) {
846 $current_user = wp_get_current_user();
847 if ($current_user && $current_user->ID > 0) {
848 $system_context['reporter'] = array(
849 'username' => sanitize_user($current_user->user_login),
850 'email' => sanitize_email($current_user->user_email),
851 'display_name' => sanitize_text_field($current_user->display_name),
852 'roles' => is_array($current_user->roles) ? $current_user->roles : array()
853 );
854 }
855 }
856
857 # Send to Sentry using User Feedback API
858 $sent_to_sentry = false;
859
860 # Check if Sentry feedback function exists
861 if (!function_exists('metasync_sentry_capture_feedback')) {
862 # Log warning if function doesn't exist
863 if (defined('WP_DEBUG') && WP_DEBUG) {
864 error_log('MetaSync: Sentry feedback function not available for report submission.');
865 }
866 } else {
867 $feedback_data = array(
868 'message' => $issue_message,
869 'severity' => $issue_severity
870 );
871
872 # Add user information if requested
873 if ($include_user_info) {
874 $current_user = wp_get_current_user();
875 if ($current_user && $current_user->ID > 0) {
876 $feedback_data['name'] = sanitize_text_field($current_user->display_name);
877 $feedback_data['email'] = sanitize_email($current_user->user_email);
878 }
879 }
880
881 $sent_to_sentry = metasync_sentry_capture_feedback($feedback_data, $attachment);
882 }
883
884 if ($sent_to_sentry) {
885 wp_send_json_success(array(
886 'message' => 'Report submitted successfully! Our team will review it shortly.',
887 'project_uuid' => $project_uuid,
888 'report_title' => esc_html($issue_title)
889 ));
890 } else {
891 # Fallback: Log locally if Sentry fails or is unavailable
892 if (defined('WP_DEBUG') && WP_DEBUG) {
893 error_log(sprintf(
894 'MetaSync Client Report (Fallback): UUID: %s | Title: %s | Message: %s | Severity: %s',
895 $project_uuid,
896 $issue_title,
897 $issue_message,
898 $issue_severity
899 ));
900 }
901
902 wp_send_json_success(array(
903 'message' => 'Report logged locally. Note: Remote reporting may be unavailable.',
904 'project_uuid' => $project_uuid,
905 'report_title' => esc_html($issue_title),
906 'fallback' => true
907 ));
908 }
909
910 } catch (Exception $e) {
911 # Log the error securely (only in debug mode)
912 if (defined('WP_DEBUG') && WP_DEBUG) {
913 error_log(sprintf(
914 'MetaSync Report Submission Error: %s in %s on line %d',
915 $e->getMessage(),
916 $e->getFile(),
917 $e->getLine()
918 ));
919 }
920
921 # Send generic error message to client
922 wp_send_json_error(array('message' => 'Failed to submit report. Please try again later.'));
923 }
924 }
925
926 public function ajax_recover_password()
927 {
928 try {
929 if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'metasync_recover_password_nonce')) {
930 wp_send_json_error(array('message' => 'Security verification failed.'));
931 return;
932 }
933
934 $whitelabel_settings = Metasync::get_whitelabel_settings();
935 $password = Metasync::get_whitelabel_password();
936 $recovery_email = $whitelabel_settings['recovery_email'] ?? '';
937
938 if (empty($password)) {
939 wp_send_json_error(array('message' => 'No password is configured for recovery.'));
940 return;
941 }
942
943 if (empty($recovery_email) || !is_email($recovery_email)) {
944 wp_send_json_error(array('message' => 'No valid recovery email is configured. Please contact your administrator.'));
945 return;
946 }
947
948 $site_name = get_bloginfo('name');
949 $site_url = home_url();
950 $plugin_name = Metasync::get_effective_plugin_name('');
951 $settings_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '&tab=whitelabel');
952 $to = $recovery_email;
953
954 $subject = sprintf('[%s] Settings Password Recovery', $site_name);
955
956 $message = sprintf(
957 "Hello,\n\n" .
958 "A password recovery request was made for the %s settings on %s.\n\n" .
959 "Your Settings Password is:\n%s\n\n" .
960 "You can use this password to access the protected settings at:\n%s\n\n" .
961 "If you did not request this password recovery, please secure your WordPress admin account immediately.\n\n" .
962 "---\n" .
963 "This is an automated message from %s\n%s",
964 $plugin_name,
965 $site_name,
966 $password,
967 $settings_url,
968 $site_name,
969 $site_url
970 );
971
972 $from_name = !empty($whitelabel_settings['company_name'])
973 ? $whitelabel_settings['company_name']
974 : $site_name;
975
976 $headers = array(
977 'Content-Type: text/plain; charset=UTF-8',
978 sprintf('From: %s <%s>', $from_name, get_option('admin_email'))
979 );
980
981 $mail_error = '';
982 add_action('wp_mail_failed', function($error) use (&$mail_error) {
983 $mail_error = $error->get_error_message();
984 });
985
986 $sent = wp_mail($to, $subject, $message, $headers);
987
988 if ($sent) {
989 wp_send_json_success(array(
990 'message' => sprintf('Password recovery email sent to %s', esc_html($recovery_email))
991 ));
992 } else {
993 $error_message = 'Failed to send recovery email. ';
994 if (!empty($mail_error)) {
995 $error_message .= 'Error: ' . $mail_error;
996 } else {
997 $error_message .= 'Your server may not be configured to send emails. Please check your email configuration or contact your administrator.';
998 }
999
1000 wp_send_json_error(array('message' => $error_message));
1001 }
1002
1003 } catch (Exception $e) {
1004 if (defined('WP_DEBUG') && WP_DEBUG) {
1005 error_log(sprintf(
1006 'MetaSync Password Recovery Error: %s in %s on line %d',
1007 $e->getMessage(),
1008 $e->getFile(),
1009 $e->getLine()
1010 ));
1011 }
1012
1013 wp_send_json_error(array('message' => 'An error occurred while processing your request. Please try again later.'));
1014 }
1015 }
1016
1017 public function ajax_save_theme()
1018 {
1019 try {
1020 # Verify nonce for security
1021 if (!isset($_POST['_ajax_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_ajax_nonce'])), 'metasync_theme_nonce')) {
1022 wp_send_json_error(array('message' => 'Security verification failed.'));
1023 return;
1024 }
1025
1026 # Check user capabilities
1027 if (!Metasync::current_user_has_plugin_access()) {
1028 wp_send_json_error(array('message' => 'Insufficient permissions.'));
1029 return;
1030 }
1031
1032 # Get and validate theme value
1033 $theme = isset($_POST['theme']) ? sanitize_text_field(wp_unslash($_POST['theme'])) : '';
1034
1035 # Validate theme is either 'light' or 'dark'
1036 if (!in_array($theme, array('light', 'dark'), true)) {
1037 wp_send_json_error(array('message' => 'Invalid theme value.'));
1038 return;
1039 }
1040
1041 # Save theme preference to WordPress options
1042 update_option('metasync_theme', $theme, true);
1043
1044 # Send success response
1045 wp_send_json_success(array(
1046 'message' => 'Theme preference saved successfully.',
1047 'theme' => $theme
1048 ));
1049
1050 } catch (Exception $e) {
1051 # Log error if debug is enabled
1052 if (defined('WP_DEBUG') && WP_DEBUG) {
1053 error_log('MetaSync Theme Save Error: ' . $e->getMessage());
1054 }
1055
1056 wp_send_json_error(array('message' => 'Failed to save theme preference.'));
1057 }
1058 }
1059
1060 public function ajax_track_one_click_activation()
1061 {
1062 check_ajax_referer('metasync_nonce', 'nonce');
1063 # Check user capabilities
1064 if (!Metasync::current_user_has_plugin_access()) {
1065 wp_send_json_error(['message' => 'Insufficient permissions']);
1066 return;
1067 }
1068
1069 # Get parameters
1070 $auth_method = isset($_POST['auth_method']) ? sanitize_text_field(wp_unslash($_POST['auth_method'])) : 'searchatlas_connect';
1071 $is_reconnection = isset($_POST['is_reconnection']) ? filter_var($_POST['is_reconnection'], FILTER_VALIDATE_BOOLEAN) : false;
1072
1073 # Track the event in GA4
1074 try {
1075 Metasync_GA4::get_instance()->track_one_click_activation($auth_method, $is_reconnection);
1076
1077 wp_send_json_success([
1078 'message' => '1-click activation tracked successfully',
1079 'auth_method' => $auth_method,
1080 'is_reconnection' => $is_reconnection
1081 ]);
1082 } catch (Exception $e) {
1083 # Still return success to avoid breaking the auth flow
1084 wp_send_json_success([
1085 'message' => 'Authentication successful'
1086 ]);
1087 }
1088 }
1089
1090 public function handle_export_whitelabel_settings()
1091 {
1092 # Check user capabilities
1093 if (!Metasync::current_user_has_plugin_access()) {
1094 wp_die('Insufficient permissions');
1095 }
1096
1097 # Verify nonce for security (check both GET and POST)
1098 $nonce = '';
1099 if (isset($_POST['_wpnonce'])) {
1100 $nonce = sanitize_text_field(wp_unslash($_POST['_wpnonce']));
1101 } elseif (isset($_GET['_wpnonce'])) {
1102 $nonce = sanitize_text_field(wp_unslash($_GET['_wpnonce']));
1103 }
1104
1105 if (empty($nonce) || !wp_verify_nonce($nonce, 'metasync_export_whitelabel')) {
1106 wp_die('Security verification failed.');
1107 }
1108
1109 try {
1110 # Get all whitelabel settings
1111 $whitelabel_settings = Metasync::get_whitelabel_settings();
1112
1113 # Export the settings password as plaintext (not the encrypted blob):
1114 # the encryption key derives from this site's salts, so the blob
1115 # would be undecryptable after import on a different site. The
1116 # importer re-encrypts it at rest with the destination site's salts.
1117 if (!empty($whitelabel_settings['settings_password'])) {
1118 $whitelabel_settings['settings_password'] = Metasync::get_whitelabel_password();
1119 }
1120
1121 # Get general settings that relate to whitelabel
1122 $general_settings = Metasync::get_option('general');
1123 $whitelabel_related_general = array();
1124
1125 # Include ALL whitelabel-related general settings
1126 $whitelabel_keys = array(
1127 'white_label_plugin_name',
1128 'white_label_plugin_description',
1129 'white_label_plugin_author',
1130 'white_label_plugin_author_uri',
1131 'white_label_plugin_uri',
1132 'white_label_plugin_menu_slug',
1133 'white_label_plugin_menu_icon',
1134 'whitelabel_otto_name'
1135 );
1136
1137 foreach ($whitelabel_keys as $key) {
1138 if (isset($general_settings[$key])) {
1139 $whitelabel_related_general[$key] = $general_settings[$key];
1140 }
1141 }
1142
1143 # Bundle the menu icon file so it survives import on a different site.
1144 # If the icon is a local URL (media library), replace it with a special
1145 # marker and include the actual file in the ZIP under the plugin folder.
1146 $bundled_icon_filename = null;
1147 $icon_url = $whitelabel_related_general['white_label_plugin_menu_icon'] ?? '';
1148 if (!empty($icon_url) && filter_var($icon_url, FILTER_VALIDATE_URL)) {
1149 $site_url = trailingslashit(site_url());
1150 if (strpos($icon_url, $site_url) === 0) {
1151 // Resolve URL to an absolute filesystem path
1152 $relative_path = str_replace($site_url, ABSPATH, $icon_url);
1153 $icon_abs_path = realpath($relative_path);
1154 if ($icon_abs_path && file_exists($icon_abs_path)) {
1155 $ext = strtolower(pathinfo($icon_abs_path, PATHINFO_EXTENSION));
1156 $bundled_icon_filename = 'whitelabel-icon.' . $ext;
1157 // Replace the URL with a marker so the importer knows to restore it
1158 $whitelabel_related_general['white_label_plugin_menu_icon'] = '__bundled_icon__' . $ext;
1159 }
1160 }
1161 }
1162
1163 # Prepare export data
1164 $export_data = array(
1165 'version' => '1.0',
1166 'exported_at' => current_time('mysql'),
1167 'whitelabel_settings' => $whitelabel_settings,
1168 'general_settings' => $whitelabel_related_general
1169 );
1170
1171 # Convert to JSON
1172 $json_data = wp_json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1173
1174 if ($json_data === false) {
1175 wp_die('Failed to encode settings to JSON.');
1176 }
1177
1178 # Check if ZipArchive is available
1179 if (!class_exists('ZipArchive')) {
1180 wp_die('ZipArchive class is not available. Please enable PHP zip extension.');
1181 }
1182
1183 # Get plugin directory path (remove trailing slash for basename)
1184 $plugin_dir = rtrim(plugin_dir_path(dirname(__FILE__)), '/');
1185 $plugin_folder_name = basename($plugin_dir);
1186
1187 # Create temporary directory for zip file
1188 $upload_dir = wp_upload_dir();
1189 $temp_dir = $upload_dir['basedir'] . '/metasync-export-temp';
1190
1191 # Create temp directory if it doesn't exist
1192 if (!file_exists($temp_dir)) {
1193 wp_mkdir_p($temp_dir);
1194 }
1195
1196 # Generate unique filename
1197 $timestamp = date('Y-m-d_H-i-s');
1198 $zip_filename = 'metasync-whitelabel-plugin-' . $timestamp . '.zip';
1199 $json_filename = 'whitelabel-settings.json';
1200 $zip_path = $temp_dir . '/' . $zip_filename;
1201
1202 # Create zip file
1203 $zip = new ZipArchive();
1204 if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
1205 wp_die('Failed to create zip file.');
1206 }
1207
1208 # Add whitelabel settings JSON file to zip (inside plugin folder)
1209 $zip->addFromString($plugin_folder_name . '/' . $json_filename, $json_data);
1210
1211 # Bundle icon file if one was detected
1212 if ($bundled_icon_filename !== null && isset($icon_abs_path) && file_exists($icon_abs_path)) {
1213 $zip->addFile($icon_abs_path, $plugin_folder_name . '/' . $bundled_icon_filename);
1214 }
1215
1216
1217 # Files and directories to exclude from the zip
1218 $exclude_patterns = array(
1219 '.git',
1220 '.gitignore',
1221 '.gitattributes',
1222 'node_modules',
1223 '.DS_Store',
1224 'Thumbs.db',
1225 '.idea',
1226 '.vscode',
1227 'composer.lock',
1228 'package-lock.json',
1229 'yarn.lock',
1230 '.env',
1231 '.env.local',
1232 'docker-compose.yml',
1233 'Dockerfile',
1234 'Makefile',
1235 'renovate.json',
1236 'sonar-project.properties',
1237 'CODEOWNERS',
1238 'metasync-export-temp',
1239 'whitelabel-settings.json'
1240 );
1241
1242 # Recursively add plugin files to zip (with plugin folder as root in zip)
1243 self::add_directory_to_zip($zip, $plugin_dir . '/', $plugin_folder_name . '/', $exclude_patterns);
1244
1245 $zip->close();
1246
1247 # Check if file was created
1248 if (!file_exists($zip_path)) {
1249 wp_die('Zip file was not created successfully.');
1250 }
1251
1252 # Set headers for file download
1253 header('Content-Type: application/zip');
1254 header('Content-Disposition: attachment; filename="' . $zip_filename . '"');
1255 header('Content-Length: ' . filesize($zip_path));
1256 header('Pragma: no-cache');
1257 header('Expires: 0');
1258
1259 # Output file and clean up
1260 readfile($zip_path);
1261 unlink($zip_path);
1262
1263 # Clean up temp directory if empty
1264 if (is_dir($temp_dir) && count(scandir($temp_dir)) == 2) {
1265 rmdir($temp_dir);
1266 }
1267
1268 # Exit to prevent WordPress from adding anything to the response
1269 exit;
1270
1271 } catch (Exception $e) {
1272 # Log error if debug is enabled
1273 if (defined('WP_DEBUG') && WP_DEBUG) {
1274 error_log('MetaSync Whitelabel Export Error: ' . $e->getMessage());
1275 }
1276
1277 wp_die('Failed to export whitelabel settings: ' . $e->getMessage());
1278 }
1279 }
1280
1281 private static function add_directory_to_zip($zip, $dir, $zip_path = '', $exclude_patterns = array())
1282 {
1283 if (!is_dir($dir)) {
1284 return;
1285 }
1286
1287 $files = scandir($dir);
1288
1289 foreach ($files as $file) {
1290 if ($file === '.' || $file === '..') {
1291 continue;
1292 }
1293
1294 $file_path = $dir . $file;
1295 $zip_file_path = $zip_path . $file;
1296
1297 $should_exclude = false;
1298 foreach ($exclude_patterns as $pattern) {
1299 if (strpos($file, $pattern) !== false || strpos($file_path, $pattern) !== false) {
1300 $should_exclude = true;
1301 break;
1302 }
1303 }
1304
1305 if ($should_exclude) {
1306 continue;
1307 }
1308
1309 if (is_dir($file_path)) {
1310 $zip->addEmptyDir($zip_file_path);
1311 self::add_directory_to_zip($zip, $file_path . '/', $zip_file_path . '/', $exclude_patterns);
1312 } else {
1313 if (file_exists($file_path) && is_readable($file_path)) {
1314 $zip->addFile($file_path, $zip_file_path);
1315 }
1316 }
1317 }
1318 }
1319
1320 public function render_html_pages_dashboard_widget()
1321 {
1322 global $wpdb;
1323
1324 $query = "
1325 SELECT p.ID, p.post_title, p.post_modified, p.post_type
1326 FROM {$wpdb->posts} p
1327 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
1328 WHERE p.post_status = 'publish'
1329 AND p.post_type IN ('post', 'page')
1330 AND (pm.meta_key = '_metasync_raw_html_enabled' OR pm.meta_key = '_metasync_custom_css')
1331 GROUP BY p.ID
1332 ORDER BY p.post_modified DESC
1333 LIMIT 10
1334 ";
1335
1336 $html_pages = $wpdb->get_results($query);
1337 $total_count = count($html_pages);
1338
1339 $label = $this->get_html_source_label();
1340
1341 if (empty($html_pages)) {
1342 echo '<div class="metasync-dashboard-widget-empty">';
1343 echo '<span class="dashicons dashicons-admin-page" style="font-size: 48px; opacity: 0.3; display: block; margin: 20px auto;"></span>';
1344 echo '<p style="text-align: center; color: #666;">';
1345 echo sprintf(__('No pages created with %s yet.', 'metasync'), '<strong>' . esc_html($label) . '</strong>');
1346 echo '</p>';
1347 echo '<p style="text-align: center;">';
1348 echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-primary">';
1349 echo __('Get Started', 'metasync');
1350 echo '</a>';
1351 echo '</p>';
1352 echo '</div>';
1353 return;
1354 }
1355
1356 echo '<div class="metasync-dashboard-widget">';
1357
1358 echo '<div class="metasync-widget-stats">';
1359 echo '<div class="metasync-stat-box">';
1360 echo '<span class="metasync-stat-number">' . $total_count . '</span>';
1361 echo '<span class="metasync-stat-label">' . __('AI-Generated Pages', 'metasync') . '</span>';
1362 echo '</div>';
1363 echo '</div>';
1364
1365 echo '<div class="metasync-widget-list">';
1366 echo '<h4>' . __('Recent Pages', 'metasync') . '</h4>';
1367 echo '<ul>';
1368
1369 foreach ($html_pages as $page) {
1370 $edit_link = get_edit_post_link($page->ID);
1371 $view_link = get_permalink($page->ID);
1372 $time_ago = human_time_diff(strtotime($page->post_modified), current_time('timestamp'));
1373
1374 echo '<li class="metasync-widget-page-item">';
1375 echo '<span class="metasync-page-icon">⚡</span>';
1376 echo '<div class="metasync-page-details">';
1377 echo '<a href="' . esc_url($edit_link) . '" class="metasync-page-title">';
1378 echo esc_html($page->post_title ?: __('(no title)', 'metasync'));
1379 echo '</a>';
1380 echo '<span class="metasync-page-meta">';
1381 echo sprintf(__('Updated %s ago', 'metasync'), $time_ago);
1382 echo '';
1383 echo '<a href="' . esc_url($view_link) . '" target="_blank">' . __('View', 'metasync') . '</a>';
1384 echo '</span>';
1385 echo '</div>';
1386 echo '</li>';
1387 }
1388
1389 echo '</ul>';
1390 echo '</div>';
1391
1392 echo '<div class="metasync-widget-footer">';
1393 echo '<a href="' . admin_url('edit.php?post_type=page') . '">';
1394 echo __('View All Pages', 'metasync') . '';
1395 echo '</a>';
1396 echo '</div>';
1397
1398 echo '</div>';
1399 }
1400
1401 private function get_html_source_label()
1402 {
1403 $whitelabel_company = Metasync::get_whitelabel_company_name();
1404 if (!empty($whitelabel_company)) {
1405 return $whitelabel_company . ' AI';
1406 }
1407
1408 return Metasync::get_effective_plugin_name() . ' AI';
1409 }
1410 }
1411