| @@ -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>'; |