| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- <?php
- /**
- * Import class
- *
- * Handles importing product categories from CSV
- *
- * @since 1.0.0
- * @package Studiou_WC_Product_Cat_Manage
- * @subpackage Studiou_WC_Product_Cat_Manage/includes
- */
- // If this file is called directly, abort.
- if (!defined('WPINC')) {
- die;
- }
- class Studiou_WC_Product_Cat_Manage_Import {
-
- /**
- * Database handler
- *
- * @var Studiou_WC_Product_Cat_Manage_DB
- */
- private $db;
-
- /**
- * Required CSV headers
- *
- * @var array
- */
- private $required_headers = array(
- 'name',
- 'url-name',
- 'parent-name',
- 'description',
- 'display-type',
- 'visibility',
- 'protected-passwords',
- 'operation'
- );
-
- /**
- * Valid display types
- *
- * @var array
- */
- private $valid_display_types = array(
- 'default',
- 'products',
- 'subcategories',
- 'both'
- );
-
- /**
- * Valid visibility values
- *
- * @var array
- */
- private $valid_visibility = array(
- 'public',
- 'protected'
- );
-
- /**
- * Valid operation values
- *
- * @var array
- */
- private $valid_operations = array(
- 'A', // Add
- 'U', // Update
- 'D' // Delete
- );
-
- /**
- * Constructor
- *
- * @param Studiou_WC_Product_Cat_Manage_DB $db Database handler
- */
- public function __construct($db) {
- $this->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;
- }
- }
|