db = $db; // Legacy single-shot import (v1.0+). Kept for back-compat; in // practice the JS now uses the batched parse+batch flow added in // v1.7.0. Slated for removal after one release. add_action('wp_ajax_studiou_wcpcm_import', array($this, 'handle_import_ajax')); // v1.7.0 — M1: batched category import. Same shape as the product // importer: parse stages rows to disk, batch processes N at a time // by offset, JS polls until processed >= total. Removes the ~60s // PHP-timeout cliff on large category trees. add_action('wp_ajax_studiou_wcpcm_import_parse', array($this, 'handle_import_parse_ajax')); add_action('wp_ajax_studiou_wcpcm_import_batch', array($this, 'handle_import_batch_ajax')); } /** * v1.7.0 — Step 1: parse the uploaded CSV, validate headers, stage rows * on disk. Returns the staging token + total count. */ public function handle_import_parse_ajax() { while (ob_get_level()) { ob_end_clean(); } if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) { wp_send_json_error(array('message' => __('Security check failed', 'studiou-wc-product-cat-manage'))); } if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage'))); } if (!isset($_FILES['import_file']) || empty($_FILES['import_file']['tmp_name'])) { wp_send_json_error(array('message' => __('No file was uploaded', 'studiou-wc-product-cat-manage'))); } try { $file = fopen($_FILES['import_file']['tmp_name'], 'r'); if (!$file) { throw new Exception(__('Failed to open the uploaded file', 'studiou-wc-product-cat-manage')); } $bom = fread($file, 3); if ($bom !== "\xEF\xBB\xBF") { rewind($file); } $headers = fgetcsv($file); if (!$headers) { fclose($file); throw new Exception(__('Failed to read CSV headers', 'studiou-wc-product-cat-manage')); } $this->validate_headers($headers); $header_count = count($headers); $rows = array(); $row_number = 1; while (($row = fgetcsv($file)) !== false) { $row_number++; if (empty(array_filter($row, function($v) { return $v !== '' && $v !== null; }))) { continue; } $rc = count($row); if ($rc < $header_count) { $row = array_pad($row, $header_count, ''); } elseif ($rc > $header_count) { $row = array_slice($row, 0, $header_count); } $data = array(); foreach ($headers as $i => $h) { $data[$h] = isset($row[$i]) ? $row[$i] : ''; } $rows[] = array('row_number' => $row_number, 'data' => $data); } fclose($file); if (empty($rows)) { wp_send_json_error(array('message' => __('CSV contains no data rows.', 'studiou-wc-product-cat-manage'))); } $token = Studiou_WC_Product_Manage_Product_Import::stage_rows($rows, 'category'); if ($token === false) { wp_send_json_error(array('message' => __('Failed to stage import data on disk (write error).', 'studiou-wc-product-cat-manage'))); } wp_send_json_success(array( 'staging_key' => $token, 'total' => count($rows), )); } catch (\Throwable $e) { error_log('STUDIOU WC Category Import Parse Error: ' . $e->getMessage()); wp_send_json_error(array('message' => $e->getMessage())); } } /** * v1.7.0 — Step 2: process a batch of N rows from offset. JS polls until * processed >= total. */ public function handle_import_batch_ajax() { while (ob_get_level()) { ob_end_clean(); } if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) { wp_send_json_error(array('message' => __('Security check failed', 'studiou-wc-product-cat-manage'))); } if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage'))); } $staging_key = isset($_POST['staging_key']) ? sanitize_text_field($_POST['staging_key']) : ''; $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0; $batch_size = 10; // category ops are cheap; 10 per batch keeps each AJAX call well under PHP timeout if ($staging_key === '') { wp_send_json_error(array('message' => __('Invalid request', 'studiou-wc-product-cat-manage'))); } try { $slice = Studiou_WC_Product_Manage_Product_Import::read_stage_slice($staging_key, $offset, $batch_size); if ($slice === null) { wp_send_json_error(array('message' => __('Import data not found or expired', 'studiou-wc-product-cat-manage'))); } $messages = array(); $success = 0; $errors = 0; foreach ($slice['batch'] as $item) { $row_number = $item['row_number']; $data = $item['data']; try { $this->validate_row_data($data, $row_number); $operation = strtoupper(trim($data['operation'])); switch ($operation) { case 'A': $r = $this->db->create_category($data); break; case 'U': $r = $this->db->update_category($data); break; case 'D': $r = $this->db->delete_category($data['name']); break; default: throw new Exception(sprintf(__('Invalid operation "%s" at row %d', 'studiou-wc-product-cat-manage'), $operation, $row_number)); } if (isset($r['status']) && $r['status'] === 'error') { $errors++; $messages[] = sprintf(__('Row %d: %s', 'studiou-wc-product-cat-manage'), $row_number, $r['message']); } else { $success++; $messages[] = sprintf(__('Row %d: %s', 'studiou-wc-product-cat-manage'), $row_number, isset($r['message']) ? $r['message'] : __('OK', 'studiou-wc-product-cat-manage')); } } catch (\Throwable $row_e) { $errors++; $messages[] = sprintf(__('Row %d: %s', 'studiou-wc-product-cat-manage'), $row_number, $row_e->getMessage()); } } $processed = $offset + count($slice['batch']); $is_final = $processed >= $slice['total']; if ($is_final) { Studiou_WC_Product_Manage_Product_Import::delete_stage($staging_key); } wp_send_json_success(array( 'processed' => $processed, 'total' => $slice['total'], 'success' => $success, 'errors' => $errors, 'messages' => $messages, 'done' => $is_final, )); } catch (\Throwable $e) { error_log('STUDIOU WC Category Import Batch Error: ' . $e->getMessage()); wp_send_json_error(array('message' => $e->getMessage())); } } /** * Handle AJAX import request */ public function handle_import_ajax() { // Check nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'studiou-wcpcm-nonce')) { wp_send_json_error(array( 'message' => __('Security check failed', 'studiou-wc-product-cat-manage') )); } // Check permissions if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array( 'message' => __('You do not have sufficient permissions', 'studiou-wc-product-cat-manage') )); } // Check if file was uploaded if (!isset($_FILES['import_file']) || empty($_FILES['import_file']['tmp_name'])) { wp_send_json_error(array( 'message' => __('No file was uploaded', 'studiou-wc-product-cat-manage') )); } try { // Process the uploaded file $results = $this->process_import_file($_FILES['import_file']['tmp_name']); // Generate response message $message = $this->generate_import_results_message($results); // Return success response wp_send_json_success(array( 'message' => $message )); } catch (Exception $e) { wp_send_json_error(array( 'message' => sprintf( __('Import error: %s', 'studiou-wc-product-cat-manage'), $e->getMessage() ) )); } } /** * Process import file * * @param string $file_path Path to uploaded CSV file * @return array Import results * @throws Exception On validation or processing error */ private function process_import_file($file_path) { // Open file $file = fopen($file_path, 'r'); if (!$file) { throw new Exception(__('Failed to open the uploaded file', 'studiou-wc-product-cat-manage')); } // Read and remove UTF-8 BOM if present $bom = fread($file, 3); if ($bom !== "\xEF\xBB\xBF") { // Not a BOM, rewind the file pointer rewind($file); } // Read headers $headers = fgetcsv($file); if (!$headers) { fclose($file); throw new Exception(__('Failed to read CSV headers', 'studiou-wc-product-cat-manage')); } // Validate headers $this->validate_headers($headers); // Process rows $results = array(); $row_number = 1; // Header row is 1 while (($row = fgetcsv($file)) !== false) { $row_number++; try { // Map row data to associative array $category_data = array(); foreach ($headers as $index => $header) { $category_data[$header] = isset($row[$index]) ? $row[$index] : ''; } // Validate row data $this->validate_row_data($category_data, $row_number); // Process based on operation $operation = strtoupper(trim($category_data['operation'])); switch ($operation) { case 'A': $result = $this->db->create_category($category_data); break; case 'U': $result = $this->db->update_category($category_data); break; case 'D': $result = $this->db->delete_category($category_data['name']); break; default: throw new Exception(sprintf( __('Invalid operation "%s" at row %d', 'studiou-wc-product-cat-manage'), $operation, $row_number )); } $results[] = $result; } catch (Exception $e) { $results[] = array( 'status' => 'error', 'message' => sprintf( __('Error at row %d: %s', 'studiou-wc-product-cat-manage'), $row_number, $e->getMessage() ) ); } } fclose($file); return $results; } /** * Validate CSV headers * * @param array $headers Headers from CSV * @throws Exception If headers are invalid */ private function validate_headers($headers) { // Check that all required headers are present foreach ($this->required_headers as $required_header) { if (!in_array($required_header, $headers)) { throw new Exception(sprintf( __('Missing required header: %s', 'studiou-wc-product-cat-manage'), $required_header )); } } } /** * Validate row data * * @param array $data Row data * @param int $row_number Row number for error messages * @throws Exception If data is invalid */ private function validate_row_data($data, $row_number) { // Check required fields based on operation $operation = strtoupper(trim($data['operation'])); if (!in_array($operation, $this->valid_operations)) { throw new Exception(sprintf( __('Invalid operation "%s" at row %d. Must be one of: A, U, D', 'studiou-wc-product-cat-manage'), $operation, $row_number )); } // Check name is required for all operations if (empty($data['name'])) { throw new Exception(sprintf( __('Missing required field "name" at row %d', 'studiou-wc-product-cat-manage'), $row_number )); } // Additional validations for Add and Update operations if ($operation === 'A' || $operation === 'U') { // URL name is required if (empty($data['url-name'])) { throw new Exception(sprintf( __('Missing required field "url-name" at row %d', 'studiou-wc-product-cat-manage'), $row_number )); } // Validate display type if (!empty($data['display-type']) && !in_array($data['display-type'], $this->valid_display_types)) { throw new Exception(sprintf( __('Invalid display type "%s" at row %d. Must be one of: default, products, subcategories, both', 'studiou-wc-product-cat-manage'), $data['display-type'], $row_number )); } // Validate visibility if (!empty($data['visibility']) && !in_array($data['visibility'], $this->valid_visibility)) { throw new Exception(sprintf( __('Invalid visibility "%s" at row %d. Must be one of: public, protected', 'studiou-wc-product-cat-manage'), $data['visibility'], $row_number )); } // If visibility is protected, passwords are required if (isset($data['visibility']) && $data['visibility'] === 'protected' && empty($data['protected-passwords'])) { throw new Exception(sprintf( __('Protected visibility requires at least one password at row %d', 'studiou-wc-product-cat-manage'), $row_number )); } } } /** * Generate import results message * * @param array $results Import results * @return string Formatted message */ private function generate_import_results_message($results) { if (empty($results)) { return __('No items were processed.', 'studiou-wc-product-cat-manage'); } $count = count($results); $message = sprintf(__('Imported %d items:', 'studiou-wc-product-cat-manage'), $count) . "\n"; foreach ($results as $result) { $message .= "\t" . $result['message'] . "\n"; } return $message; } }