From 981ffe94edbd2b97580464d20462cd1cd0cc0e30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=96=87=E6=B4=BE=E5=A4=87=E6=A1=88?=
<130886204+modiqi@users.noreply.github.com>
Date: Tue, 10 Jun 2025 20:38:16 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=BC=E5=85=A5=E5=AF=BC?=
=?UTF-8?q?=E5=87=BA=E6=89=B9=E5=A4=84=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
admin/pages.php | 737 ++++++++++++++++++++++++---------------
includes/class-tools.php | 430 +++++++++++++++++++----
2 files changed, 823 insertions(+), 344 deletions(-)
diff --git a/admin/pages.php b/admin/pages.php
index 508252d..4fc080c 100644
--- a/admin/pages.php
+++ b/admin/pages.php
@@ -700,296 +700,479 @@ function dm_render_installation_check() {
/**
* Render health tab content
*/
-function dm_render_health_content() {
- // Get all domains
- global $wpdb;
- $tables = dm_get_table_names();
- $domains = $wpdb->get_results("
- SELECT d.*, b.domain as original_domain, b.path
- FROM {$tables['domains']} d
- JOIN {$wpdb->blogs} b ON d.blog_id = b.blog_id
- ORDER BY d.blog_id ASC, d.active DESC
- ");
+ function dm_render_health_content() {
+ // Get all domains
+ global $wpdb;
+ $tables = dm_get_table_names();
+ $domains = $wpdb->get_results("
+ SELECT d.*, b.domain as original_domain, b.path
+ FROM {$tables['domains']} d
+ JOIN {$wpdb->blogs} b ON d.blog_id = b.blog_id
+ ORDER BY d.blog_id ASC, d.active DESC
+ ");
- // Get health check results
- $health_results = get_site_option( 'dm_domain_health_results', array() );
+ // Get health check results
+ $health_results = get_site_option( 'dm_domain_health_results', array() );
- // Check if batch processing is in progress
- $health_check_progress = get_site_option( 'dm_health_check_progress', false );
- ?>
+ // Check if batch processing is in progress
+ $health_check_progress = get_site_option( 'dm_health_check_progress', false );
+ ?>
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+ 0 ? round( ( $health_check_progress['processed'] / $health_check_progress['total'] ) * 100 ) : 0
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
- '
+ },
+ success: function(response) {
+ retryCount = 0; // Reset retry count
+
+ if (response.success) {
+ var data = response.data;
+
+ // Update progress display
+ updateProgressDisplay(data);
+
+ if (data.complete) {
+ // Health check completed
+ stopHealthCheckMonitoring('completed');
+
+ // Delay page refresh to show results
+ setTimeout(function() {
+ window.location.href = removeUrlParameter(window.location.href, 'checking');
+ }, 2000);
+ }
+ } else {
+ console.error('Health check error:', response.data);
+ handleHealthCheckError(response.data);
+ }
+ },
+ error: function(xhr, status, error) {
+ console.error('AJAX error:', status, error, xhr.responseText);
+ handleHealthCheckError('Network error: ' + error);
+ }
+ });
+ }
+
+ /**
+ * Handle health check error
+ */
+ function handleHealthCheckError(errorMessage) {
+ retryCount++;
+
+ if (retryCount >= maxRetries) {
+ stopHealthCheckMonitoring('max_retries');
+ showHealthCheckError('Health check failed after ' + maxRetries + ' attempts: ' + errorMessage);
+ } else {
+ console.log('Health check error (attempt ' + retryCount + '/' + maxRetries + '):', errorMessage);
+ // Continue retrying with longer interval
+ clearInterval(healthCheckInterval);
+ healthCheckInterval = setInterval(function() {
+ checkHealthProgress();
+ }, 5000); // Extend to 5 second interval after error
+ }
+ }
+
+ /**
+ * Update progress display
+ */
+ function updateProgressDisplay(data) {
+ var percentage = data.percentage || 0;
+ var processed = data.processed || 0;
+ var total = data.total || 0;
+
+ // Update progress text
+ $('#health-check-progress-text').text(
+ '' +
+ processed + ' ' +
+ total + ' (' +
+ percentage + '%)'
+ );
+
+ // Update progress bar
+ $('#health-check-progress-bar').css('width', percentage + '%');
+
+ // If there's a message, show it
+ if (data.message) {
+ console.log('Health check progress:', data.message);
+ }
+ }
+
+ /**
+ * Show health check error
+ */
+ function showHealthCheckError(message) {
+ // Hide progress bar
+ $('.notice.notice-info').hide();
+
+ // Show error message
+ var $errorNotice = $('Health Check Error: ' + message + '
');
+ $('.wrap h1').after($errorNotice);
+
+ // Auto-hide error message after 5 seconds
+ setTimeout(function() {
+ $errorNotice.fadeOut();
+ }, 5000);
+ }
+
+ /**
+ * Remove parameter from URL
+ */
+ function removeUrlParameter(url, parameter) {
+ var urlParts = url.split('?');
+ if (urlParts.length >= 2) {
+ var prefix = encodeURIComponent(parameter) + '=';
+ var params = urlParts[1].split(/[&;]/g);
+
+ for (var i = params.length; i-- > 0;) {
+ if (params[i].lastIndexOf(prefix, 0) !== -1) {
+ params.splice(i, 1);
+ }
+ }
+
+ return urlParts[0] + (params.length > 0 ? '?' + params.join('&') : '');
+ }
+ return url;
+ }
+
+ /**
+ * Manual stop health check
+ */
+ window.stopHealthCheck = function() {
+ if (confirm('Are you sure you want to stop the health check?')) {
+ $.ajax({
+ url: ajaxurl,
+ type: 'POST',
+ data: {
+ action: 'dm_stop_health_check',
+ nonce: ''
+ },
+ success: function(response) {
+ stopHealthCheckMonitoring('user_stopped');
+ location.reload();
+ }
+ });
+ }
+ };
+
+ // Clean up on page unload
+ $(window).on('beforeunload', function() {
+ if (healthCheckInterval) {
+ clearInterval(healthCheckInterval);
+ }
+ });
+ });
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+ domain );
+ $health_data = isset( $health_results[$domain_key] ) ? $health_results[$domain_key] : null;
+ $site_name = get_blog_option( $domain->blog_id, 'blogname', __( 'Unknown', 'wp-domain-mapping' ) );
+ ?>
+
+
+ domain ); ?>
+ active ) : ?>
+
+
+ |
+
+
+
+
+ original_domain . $domain->path ); ?>
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ start_health_check_batch();
+ // Clear any existing progress data
+ delete_site_option( 'dm_health_check_queue' );
+ delete_site_option( 'dm_health_check_progress' );
- // Redirect back to health page
- wp_redirect( add_query_arg( array( 'page' => 'domain-mapping', 'tab' => 'health', 'checking' => 1 ), network_admin_url( 'settings.php' ) ) );
+ // Start batch processing
+ $started = $this->start_health_check_batch();
+
+ if ( $started ) {
+ wp_redirect( add_query_arg( array(
+ 'page' => 'domain-mapping',
+ 'tab' => 'health',
+ 'checking' => 1
+ ), network_admin_url( 'settings.php' ) ) );
+ } else {
+ wp_redirect( add_query_arg( array(
+ 'page' => 'domain-mapping',
+ 'tab' => 'health',
+ 'error' => 'no_domains'
+ ), network_admin_url( 'settings.php' ) ) );
+ }
exit;
}
}
@@ -382,13 +407,13 @@ class WP_Domain_Mapping_Tools {
$health_notifications_enabled = isset( $_POST['health_notifications_enabled'] ) ? (bool) $_POST['health_notifications_enabled'] : false;
$notification_email = isset( $_POST['notification_email'] ) ? sanitize_email( $_POST['notification_email'] ) : '';
$ssl_expiry_threshold = isset( $_POST['ssl_expiry_threshold'] ) ? intval( $_POST['ssl_expiry_threshold'] ) : 14;
- $batch_size = isset( $_POST['health_check_batch_size'] ) ? intval( $_POST['health_check_batch_size'] ) : 10;
+ $batch_size = isset( $_POST['health_check_batch_size'] ) ? intval( $_POST['health_check_batch_size'] ) : 5;
update_site_option( 'dm_health_check_enabled', $health_check_enabled );
update_site_option( 'dm_health_notifications_enabled', $health_notifications_enabled );
update_site_option( 'dm_notification_email', $notification_email );
update_site_option( 'dm_ssl_expiry_threshold', $ssl_expiry_threshold );
- update_site_option( 'dm_health_check_batch_size', max( 5, min( 50, $batch_size ) ) );
+ update_site_option( 'dm_health_check_batch_size', max( 3, min( 20, $batch_size ) ) );
// If auto check is enabled, ensure cron is set
if ( $health_check_enabled ) {
@@ -468,12 +493,50 @@ class WP_Domain_Mapping_Tools {
wp_send_json_error( __( 'Security check failed.', 'wp-domain-mapping' ) );
}
+ // Check if there's a health check in progress
+ $progress = get_site_option( 'dm_health_check_progress', false );
+ $queue = get_site_option( 'dm_health_check_queue', array() );
+
+ if ( $progress === false && empty( $queue ) ) {
+ wp_send_json_success( array(
+ 'complete' => true,
+ 'message' => __( 'No health check in progress.', 'wp-domain-mapping' )
+ ) );
+ return;
+ }
+
// Process next batch
$result = $this->process_health_check_batch( true );
wp_send_json_success( $result );
}
+ /**
+ * AJAX handler to stop health check
+ */
+ public function ajax_stop_health_check() {
+ // Check permissions
+ if ( ! current_user_can( 'manage_network' ) ) {
+ wp_send_json_error( __( 'You do not have sufficient permissions to perform this action.', 'wp-domain-mapping' ) );
+ }
+
+ // Verify nonce
+ if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'dm_stop_health_check' ) ) {
+ wp_send_json_error( __( 'Security check failed.', 'wp-domain-mapping' ) );
+ }
+
+ // Clear health check progress data
+ delete_site_option( 'dm_health_check_queue' );
+ delete_site_option( 'dm_health_check_progress' );
+
+ // Clear related cron tasks
+ wp_clear_scheduled_hook( 'dm_domain_health_check_batch' );
+
+ error_log( 'WP Domain Mapping: Health check stopped by user' );
+
+ wp_send_json_success( __( 'Health check stopped successfully.', 'wp-domain-mapping' ) );
+ }
+
/**
* Scheduled health check - improved version
*/
@@ -502,30 +565,44 @@ class WP_Domain_Mapping_Tools {
private function start_health_check_batch() {
global $wpdb;
- // Get all domains
- $domains = $wpdb->get_col( "SELECT domain FROM {$this->tables['domains']}" );
+ // Clear any existing progress data
+ delete_site_option( 'dm_health_check_queue' );
+ delete_site_option( 'dm_health_check_progress' );
+
+ // Get all domains to check
+ $domains = array();
+
+ // Get mapped domains
+ $mapped_domains = $wpdb->get_col( "SELECT domain FROM {$this->tables['domains']}" );
+ if ( $mapped_domains ) {
+ $domains = array_merge( $domains, $mapped_domains );
+ }
// Add original domains for sites without mapped domains (excluding main site)
- $sites = get_sites( array( 'number' => 0 ) );
- foreach ( $sites as $site ) {
- // Skip main site
- if ( is_main_site( $site->blog_id ) ) {
- continue;
- }
+ $sites = get_sites( array(
+ 'number' => 0,
+ 'site__not_in' => array( get_main_site_id() ) // Exclude main site
+ ) );
+ foreach ( $sites as $site ) {
// Check if this site has a mapped domain
$has_mapped = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$this->tables['domains']} WHERE blog_id = %d",
$site->blog_id
));
+ // If no mapped domain, add original domain
if ( ! $has_mapped ) {
$domains[] = $site->domain;
}
}
+ // Remove duplicates
+ $domains = array_unique( $domains );
+
if ( empty( $domains ) ) {
- return;
+ error_log( 'WP Domain Mapping: No domains found for health check' );
+ return false;
}
// Store domains in queue
@@ -539,6 +616,9 @@ class WP_Domain_Mapping_Tools {
// Schedule first batch
wp_schedule_single_event( time() + 1, 'dm_domain_health_check_batch' );
+
+ error_log( 'WP Domain Mapping: Health check batch started with ' . count( $domains ) . ' domains' );
+ return true;
}
/**
@@ -549,10 +629,16 @@ class WP_Domain_Mapping_Tools {
*/
public function process_health_check_batch( $return_data = false ) {
$queue = get_site_option( 'dm_health_check_queue', array() );
- $progress = get_site_option( 'dm_health_check_progress', array() );
+ $progress = get_site_option( 'dm_health_check_progress', array(
+ 'total' => 0,
+ 'processed' => 0,
+ 'started' => current_time( 'mysql' ),
+ 'issues' => array()
+ ) );
+ // If queue is empty, complete the process
if ( empty( $queue ) ) {
- // Batch processing complete
+ // Send notification if there are issues and notifications are enabled
if ( ! empty( $progress['issues'] ) && get_site_option( 'dm_health_notifications_enabled', true ) ) {
$this->send_health_notification( $progress['issues'] );
}
@@ -561,21 +647,26 @@ class WP_Domain_Mapping_Tools {
delete_site_option( 'dm_health_check_queue' );
delete_site_option( 'dm_health_check_progress' );
+ // Update last check time
+ update_site_option( 'dm_last_health_check', current_time( 'timestamp' ) );
+
if ( $return_data ) {
return array(
'complete' => true,
- 'message' => __( 'Health check completed.', 'wp-domain-mapping' )
+ 'total' => isset($progress['total']) ? $progress['total'] : 0,
+ 'processed' => isset($progress['processed']) ? $progress['processed'] : 0,
+ 'message' => __( 'Health check completed successfully.', 'wp-domain-mapping' )
);
}
return;
}
// Get batch size
- $batch_size = get_site_option( 'dm_health_check_batch_size', 10 );
+ $batch_size = get_site_option( 'dm_health_check_batch_size', 5 ); // Reduced default batch size
// Process batch
$batch = array_splice( $queue, 0, $batch_size );
- $processed = 0;
+ $processed_in_batch = 0;
// Get main site domain to skip it
$main_site = get_site( get_main_site_id() );
@@ -584,43 +675,104 @@ class WP_Domain_Mapping_Tools {
foreach ( $batch as $domain ) {
// Skip main site domain
if ( $domain === $main_domain ) {
- $processed++;
$progress['processed']++;
+ $processed_in_batch++;
continue;
}
- $result = $this->check_domain_health( $domain );
- dm_save_health_result( $domain, $result );
+ try {
+ // Execute health check with timeout control
+ $start_time = microtime(true);
+ $result = $this->check_domain_health_with_timeout( $domain, 10 ); // 10 second timeout
+ $end_time = microtime(true);
- // Check for issues
- if ( $this->has_health_issues( $result ) ) {
- $progress['issues'][$domain] = $result;
+ // Record check duration
+ $result['check_duration'] = round($end_time - $start_time, 2);
+
+ // Save result
+ dm_save_health_result( $domain, $result );
+
+ // Check for issues
+ if ( $this->has_health_issues( $result ) ) {
+ $progress['issues'][$domain] = $result;
+ }
+
+ } catch ( Exception $e ) {
+ // Log error but continue processing
+ error_log( 'WP Domain Mapping: Health check error for ' . $domain . ': ' . $e->getMessage() );
+
+ // Save error result
+ $error_result = array(
+ 'domain' => $domain,
+ 'last_check' => current_time( 'mysql' ),
+ 'error' => $e->getMessage(),
+ 'dns_status' => 'error',
+ 'ssl_valid' => false,
+ 'accessible' => false
+ );
+ dm_save_health_result( $domain, $error_result );
}
- $processed++;
$progress['processed']++;
+ $processed_in_batch++;
+
+ // Prevent memory leaks
+ if ( function_exists( 'wp_cache_flush' ) ) {
+ wp_cache_flush();
+ }
}
// Update queue and progress
update_site_option( 'dm_health_check_queue', $queue );
update_site_option( 'dm_health_check_progress', $progress );
- // Schedule next batch if needed
+ // If there are still unprocessed domains, schedule next batch
if ( ! empty( $queue ) && ! $return_data ) {
- wp_schedule_single_event( time() + 2, 'dm_domain_health_check_batch' );
+ wp_schedule_single_event( time() + 3, 'dm_domain_health_check_batch' ); // 3 second interval
}
if ( $return_data ) {
+ $remaining = count( $queue );
+ $percentage = $progress['total'] > 0 ? round( ( $progress['processed'] / $progress['total'] ) * 100 ) : 100;
+
return array(
'complete' => empty( $queue ),
'total' => $progress['total'],
'processed' => $progress['processed'],
- 'remaining' => count( $queue ),
- 'percentage' => round( ( $progress['processed'] / $progress['total'] ) * 100 )
+ 'remaining' => $remaining,
+ 'percentage' => $percentage,
+ 'processed_in_batch' => $processed_in_batch,
+ 'message' => sprintf(
+ __( 'Processed %d of %d domains (%d%% complete)', 'wp-domain-mapping' ),
+ $progress['processed'],
+ $progress['total'],
+ $percentage
+ )
);
}
}
+ /**
+ * Domain health check with timeout control
+ */
+ private function check_domain_health_with_timeout( $domain, $timeout = 10 ) {
+ // Set maximum execution time
+ $original_time_limit = ini_get('max_execution_time');
+ if ( function_exists('set_time_limit') ) {
+ set_time_limit( $timeout + 5 );
+ }
+
+ try {
+ $result = $this->check_domain_health( $domain );
+ return $result;
+ } finally {
+ // Restore original time limit
+ if ( function_exists('set_time_limit') && $original_time_limit ) {
+ set_time_limit( $original_time_limit );
+ }
+ }
+ }
+
/**
* Check domain health
*
@@ -696,45 +848,64 @@ class WP_Domain_Mapping_Tools {
}
/**
- * Test domain connection
+ * Test domain connection - improved with better timeout control
*
* @param string $domain Domain to test
* @return array|false Connection test result
*/
private function test_domain_connection( $domain ) {
if ( ! function_exists( 'curl_init' ) ) {
- return false;
+ return array(
+ 'accessible' => false,
+ 'response_code' => 0,
+ 'ssl_valid' => false,
+ 'ssl_expiry' => '',
+ 'error' => 'cURL not available'
+ );
}
$result = array(
'accessible' => false,
'response_code' => 0,
'ssl_valid' => false,
- 'ssl_expiry' => ''
+ 'ssl_expiry' => '',
+ 'error' => ''
);
// Test HTTPS connection
$ch = curl_init( 'https://' . $domain );
- curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
- curl_setopt( $ch, CURLOPT_HEADER, true );
- curl_setopt( $ch, CURLOPT_NOBODY, true );
- curl_setopt( $ch, CURLOPT_TIMEOUT, 10 );
- curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
- curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
- curl_setopt( $ch, CURLOPT_CERTINFO, true );
+ curl_setopt_array( $ch, array(
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HEADER => true,
+ CURLOPT_NOBODY => true,
+ CURLOPT_TIMEOUT => 8, // Reduced timeout
+ CURLOPT_CONNECTTIMEOUT => 5, // Connection timeout
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_SSL_VERIFYHOST => 2,
+ CURLOPT_CERTINFO => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 3,
+ CURLOPT_USERAGENT => 'WP Domain Mapping Health Check/1.0'
+ ));
$response = curl_exec( $ch );
$response_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ $ssl_error = curl_error( $ch );
+
$result['response_code'] = $response_code;
if ( $response !== false && $response_code > 0 ) {
$result['accessible'] = ( $response_code >= 200 && $response_code < 400 );
- $result['ssl_valid'] = ( $response !== false );
+ $result['ssl_valid'] = empty( $ssl_error );
// Get SSL certificate info
- $cert_info = curl_getinfo( $ch, CURLINFO_CERTINFO );
- if ( ! empty( $cert_info ) && isset( $cert_info[0]['Expire date'] ) ) {
- $result['ssl_expiry'] = $cert_info[0]['Expire date'];
+ if ( $result['ssl_valid'] ) {
+ $cert_info = curl_getinfo( $ch, CURLINFO_CERTINFO );
+ if ( ! empty( $cert_info ) && isset( $cert_info[0]['Expire date'] ) ) {
+ $result['ssl_expiry'] = $cert_info[0]['Expire date'];
+ }
+ } else {
+ $result['error'] = $ssl_error;
}
}
@@ -743,17 +914,26 @@ class WP_Domain_Mapping_Tools {
// If HTTPS failed, try HTTP
if ( ! $result['accessible'] ) {
$ch = curl_init( 'http://' . $domain );
- curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
- curl_setopt( $ch, CURLOPT_HEADER, true );
- curl_setopt( $ch, CURLOPT_NOBODY, true );
- curl_setopt( $ch, CURLOPT_TIMEOUT, 10 );
+ curl_setopt_array( $ch, array(
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_HEADER => true,
+ CURLOPT_NOBODY => true,
+ CURLOPT_TIMEOUT => 8,
+ CURLOPT_CONNECTTIMEOUT => 5,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 3,
+ CURLOPT_USERAGENT => 'WP Domain Mapping Health Check/1.0'
+ ));
$response = curl_exec( $ch );
$response_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ $http_error = curl_error( $ch );
if ( $response !== false && $response_code > 0 ) {
$result['accessible'] = ( $response_code >= 200 && $response_code < 400 );
$result['response_code'] = $response_code;
+ } else {
+ $result['error'] = $http_error;
}
curl_close( $ch );
@@ -864,7 +1044,30 @@ class WP_Domain_Mapping_Tools {
}
/**
- * Add new method: check cron status
+ * Clean up stuck health checks
+ */
+ public function cleanup_stuck_health_check() {
+ $progress = get_site_option( 'dm_health_check_progress', false );
+
+ if ( $progress !== false ) {
+ $started_time = strtotime( $progress['started'] );
+ $current_time = current_time( 'timestamp' );
+
+ // If health check running for more than 30 minutes, consider it stuck
+ if ( ( $current_time - $started_time ) > 1800 ) {
+ delete_site_option( 'dm_health_check_queue' );
+ delete_site_option( 'dm_health_check_progress' );
+
+ error_log( 'WP Domain Mapping: Cleaned up stuck health check that started at ' . $progress['started'] );
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get cron status
*/
public function get_cron_status() {
$scheduled_info = get_site_option( 'dm_health_check_scheduled', false );
@@ -995,7 +1198,7 @@ class WP_Domain_Mapping_Tools {
}
/**
- * AJAX handler for CSV import
+ * AJAX handler for CSV import - FIXED
*/
public function ajax_import_csv() {
// Check permissions
@@ -1008,15 +1211,84 @@ class WP_Domain_Mapping_Tools {
wp_send_json_error( __( 'Invalid security token. Please try again.', 'wp-domain-mapping' ) );
}
+ // Check if file was uploaded
+ if ( ! isset( $_FILES['csv_file'] ) || $_FILES['csv_file']['error'] != UPLOAD_ERR_OK ) {
+ wp_send_json_error( __( 'No file uploaded or upload error.', 'wp-domain-mapping' ) );
+ }
+
// Check file size before processing
if ($_FILES['csv_file']['size'] > 5 * 1024 * 1024) {
wp_send_json_error(__('File size exceeds 5MB limit.', 'wp-domain-mapping'));
return;
}
- // Check file
- if ( ! isset( $_FILES['csv_file'] ) || $_FILES['csv_file']['error'] != UPLOAD_ERR_OK ) {
- wp_send_json_error( __( 'No file uploaded or upload error.', 'wp-domain-mapping' ) );
+ // Improved file type validation
+ $uploaded_filename = $_FILES['csv_file']['name'];
+ $file_extension = strtolower(pathinfo($uploaded_filename, PATHINFO_EXTENSION));
+
+ // Check file extension
+ if (!in_array($file_extension, array('csv', 'txt'))) {
+ wp_send_json_error(__('Only CSV files are allowed. File extension detected: ' . $file_extension, 'wp-domain-mapping'));
+ return;
+ }
+
+ // Additional MIME type check (more reliable)
+ $allowed_mime_types = array(
+ 'text/csv',
+ 'text/plain',
+ 'application/csv',
+ 'application/excel',
+ 'application/vnd.ms-excel',
+ 'application/vnd.msexcel'
+ );
+
+ $file_mime_type = '';
+
+ // Method 1: Use finfo if available (most reliable)
+ if (function_exists('finfo_open')) {
+ $finfo = finfo_open(FILEINFO_MIME_TYPE);
+ $file_mime_type = finfo_file($finfo, $_FILES['csv_file']['tmp_name']);
+ finfo_close($finfo);
+ }
+ // Method 2: Fallback to mime_content_type if available
+ elseif (function_exists('mime_content_type')) {
+ $file_mime_type = mime_content_type($_FILES['csv_file']['tmp_name']);
+ }
+ // Method 3: Use WordPress built-in function
+ else {
+ $file_type_check = wp_check_filetype_and_ext($_FILES['csv_file']['tmp_name'], $uploaded_filename);
+ $file_mime_type = $file_type_check['type'];
+ }
+
+ // Validate MIME type (but be more lenient since CSV files can have various MIME types)
+ if (!empty($file_mime_type) && !in_array($file_mime_type, $allowed_mime_types)) {
+ // Additional check: try to detect if file content looks like CSV
+ $file_handle = fopen($_FILES['csv_file']['tmp_name'], 'r');
+ if ($file_handle) {
+ $first_line = fgets($file_handle);
+ fclose($file_handle);
+
+ // Basic CSV content check - look for common CSV patterns
+ $csv_patterns = array(
+ '/,/', // Contains commas
+ '/;/', // Contains semicolons (common in European CSV)
+ '/\t/', // Contains tabs (TSV)
+ '/\r\n|\r|\n/' // Contains line breaks
+ );
+
+ $looks_like_csv = false;
+ foreach ($csv_patterns as $pattern) {
+ if (preg_match($pattern, $first_line)) {
+ $looks_like_csv = true;
+ break;
+ }
+ }
+
+ if (!$looks_like_csv) {
+ wp_send_json_error(__('File does not appear to be a valid CSV format. MIME type detected: ' . $file_mime_type, 'wp-domain-mapping'));
+ return;
+ }
+ }
}
// Get options
@@ -1024,19 +1296,20 @@ class WP_Domain_Mapping_Tools {
$update_existing = isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false;
$validate_sites = isset( $_POST['validate_sites'] ) ? (bool) $_POST['validate_sites'] : true;
- // Validate file type
- $file_type = wp_check_filetype($_FILES['csv_file']['name']);
- if (!in_array($file_type['ext'], array('csv', 'txt'))) {
- wp_send_json_error(__('Only CSV files are allowed.', 'wp-domain-mapping'));
- return;
- }
-
// Open file
$file = fopen( $_FILES['csv_file']['tmp_name'], 'r' );
if ( ! $file ) {
wp_send_json_error( __( 'Could not open the uploaded file.', 'wp-domain-mapping' ) );
}
+ // Check for UTF-8 BOM and remove it
+ $first_bytes = fread($file, 3);
+ if ($first_bytes !== "\xEF\xBB\xBF") {
+ // If no BOM found, rewind to beginning
+ rewind($file);
+ }
+ // If BOM found, it's already skipped
+
// Initialize counters and log
$imported = 0;
$skipped = 0;
@@ -1048,28 +1321,42 @@ class WP_Domain_Mapping_Tools {
// Skip header row
if ( $has_header ) {
- fgetcsv( $file );
+ $header_row = fgetcsv( $file );
+ if ($header_row === false) {
+ fclose($file);
+ wp_send_json_error(__('Could not read CSV file or file is empty.', 'wp-domain-mapping'));
+ return;
+ }
}
// Process each row
$row_num = $has_header ? 2 : 1; // Account for header row
+ $processed_rows = 0;
while ( ( $data = fgetcsv( $file ) ) !== false ) {
+ $processed_rows++;
+
+ // Skip empty rows
+ if (empty($data) || (count($data) == 1 && empty($data[0]))) {
+ $row_num++;
+ continue;
+ }
+
// Check data format
if ( count( $data ) < 3 ) {
$log[] = array(
'status' => 'error',
- 'message' => sprintf( __( 'Row %d: Invalid format. Expected at least 3 columns.', 'wp-domain-mapping' ), $row_num )
+ 'message' => sprintf( __( 'Row %d: Invalid format. Expected at least 3 columns, got %d.', 'wp-domain-mapping' ), $row_num, count($data) )
);
$errors++;
$row_num++;
continue;
}
- // Parse data
- $blog_id = intval( $data[0] );
+ // Parse data with trimming
+ $blog_id = intval( trim($data[0]) );
$domain = dm_clean_domain( trim( $data[1] ) );
- $active = intval( $data[2] );
+ $active = intval( trim($data[2]) );
// Check if we've already processed this domain
if ( isset( $processed_domains[$domain] ) ) {
@@ -1178,6 +1465,14 @@ class WP_Domain_Mapping_Tools {
}
$row_num++;
+
+ // Prevent timeout on large files
+ if ($processed_rows % 100 == 0) {
+ // Flush output and extend time limit
+ if (function_exists('set_time_limit')) {
+ set_time_limit(30);
+ }
+ }
}
fclose( $file );
@@ -1193,7 +1488,8 @@ class WP_Domain_Mapping_Tools {
'imported' => $imported,
'skipped' => $skipped,
'errors' => $errors,
- 'details' => $log
+ 'details' => $log,
+ 'processed_rows' => $processed_rows
) );
}
}