PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-admin-ajax.php +122 -79 2.6.25 → 2.7.2 View file →
@@ -57,9 +57,9 @@
57 57 $replacement = array('owner' => $owner, 'time' => time());
58 58 global $wpdb;
59 59 if (isset($wpdb) && is_object($wpdb) && method_exists($wpdb, 'query') && method_exists($wpdb, 'prepare')) {
60 60 $serialize = function ($value) { return function_exists('maybe_serialize') ? maybe_serialize($value) : serialize($value); };
61 - $wpdb->query($wpdb->prepare(
61 + $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- atomic compare-and-swap on option rows — the WHERE-guarded UPDATE/DELETE is the lock primitive
62 62 "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
63 63 $serialize($replacement), $key, $serialize($held)
64 64 ));
65 65 if (function_exists('wp_cache_delete')) wp_cache_delete($key, 'options');
@@ -79,9 +79,9 @@
79 79 if (is_array($held) && isset($held['owner']) && hash_equals((string) $held['owner'], (string) $owner)) {
80 80 global $wpdb;
81 81 if (isset($wpdb) && is_object($wpdb) && method_exists($wpdb, 'query') && method_exists($wpdb, 'prepare')) {
82 82 $serialized = function_exists('maybe_serialize') ? maybe_serialize($held) : serialize($held);
83 - $wpdb->query($wpdb->prepare(
83 + $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- atomic compare-and-swap on option rows — the WHERE-guarded UPDATE/DELETE is the lock primitive
84 84 "DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
85 85 $key,
86 86 $serialized
87 87 ));
@@ -127,9 +127,9 @@
127 127 public function ajax_import_external_data()
128 128 {
129 129 $execution_time = Metasync_Settings_Fields::instance()->get_execution_setting('max_execution_time');
130 130 if (function_exists('set_time_limit')) {
131 - @set_time_limit($execution_time);
131 + @set_time_limit($execution_time); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
132 132 }
133 133
134 134 Metasync_Settings_Fields::instance()->apply_memory_limit();
135 135
@@ -138,10 +138,10 @@
138 138 if (!Metasync::current_user_has_plugin_access()) {
139 139 wp_send_json_error(['message' => 'Insufficient permissions.']);
140 140 }
141 141
142 - $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : '';
143 - $plugin = isset($_POST['plugin']) ? sanitize_text_field($_POST['plugin']) : '';
142 + $type = isset($_POST['type']) ? sanitize_text_field(wp_unslash($_POST['type'])) : '';
143 + $plugin = isset($_POST['plugin']) ? sanitize_text_field(wp_unslash($_POST['plugin'])) : '';
144 144 $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
145 145
146 146 if (empty($type) || empty($plugin)) {
147 147 wp_send_json_error(['message' => 'Missing required parameters.']);
@@ -168,8 +168,17 @@
168 168 $result = $importer->import_schema($plugin);
169 169 break;
170 170 }
171 171
172 + // A schema import that only found already-migrated or unsupported
173 + // records is an accurate report, not a failure — keep the breakdown
174 + // with the response so the UI can show each category separately.
175 + if (isset($result['breakdown'])) {
176 + $result['breakdown'] = array_map('intval', (array) $result['breakdown']);
177 + }
178 +
179 + Metasync_PostHog::import_batch($plugin, $type, $result, $offset);
180 +
172 181 if ($result['success']) {
173 182 wp_send_json_success($result);
174 183 } else {
175 184 wp_send_json_error($result);
@@ -179,9 +188,9 @@
179 188 public function ajax_import_seo_metadata()
180 189 {
181 190 $execution_time = Metasync_Settings_Fields::instance()->get_execution_setting('max_execution_time');
182 191 if (function_exists('set_time_limit')) {
183 - @set_time_limit($execution_time);
192 + @set_time_limit($execution_time); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- runtime PHP setting with no WordPress API
184 193 }
185 194
186 195 Metasync_Settings_Fields::instance()->apply_memory_limit();
187 196
@@ -190,9 +199,9 @@
190 199 if (!Metasync::current_user_has_plugin_access()) {
191 200 wp_send_json_error(['message' => 'Insufficient permissions.']);
192 201 }
193 202
194 - $plugin = isset($_POST['plugin']) ? sanitize_text_field($_POST['plugin']) : '';
203 + $plugin = isset($_POST['plugin']) ? sanitize_text_field(wp_unslash($_POST['plugin'])) : '';
195 204 $import_titles = isset($_POST['import_titles']) ? (bool) intval($_POST['import_titles']) : true;
196 205 $import_descriptions = isset($_POST['import_descriptions']) ? (bool) intval($_POST['import_descriptions']) : true;
197 206 $import_social_text = isset($_POST['import_social_text']) ? (bool) intval($_POST['import_social_text']) : true;
198 207 $import_social_images = isset($_POST['import_social_images']) ? (bool) intval($_POST['import_social_images']) : true;
@@ -217,8 +226,12 @@
217 226 ];
218 227
219 228 $result = $importer->import_seo_metadata($plugin, $options);
220 229
230 + Metasync_PostHog::import_batch($plugin, 'seo_metadata', $result, $offset, array(
231 + 'overwrite_existing' => $overwrite_existing,
232 + ));
233 +
221 234 if ($result['success']) {
222 235 wp_send_json_success($result);
223 236 } else {
224 237 wp_send_json_error($result);
@@ -319,9 +332,9 @@
319 332 $step = isset($_POST['step']) ? intval($_POST['step']) : 0;
320 333 $raw_data = isset($_POST['data']) ? $_POST['data'] : array();
321 334
322 335 // Allowlist top-level wizard data keys to prevent mass assignment
323 - $allowed_wizard_keys = array('verification', 'seo_settings', 'schema');
336 + $allowed_wizard_keys = array('verification', 'seo_settings', 'schema', 'analytics');
324 337 $data = is_array($raw_data) ? array_intersect_key($raw_data, array_flip($allowed_wizard_keys)) : array();
325 338
326 339 $options = get_option('metasync_options', array());
327 340
@@ -351,8 +364,13 @@
351 364 $options['general']['enable_schema_markup'] = $data['schema']['enabled'];
352 365 $options['general']['default_schema_type'] = sanitize_text_field($data['schema']['default_type']);
353 366 }
354 367
368 + if (isset($data['analytics']) && is_array($data['analytics'])) {
369 + update_option('metasync_analytics_opt_in', !empty($data['analytics']['enabled']) ? 'yes' : 'no');
370 + update_option('metasync_analytics_consent_explicit', 'yes');
371 + }
372 +
355 373 update_option('metasync_options', $options);
356 374
357 375 wp_send_json_success(array('message' => 'Progress saved'));
358 376 }
@@ -374,8 +392,14 @@
374 392
375 393 $user_id = get_current_user_id();
376 394 delete_transient("metasync_wizard_state_{$user_id}");
377 395
396 + // Consent-gated product analytics: setup funnel completion.
397 + $general_settings = Metasync::get_option('general');
398 + Metasync_PostHog::feature('setup_wizard_completed', array(
399 + 'connected' => !empty($general_settings['searchatlas_api_key']),
400 + ));
401 +
378 402 wp_send_json_success(array('message' => 'Wizard completed'));
379 403 }
380 404
381 405 public function ajax_validate_robots()
@@ -563,10 +587,10 @@
563 587 if (!Metasync::current_user_has_plugin_access()) {
564 588 wp_die('Insufficient permissions');
565 589 }
566 590
567 - $uri = sanitize_text_field($_POST['uri']);
568 - $redirect_url = sanitize_url($_POST['redirect_url']);
591 + $uri = sanitize_text_field(wp_unslash($_POST['uri']));
592 + $redirect_url = sanitize_url(wp_unslash($_POST['redirect_url']));
569 593
570 594 if (empty($uri) || empty($redirect_url)) {
571 595 wp_send_json_error('Missing required parameters');
572 596 }
@@ -635,9 +659,9 @@
635 659 try {
636 660 delete_expired_transients(true);
637 661 $cleanup_stats['expired_transients'] = 'cleaned_by_wordpress';
638 662
639 - $plugin_transients = $wpdb->get_results(
663 + $plugin_transients = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- transient enumeration/cleanup on the options table — no API to scan or bulk-delete transient rows
640 664 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_%'",
641 665 ARRAY_A
642 666 );
643 667
@@ -646,9 +670,9 @@
646 670 delete_transient($transient_name);
647 671 $cleanup_stats['plugin_transients']++;
648 672 }
649 673
650 - $rate_limit_transients = $wpdb->get_results(
674 + $rate_limit_transients = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- transient enumeration/cleanup on the options table — no API to scan or bulk-delete transient rows
651 675 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_sa_connect_rate_limit_%'",
652 676 ARRAY_A
653 677 );
654 678
@@ -657,9 +681,9 @@
657 681 delete_transient($transient_name);
658 682 $cleanup_stats['rate_limit_transients']++;
659 683 }
660 684
661 - $telemetry_transients = $wpdb->get_results(
685 + $telemetry_transients = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- transient enumeration/cleanup on the options table — no API to scan or bulk-delete transient rows
662 686 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_telemetry_%'",
663 687 ARRAY_A
664 688 );
665 689
@@ -668,9 +692,9 @@
668 692 delete_transient($transient_name);
669 693 $cleanup_stats['telemetry_transients']++;
670 694 }
671 695
672 - $sa_connect_success_transients = $wpdb->get_results(
696 + $sa_connect_success_transients = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- transient enumeration/cleanup on the options table — no API to scan or bulk-delete transient rows
673 697 "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_metasync_sa_connect_success_%'",
674 698 ARRAY_A
675 699 );
676 700
@@ -681,17 +705,50 @@
681 705 }
682 706
683 707 $cleanup_stats['execution_time'] = round((microtime(true) - $cleanup_stats['start_time']) * 1000, 2);
684 708 $cleanup_stats['next_run'] = wp_next_scheduled('metasync_cleanup_transients') ?
685 - date('Y-m-d H:i:s T', wp_next_scheduled('metasync_cleanup_transients')) : 'N/A';
709 + gmdate('Y-m-d H:i:s T', wp_next_scheduled('metasync_cleanup_transients')) : 'N/A';
686 710
687 - error_log('MetaSync: Transient cleanup completed - ' . json_encode($cleanup_stats));
688 -
689 711 } catch (Exception $e) {
690 - error_log('MetaSync: Transient cleanup failed - ' . $e->getMessage());
712 + error_log('MetaSync: Transient cleanup failed - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
691 713 }
692 714 }
693 715
716 + /**
717 + * Collect the locally stored white-label settings for an issue-report payload.
718 + *
719 + * The values are attached to the Sentry event so support can tell from the
720 + * event alone whether the reporting site is white-labeled. Local options
721 + * can be stale — the reply-drafting process must still cross-check the
722 + * backend account record before sending.
723 + *
724 + * Static and public so the unit suite can exercise the reads directly; the
725 + * AJAX handler itself dies on wp_send_json_* before returning.
726 + *
727 + * @return array Keys: is_whitelabel, whitelabel_brand_name, whitelabel_support_email, site_url.
728 + */
729 + public static function build_issue_report_whitelabel_context()
730 + {
731 + $whitelabel_settings = Metasync::get_whitelabel_settings();
732 + if (!is_array($whitelabel_settings)) {
733 + $whitelabel_settings = array();
734 + }
735 + $whitelabel_support_email = '';
736 + foreach (array('support_email_override', 'support_email', 'recovery_email') as $email_key) {
737 + if (!empty($whitelabel_settings[$email_key])) {
738 + $whitelabel_support_email = sanitize_email($whitelabel_settings[$email_key]);
739 + break;
740 + }
741 + }
742 + return array(
743 + 'is_whitelabel' => Metasync::is_whitelabel_enabled(),
744 + 'whitelabel_brand_name' => !empty($whitelabel_settings['company_name'])
745 + ? sanitize_text_field($whitelabel_settings['company_name']) : '',
746 + 'whitelabel_support_email' => $whitelabel_support_email,
747 + 'site_url' => esc_url_raw(home_url()),
748 + );
749 + }
750 +
694 751 public function ajax_submit_issue_report()
695 752 {
696 753 try {
697 754 # Verify nonce
@@ -762,8 +819,11 @@
762 819 );
763 820 }
764 821 }
765 822
823 + # Read the locally stored white-label settings for the report payload.
824 + $whitelabel_context = self::build_issue_report_whitelabel_context();
825 +
766 826 # Get general options (same way as used throughout the plugin)
767 827 $general_options = Metasync::get_option('general');
768 828 if (!is_array($general_options)) {
769 829 $general_options = array();
@@ -818,14 +878,18 @@
818 878 # Check if Sentry feedback function exists
819 879 if (!function_exists('metasync_sentry_capture_feedback')) {
820 880 # Log warning if function doesn't exist
821 881 if (defined('WP_DEBUG') && WP_DEBUG) {
822 - error_log('MetaSync: Sentry feedback function not available for report submission.');
882 + error_log('MetaSync: Sentry feedback function not available for report submission.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
823 883 }
824 884 } else {
825 885 $feedback_data = array(
826 886 'message' => $issue_message,
827 - 'severity' => $issue_severity
887 + 'severity' => $issue_severity,
888 + 'is_whitelabel' => $whitelabel_context['is_whitelabel'],
889 + 'whitelabel_brand_name' => $whitelabel_context['whitelabel_brand_name'],
890 + 'whitelabel_support_email' => $whitelabel_context['whitelabel_support_email'],
891 + 'site_url' => $whitelabel_context['site_url']
828 892 );
829 893
830 894 # Add user information if requested
831 895 if ($include_user_info) {
@@ -847,13 +911,11 @@
847 911 ));
848 912 } else {
849 913 # Fallback: Log locally if Sentry fails or is unavailable
850 914 if (defined('WP_DEBUG') && WP_DEBUG) {
851 - error_log(sprintf(
852 - 'MetaSync Client Report (Fallback): UUID: %s | Title: %s | Message: %s | Severity: %s',
915 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
916 + 'MetaSync Client Report (Fallback): UUID: %s | Severity: %s',
853 917 $project_uuid,
854 - $issue_title,
855 - $issue_message,
856 918 $issue_severity
857 919 ));
858 920 }
859 921
@@ -867,9 +929,9 @@
867 929
868 930 } catch (Exception $e) {
869 931 # Log the error securely (only in debug mode)
870 932 if (defined('WP_DEBUG') && WP_DEBUG) {
871 - error_log(sprintf(
933 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
872 934 'MetaSync Report Submission Error: %s in %s on line %d',
873 935 $e->getMessage(),
874 936 $e->getFile(),
875 937 $e->getLine()
@@ -1012,9 +1074,9 @@
1012 1074 if ($rate_owner !== '') {
1013 1075 $this->release_recovery_lock($rate_owner);
1014 1076 }
1015 1077 if (defined('WP_DEBUG') && WP_DEBUG) {
1016 - error_log(sprintf(
1078 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
1017 1079 'MetaSync Password Recovery Error: %s in %s on line %d',
1018 1080 $e->getMessage(),
1019 1081 $e->getFile(),
1020 1082 $e->getLine()
@@ -1162,9 +1224,9 @@
1162 1224
1163 1225 } catch (Exception $e) {
1164 1226 # Log error if debug is enabled
1165 1227 if (defined('WP_DEBUG') && WP_DEBUG) {
1166 - error_log('MetaSync Theme Save Error: ' . $e->getMessage());
1228 + error_log('MetaSync Theme Save Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
1167 1229 }
1168 1230
1169 1231 wp_send_json_error(array('message' => 'Failed to save theme preference.'));
1170 1232 }
@@ -1218,40 +1280,20 @@
1218 1280 wp_die('Security verification failed.');
1219 1281 }
1220 1282
1221 1283 try {
1222 - # Get all whitelabel settings
1223 - $whitelabel_settings = Metasync::get_whitelabel_settings();
1224 -
1225 - # Export the settings password as plaintext (not the encrypted blob):
1226 - # the encryption key derives from this site's salts, so the blob
1227 - # would be undecryptable after import on a different site. The
1228 - # importer re-encrypts it at rest with the destination site's salts.
1229 - if (!empty($whitelabel_settings['settings_password'])) {
1230 - $whitelabel_settings['settings_password'] = Metasync::get_whitelabel_password();
1284 + # Source the white-label export payload from the shared helper so
1285 + # reset and export can never drift apart: whatever the reset
1286 + # preserves is exactly what the export serialises. No API
1287 + # credentials (apikey / searchatlas_api_key / otto_pixel_uuid / ...)
1288 + # are ever serialised - the helper only captures the whitelabel
1289 + # blob and the general branding keys.
1290 + if (!class_exists('Metasync_Whitelabel_Preservation')) {
1291 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-whitelabel-preservation.php';
1231 1292 }
1232 -
1233 - # Get general settings that relate to whitelabel
1234 - $general_settings = Metasync::get_option('general');
1235 - $whitelabel_related_general = array();
1236 -
1237 - # Include ALL whitelabel-related general settings
1238 - $whitelabel_keys = array(
1239 - 'white_label_plugin_name',
1240 - 'white_label_plugin_description',
1241 - 'white_label_plugin_author',
1242 - 'white_label_plugin_author_uri',
1243 - 'white_label_plugin_uri',
1244 - 'white_label_plugin_menu_slug',
1245 - 'white_label_plugin_menu_icon',
1246 - 'whitelabel_otto_name'
1247 - );
1248 -
1249 - foreach ($whitelabel_keys as $key) {
1250 - if (isset($general_settings[$key])) {
1251 - $whitelabel_related_general[$key] = $general_settings[$key];
1252 - }
1253 - }
1293 + $export_payload = Metasync_Whitelabel_Preservation::build_whitelabel_export_data();
1294 + $whitelabel_settings = $export_payload['whitelabel_settings'];
1295 + $whitelabel_related_general = $export_payload['general_settings'];
1254 1296
1255 1297 # Bundle the menu icon file so it survives import on a different site.
1256 1298 # If the icon is a local URL (media library), replace it with a special
1257 1299 # marker and include the actual file in the ZIP under the plugin folder.
@@ -1305,9 +1347,9 @@
1305 1347 wp_mkdir_p($temp_dir);
1306 1348 }
1307 1349
1308 1350 # Generate unique filename
1309 - $timestamp = date('Y-m-d_H-i-s');
1351 + $timestamp = gmdate('Y-m-d_H-i-s');
1310 1352 $zip_filename = 'metasync-whitelabel-plugin-' . $timestamp . '.zip';
1311 1353 $json_filename = 'whitelabel-settings.json';
1312 1354 $zip_path = $temp_dir . '/' . $zip_filename;
1313 1355
@@ -1368,14 +1410,14 @@
1368 1410 header('Pragma: no-cache');
1369 1411 header('Expires: 0');
1370 1412
1371 1413 # Output file and clean up
1372 - readfile($zip_path);
1373 - unlink($zip_path);
1414 + readfile($zip_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- streams a large file to output without loading it into memory
1415 + wp_delete_file($zip_path);
1374 1416
1375 1417 # Clean up temp directory if empty
1376 1418 if (is_dir($temp_dir) && count(scandir($temp_dir)) == 2) {
1377 - rmdir($temp_dir);
1419 + rmdir($temp_dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP_Filesystem would prompt for FTP credentials on non-direct hosts
1378 1420 }
1379 1421
1380 1422 # Exit to prevent WordPress from adding anything to the response
1381 1423 exit;
@@ -1382,12 +1424,12 @@
1382 1424
1383 1425 } catch (Exception $e) {
1384 1426 # Log error if debug is enabled
1385 1427 if (defined('WP_DEBUG') && WP_DEBUG) {
1386 - error_log('MetaSync Whitelabel Export Error: ' . $e->getMessage());
1428 + error_log('MetaSync Whitelabel Export Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
1387 1429 }
1388 1430
1389 - wp_die('Failed to export whitelabel settings: ' . $e->getMessage());
1431 + wp_die(esc_html__('Failed to export whitelabel settings.', 'metasync'));
1390 1432 }
1391 1433 }
1392 1434
1393 1435 private static function add_directory_to_zip($zip, $dir, $zip_path = '', $exclude_patterns = array())
@@ -1432,10 +1474,10 @@
1432 1474 public function render_html_pages_dashboard_widget()
1433 1475 {
1434 1476 global $wpdb;
1435 1477
1436 - $query = "
1437 - SELECT p.ID, p.post_title, p.post_modified, p.post_type
1478 + $html_pages = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin dashboard widget join over posts+postmeta — aggregate view, no WordPress API for the join
1479 + "SELECT p.ID, p.post_title, p.post_modified, p.post_type
1438 1480 FROM {$wpdb->posts} p
1439 1481 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
1440 1482 WHERE p.post_status = 'publish'
1441 1483 AND p.post_type IN ('post', 'page')
@@ -1442,11 +1484,10 @@
1442 1484 AND (pm.meta_key = '_metasync_raw_html_enabled' OR pm.meta_key = '_metasync_custom_css')
1443 1485 GROUP BY p.ID
1444 1486 ORDER BY p.post_modified DESC
1445 1487 LIMIT 10
1446 - ";
1447 -
1448 - $html_pages = $wpdb->get_results($query);
1488 + "
1489 + );
1449 1490 $total_count = count($html_pages);
1450 1491
1451 1492 $label = $this->get_html_source_label();
1452 1493
@@ -1453,13 +1494,14 @@
1453 1494 if (empty($html_pages)) {
1454 1495 echo '<div class="metasync-dashboard-widget-empty">';
1455 1496 echo '<span class="dashicons dashicons-admin-page" style="font-size: 48px; opacity: 0.3; display: block; margin: 20px auto;"></span>';
1456 1497 echo '<p style="text-align: center; color: #666;">';
1457 - echo sprintf(__('No pages created with %s yet.', 'metasync'), '<strong>' . esc_html($label) . '</strong>');
1498 + /* translators: %s: HTML source label. */
1499 + echo sprintf(esc_html__('No pages created with %s yet.', 'metasync'), '<strong>' . esc_html($label) . '</strong>');
1458 1500 echo '</p>';
1459 1501 echo '<p style="text-align: center;">';
1460 - echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-primary">';
1461 - echo __('Get Started', 'metasync');
1502 + echo '<a href="' . esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)) . '" class="button button-primary">';
1503 + echo esc_html__('Get Started', 'metasync');
1462 1504 echo '</a>';
1463 1505 echo '</p>';
1464 1506 echo '</div>';
1465 1507 return;
@@ -1468,15 +1510,15 @@
1468 1510 echo '<div class="metasync-dashboard-widget">';
1469 1511
1470 1512 echo '<div class="metasync-widget-stats">';
1471 1513 echo '<div class="metasync-stat-box">';
1472 - echo '<span class="metasync-stat-number">' . $total_count . '</span>';
1473 - echo '<span class="metasync-stat-label">' . __('AI-Generated Pages', 'metasync') . '</span>';
1514 + echo '<span class="metasync-stat-number">' . esc_html((string) $total_count) . '</span>';
1515 + echo '<span class="metasync-stat-label">' . esc_html__('AI-Generated Pages', 'metasync') . '</span>';
1474 1516 echo '</div>';
1475 1517 echo '</div>';
1476 1518
1477 1519 echo '<div class="metasync-widget-list">';
1478 - echo '<h4>' . __('Recent Pages', 'metasync') . '</h4>';
1520 + echo '<h4>' . esc_html__('Recent Pages', 'metasync') . '</h4>';
1479 1521 echo '<ul>';
1480 1522
1481 1523 foreach ($html_pages as $page) {
1482 1524 $edit_link = get_edit_post_link($page->ID);
@@ -1489,11 +1531,12 @@
1489 1531 echo '<a href="' . esc_url($edit_link) . '" class="metasync-page-title">';
1490 1532 echo esc_html($page->post_title ?: __('(no title)', 'metasync'));
1491 1533 echo '</a>';
1492 1534 echo '<span class="metasync-page-meta">';
1493 - echo sprintf(__('Updated %s ago', 'metasync'), $time_ago);
1535 + /* translators: %s: human-readable time difference. */
1536 + echo esc_html(sprintf(__('Updated %s ago', 'metasync'), $time_ago));
1494 1537 echo ' • ';
1495 - echo '<a href="' . esc_url($view_link) . '" target="_blank">' . __('View', 'metasync') . '</a>';
1538 + echo '<a href="' . esc_url($view_link) . '" target="_blank">' . esc_html__('View', 'metasync') . '</a>';
1496 1539 echo '</span>';
1497 1540 echo '</div>';
1498 1541 echo '</li>';
1499 1542 }
@@ -1501,10 +1544,10 @@
1501 1544 echo '</ul>';
1502 1545 echo '</div>';
1503 1546
1504 1547 echo '<div class="metasync-widget-footer">';
1505 - echo '<a href="' . admin_url('edit.php?post_type=page') . '">';
1506 - echo __('View All Pages', 'metasync') . ' →';
1548 + echo '<a href="' . esc_url(admin_url("edit.php?post_type=page")) . '">';
1549 + echo esc_html__('View All Pages', 'metasync') . ' →';
1507 1550 echo '</a>';
1508 1551 echo '</div>';
1509 1552
1510 1553 echo '</div>';