load_dependencies(); // Check required plugins add_action('admin_init', array($this, 'check_required_plugins')); // Add admin menu items add_action('admin_menu', array($this, 'add_admin_menu'), 99); // Register admin assets add_action('admin_enqueue_scripts', array($this, 'register_admin_assets')); // Initialize components $this->init_components(); // Declare WooCommerce HPOS compatibility add_action('before_woocommerce_init', array($this, 'declare_hpos_compatibility')); } /** * Declare compatibility with WooCommerce HPOS (High-Performance Order Storage) */ public function declare_hpos_compatibility() { if (class_exists('Automattic\WooCommerce\Utilities\FeaturesUtil')) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); } } /** * Load required dependencies */ private function load_dependencies() { require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-cat-manage-db.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-cat-manage-export.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-cat-manage-import.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-cat-manage-manipulator.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-manage-product-db.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-manage-product-import.php'; require_once STUDIOU_WCPCM_PLUGIN_DIR . 'includes/class-studiou-wc-product-manage-product-export.php'; } /** * Initialize components */ private function init_components() { $this->db = new Studiou_WC_Product_Cat_Manage_DB(); $this->exporter = new Studiou_WC_Product_Cat_Manage_Export($this->db); $this->importer = new Studiou_WC_Product_Cat_Manage_Import($this->db); $this->manipulator = new Studiou_WC_Product_Cat_Manage_Manipulator($this->db); $this->product_db = new Studiou_WC_Product_Manage_Product_DB(); $this->product_importer = new Studiou_WC_Product_Manage_Product_Import($this->product_db); $this->product_exporter = new Studiou_WC_Product_Manage_Product_Export($this->product_db); // Register AJAX handlers add_action('wp_ajax_studiou_wcpcm_product_import_parse', array($this, 'handle_product_import_parse')); add_action('wp_ajax_studiou_wcpcm_product_import_batch', array($this, 'handle_product_import_batch')); add_action('wp_ajax_studiou_wcpcm_product_export', array($this, 'handle_product_export')); } /** * Check if required plugins are active */ public function check_required_plugins() { if (!is_admin() || isset($_GET['activate-multi'])) { return; } // Check for WooCommerce if (!class_exists('WooCommerce') || !defined('WC_VERSION') || version_compare(WC_VERSION, '9.8', '<')) { add_action('admin_notices', array($this, 'woocommerce_missing_notice')); deactivate_plugins(STUDIOU_WCPCM_PLUGIN_BASENAME); if (isset($_GET['activate'])) { unset($_GET['activate']); } } } /** * WooCommerce missing notice */ public function woocommerce_missing_notice() { ?>

', '' ); ?>

get_cache_busted_asset_url('assets/css/admin', 'css'), array(), STUDIOU_WCPCM_VERSION ); wp_enqueue_script( 'studiou-wcpcm-admin', $this->get_cache_busted_asset_url('assets/js/admin', 'js'), array('jquery'), STUDIOU_WCPCM_VERSION, true ); wp_localize_script('studiou-wcpcm-admin', 'studiouWcpcm', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('studiou-wcpcm-nonce'), 'i18n' => array( 'importSuccess' => __('Import successful', 'studiou-wc-product-cat-manage'), 'importError' => __('Import error', 'studiou-wc-product-cat-manage'), 'exportSuccess' => __('Export successful', 'studiou-wc-product-cat-manage'), 'exportError' => __('Export error', 'studiou-wc-product-cat-manage'), 'moveSuccess' => __('Move operation successful', 'studiou-wc-product-cat-manage'), 'moveError' => __('Move operation error', 'studiou-wc-product-cat-manage'), 'selectDifferentCategories' => __('Please select different source and target categories', 'studiou-wc-product-cat-manage'), 'batchDescriptionSuccess' => __('Batch description operation successful', 'studiou-wc-product-cat-manage'), 'batchDescriptionError' => __('Batch description operation error', 'studiou-wc-product-cat-manage'), 'deleteSuccess' => __('Delete operation successful', 'studiou-wc-product-cat-manage'), 'deleteError' => __('Delete operation error', 'studiou-wc-product-cat-manage'), 'confirmDelete' => __('Are you sure you want to permanently delete ALL products in the selected categories? This action cannot be undone!', 'studiou-wc-product-cat-manage'), 'selectAtLeastOneCategory' => __('Please select at least one category', 'studiou-wc-product-cat-manage'), 'noProductsFound' => __('No products found in selected categories', 'studiou-wc-product-cat-manage'), 'confirmClearMedia' => __('Are you sure you want to permanently delete ALL media files in the selected media categories? This action cannot be undone!', 'studiou-wc-product-cat-manage'), 'clearMediaSuccess' => __('Clear media operation successful', 'studiou-wc-product-cat-manage'), 'clearMediaError' => __('Clear media operation error', 'studiou-wc-product-cat-manage'), 'noMediaFound' => __('No media files found in selected categories', 'studiou-wc-product-cat-manage'), 'confirmRemoveUnassigned' => __('Are you sure you want to permanently delete ALL unused uncategorized media files? This action cannot be undone!', 'studiou-wc-product-cat-manage'), 'removeUnassignedSuccess' => __('Remove unassigned media operation successful', 'studiou-wc-product-cat-manage'), 'removeUnassignedError' => __('Remove unassigned media operation error', 'studiou-wc-product-cat-manage'), 'noUnassignedMedia' => __('No unused uncategorized media files found', 'studiou-wc-product-cat-manage'), 'upvpSelectAttribute' => __('-- Select attribute --', 'studiou-wc-product-cat-manage'), 'upvpSelectValue' => __('-- Select value --', 'studiou-wc-product-cat-manage'), 'upvpInvalidPrice' => __('Please enter a valid new price', 'studiou-wc-product-cat-manage'), 'upvpLoadingAttributes' => __('Loading attributes...', 'studiou-wc-product-cat-manage'), 'upvpLoadingValues' => __('Loading values...', 'studiou-wc-product-cat-manage'), 'upvpDryRunComplete' => __('Dry run complete: %d variations would be updated to %s.', 'studiou-wc-product-cat-manage'), 'upvpApplySuccess' => __('Variant prices updated successfully', 'studiou-wc-product-cat-manage'), 'upvpApplyError' => __('Variant prices update error', 'studiou-wc-product-cat-manage'), 'upvpNoMatches' => __('No variations match the current filter.', 'studiou-wc-product-cat-manage'), 'upvpConfirmApply' => __('Are you sure you want to update %d variations?', 'studiou-wc-product-cat-manage'), 'tabLoading' => __('Loading...', 'studiou-wc-product-cat-manage'), 'tabLoadError' => __('Failed to load tab', 'studiou-wc-product-cat-manage'), // v1.7.1 — L1: keys for JS strings that were previously hardcoded English. 'selectBothCategories' => __('Please select both source and target categories', 'studiou-wc-product-cat-manage'), 'moveNoProducts' => __('No products to move.', 'studiou-wc-product-cat-manage'), 'importInitializing' => __('Initializing...', 'studiou-wc-product-cat-manage'), 'importComplete' => __('Import complete', 'studiou-wc-product-cat-manage'), 'deletionSummary' => __('Deletion Summary', 'studiou-wc-product-cat-manage'), 'importSummary' => __('Import Summary', 'studiou-wc-product-cat-manage'), ) )); if (defined('WP_DEBUG') && WP_DEBUG) { error_log('STUDIOU WC: Admin assets loaded successfully'); } } } /** * Build a cache-busting URL for a plugin asset. * * studiou.cz (and similar host optimisers) strip the `?ver=` query string * from static assets, so a version bump alone does not bust the browser * cache. To work around that, this serves the asset under a filename that * embeds the plugin version, e.g. assets/js/admin-1.6.0.js. The versioned * copy is generated automatically from the source file and refreshed * whenever the source changes, so no manual file renaming is needed. * * Falls back to the plain source URL if the copy cannot be written. * * @param string $rel_no_ext Asset path relative to the plugin dir, without * extension, e.g. 'assets/js/admin'. * @param string $ext File extension without the dot, e.g. 'js'. * @return string Absolute URL to the asset. */ private function get_cache_busted_asset_url($rel_no_ext, $ext) { $source_rel = $rel_no_ext . '.' . $ext; $source_abs = STUDIOU_WCPCM_PLUGIN_DIR . $source_rel; // Source missing - nothing we can do, return the plain URL. if (!file_exists($source_abs)) { return STUDIOU_WCPCM_PLUGIN_URL . $source_rel; } // v1.7.1 — L4: write the versioned copies under uploads/ instead of // the plugin directory. The plugin dir is conventionally read-only // on managed hosts and trips some security scanners when files // appear in it at runtime; uploads/ is the right place for runtime- // generated assets. The original source files in the plugin dir // are still the canonical edit targets — only the versioned copies // moved. $upload_dir = wp_upload_dir(); if (empty($upload_dir['error'])) { $cache_dir = trailingslashit($upload_dir['basedir']) . 'studiou-wcpcm-cache/' . dirname($rel_no_ext) . '/'; $cache_url = trailingslashit($upload_dir['baseurl']) . 'studiou-wcpcm-cache/' . dirname($rel_no_ext) . '/'; if (!file_exists($cache_dir)) { wp_mkdir_p($cache_dir); @file_put_contents(dirname($cache_dir) . '/index.php', ' filemtime($versioned_abs)) { // Remove stale versioned copies of this asset in the uploads cache. $old_files = glob($cache_dir . basename($rel_no_ext) . '-*.' . $ext); if (is_array($old_files)) { foreach ($old_files as $old_file) { @unlink($old_file); } } if (!@copy($source_abs, $versioned_abs)) { return STUDIOU_WCPCM_PLUGIN_URL . $source_rel; } } return $versioned_url; } // wp_upload_dir() failed — fall back to the plain source URL. return STUDIOU_WCPCM_PLUGIN_URL . $source_rel; } /** * Render import page */ public function render_import_page() { include STUDIOU_WCPCM_PLUGIN_DIR . 'views/import.php'; } /** * Render export page */ public function render_export_page() { include STUDIOU_WCPCM_PLUGIN_DIR . 'views/export.php'; } /** * Render manipulations page */ public function render_manipulations_page() { include STUDIOU_WCPCM_PLUGIN_DIR . 'views/manipulations.php'; } /** * Render product import page */ public function render_product_import_page() { include STUDIOU_WCPCM_PLUGIN_DIR . 'views/product-import.php'; } /** * Render product export page */ public function render_product_export_page() { include STUDIOU_WCPCM_PLUGIN_DIR . 'views/product-export.php'; } /** * Handle product import parse AJAX request (step 1: parse CSV) */ public function handle_product_import_parse() { // Clean any output buffers while (ob_get_level()) { ob_end_clean(); } // Verify 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'))); return; } // Check permissions if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'studiou-wc-product-cat-manage'))); return; } // Check if file was uploaded if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { wp_send_json_error(array('message' => __('No file uploaded or upload error', 'studiou-wc-product-cat-manage'))); return; } try { // Parse CSV and store in transient $result = $this->product_importer->parse_csv_for_batch($_FILES['import_file']['tmp_name']); if (isset($result['error'])) { wp_send_json_error(array('message' => $result['error'])); return; } wp_send_json_success(array( 'transient_key' => $result['transient_key'], 'total' => $result['total'] )); } catch (\Throwable $e) { // \Throwable (not Exception) — array_combine, type errors, and // other Error subclasses would otherwise escape as bare 500s. error_log('STUDIOU WC Product Import Parse Error: ' . $e->getMessage()); wp_send_json_error(array('message' => $e->getMessage())); } } /** * Handle product import batch AJAX request (step 2: process batch) */ public function handle_product_import_batch() { // Clean any output buffers while (ob_get_level()) { ob_end_clean(); } // Verify 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'))); return; } // Check permissions if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'studiou-wc-product-cat-manage'))); return; } // Get parameters $transient_key = isset($_POST['transient_key']) ? sanitize_text_field($_POST['transient_key']) : ''; $offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0; if (empty($transient_key)) { wp_send_json_error(array('message' => __('Invalid request', 'studiou-wc-product-cat-manage'))); return; } try { // Process batch $result = $this->product_importer->process_batch($transient_key, $offset, 5); if (isset($result['error'])) { wp_send_json_error(array('message' => $result['error'])); return; } // Generate failed CSV if this is the last batch and there are failed items $failed_csv = ''; if ($result['processed'] >= $result['total'] && !empty($result['failed_items'])) { $failed_csv = $this->product_importer->generate_failed_csv($result['failed_items']); } wp_send_json_success(array( 'success' => $result['success'], 'failed' => $result['failed'], 'skipped' => $result['skipped'], 'messages' => $result['messages'], 'processed' => $result['processed'], 'total' => $result['total'], 'failed_csv' => $failed_csv )); } catch (\Throwable $e) { error_log('STUDIOU WC Product Import Batch Error: ' . $e->getMessage()); wp_send_json_error(array('message' => $e->getMessage())); } } /** * Handle product export AJAX request */ public function handle_product_export() { // Clean any output buffers while (ob_get_level()) { ob_end_clean(); } // Verify 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'))); return; } // Check permissions if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('Insufficient permissions', 'studiou-wc-product-cat-manage'))); return; } // Get selected categories $category_ids = isset($_POST['export_categories']) ? array_map('intval', $_POST['export_categories']) : array(); if (empty($category_ids)) { wp_send_json_error(array('message' => __('No categories selected', 'studiou-wc-product-cat-manage'))); return; } try { // Generate export file $result = $this->product_exporter->generate_export_file($category_ids); if ($result['success']) { wp_send_json_success(array( 'message' => $result['message'], 'download_url' => $result['download_url'] )); } else { wp_send_json_error(array('message' => $result['message'])); } } catch (\Throwable $e) { error_log('STUDIOU WC Product Export Error: ' . $e->getMessage()); wp_send_json_error(array('message' => $e->getMessage())); } } } /** * Begin execution of the plugin */ function run_studiou_wc_product_cat_manage() { // Only initialize once static $initialized = false; if ($initialized) { return; } $initialized = true; // Load text domain first load_plugin_textdomain( 'studiou-wc-product-cat-manage', false, dirname(plugin_basename(__FILE__)) . '/languages/' ); // Create instance new Studiou_WC_Product_Cat_Manage(); } // Run the plugin after plugins are loaded to ensure WooCommerce is available add_action('plugins_loaded', 'run_studiou_wc_product_cat_manage');