db = $db; // v1.6.4 — gated download endpoint, mirroring the category export. add_action('admin_init', array($this, 'handle_export_download')); } /** * Return (and lazily create) the guarded export directory. * * @return string Absolute path with trailing slash. */ private function get_export_dir() { $upload_dir = wp_upload_dir(); $dir = trailingslashit($upload_dir['basedir']) . 'studiou-wcpcm-product-exports/'; if (!file_exists($dir)) { wp_mkdir_p($dir); } // Guard against direct access. Cheap idempotent writes. $idx = $dir . 'index.php'; if (!file_exists($idx)) { @file_put_contents($idx, 'get_export_dir(); $cutoff = time() - DAY_IN_SECONDS; $files = glob($dir . 'product-export-*.csv'); if (!is_array($files)) { return; } foreach ($files as $f) { $mtime = @filemtime($f); if ($mtime !== false && $mtime < $cutoff) { @unlink($f); } } } /** * v1.6.4 — gated CSV download. * * Hooked on admin_init. Verifies nonce + manage_woocommerce capability + * that the requested file is a bare basename inside the export dir * (defends against path traversal), then streams via readfile(). */ public function handle_export_download() { if (!isset($_GET['page']) || $_GET['page'] !== 'studiou-product-export') { return; } if (!isset($_GET['action']) || $_GET['action'] !== 'studiou_wcpcm_download_product_export') { return; } if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'studiou-wcpcm-product-export-download')) { return; } if (!current_user_can('manage_woocommerce')) { wp_die(__('You do not have sufficient permissions', 'studiou-wc-product-cat-manage')); } $requested = isset($_GET['file']) ? (string) $_GET['file'] : ''; // Bare basename only — no slashes, no .., no directory components. if ($requested === '' || $requested !== basename($requested) || strpos($requested, '..') !== false) { wp_die(__('Invalid file', 'studiou-wc-product-cat-manage')); } // Only our own filename shape: product-export-<32 hex>.csv if (!preg_match('/^product-export-[a-f0-9]{32}\.csv$/', $requested)) { wp_die(__('Invalid file', 'studiou-wc-product-cat-manage')); } $dir = $this->get_export_dir(); $path = $dir . $requested; if (!file_exists($path)) { wp_die(__('Export file not found. Please try exporting again.', 'studiou-wc-product-cat-manage')); } // Belt-and-braces realpath check to prevent symlink escape. $real_dir = realpath($dir); $real_path = realpath($path); if ($real_dir === false || $real_path === false || strpos($real_path, $real_dir) !== 0) { wp_die(__('Invalid file', 'studiou-wc-product-cat-manage')); } header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=product-export-' . date('Y-m-d-H-i-s') . '.csv'); header('Content-Length: ' . filesize($real_path)); header('Pragma: no-cache'); header('Expires: 0'); readfile($real_path); exit; } /** * Export products by category IDs * * @param array $category_ids Array of category IDs * @return string CSV content */ public function export_products($category_ids, &$row_count = null) { $product_ids = $this->db->get_products_by_categories($category_ids); if (empty($product_ids)) { return ''; } // Define CSV header $header = array( 'ID', 'Typ', 'Katalogové číslo', 'Jméno', 'Nadřazené', 'Krátký popis', 'Popis', 'Název 1 vlastnosti', 'Hodnota(y) 1 vlastnosti', 'Kategorie', 'Obrázky', 'Běžná cena', 'Vlastnost 1 viditelnost', 'Vlastnost 1 globální', 'Vlastnost 1 varianta', 'Povolit zákaznické recenze?', 'Minimum Quantity', 'Maximum Quantity' ); // Start CSV output $csv = ''; $csv .= '"' . implode('","', $header) . '"' . "\n"; // v1.7.1 — L2: accumulate the count during generation. The previous // substr_count("\n") - 1 over-counted whenever any field (typically // the product description) contained a newline. $count = 0; // Process each product foreach ($product_ids as $product_id) { $product = wc_get_product($product_id); if (!$product) { continue; } // Only export variable products and variations if (!$product->is_type('variable') && !$product->is_type('variation')) { continue; } $csv .= $this->format_product_row($product); $count++; } if (func_num_args() >= 2) { $row_count = $count; } return $csv; } /** * Format product data as CSV row * * @param WC_Product $product Product object * @return string CSV row */ private function format_product_row($product) { $row = array(); // ID $row[] = $product->get_id(); // Typ (Type) $row[] = $product->is_type('variation') ? 'variation' : 'variable'; // Katalogové číslo (SKU) $row[] = $product->get_sku(); // Jméno (Name) $row[] = $product->get_name(); // Nadřazené (Parent) - only for variations if ($product->is_type('variation')) { $parent_id = $product->get_parent_id(); if ($parent_id) { $parent = wc_get_product($parent_id); $row[] = $parent ? $parent->get_sku() : ''; } else { $row[] = ''; } } else { $row[] = ''; } // Krátký popis (Short description) $row[] = $product->get_short_description(); // Popis (Description) $row[] = $product->get_description(); // Název 1 vlastnosti (Attribute 1 name) // Hodnota(y) 1 vlastnosti (Attribute 1 value(s)) $attributes = $product->get_attributes(); $attribute_name = ''; $attribute_values = ''; $attribute_visible = '0'; $attribute_global = '0'; $attribute_variation = '0'; if (!empty($attributes)) { $first_attribute = reset($attributes); if ($first_attribute instanceof WC_Product_Attribute) { $attribute_name = $first_attribute->get_name(); // Remove 'pa_' prefix if present if (strpos($attribute_name, 'pa_') === 0) { $attribute_name = substr($attribute_name, 3); } // Get attribute values if ($product->is_type('variation')) { // For variations, get the specific value $variation_attributes = $product->get_variation_attributes(); if (!empty($variation_attributes)) { $attribute_values = reset($variation_attributes); } } else { // For variable products, get all options $options = $first_attribute->get_options(); if (is_array($options)) { $attribute_values = implode('|', $options); } } $attribute_visible = $first_attribute->get_visible() ? '1' : '0'; $attribute_global = $first_attribute->is_taxonomy() ? '1' : '0'; $attribute_variation = $first_attribute->get_variation() ? '1' : '0'; } } $row[] = $attribute_name; $row[] = $attribute_values; // Kategorie (Categories) - with full hierarchy for subcategories $category_ids = $product->get_category_ids(); $category_names = array(); foreach ($category_ids as $cat_id) { $category_path = $this->get_category_path($cat_id); if ($category_path) { $category_names[] = $category_path; } } $row[] = implode('|', $category_names); // Obrázky (Images) $image_id = $product->get_image_id(); $row[] = $image_id ? wp_get_attachment_url($image_id) : ''; // Běžná cena (Regular price) $row[] = $product->get_regular_price(); // Vlastnost 1 viditelnost (Attribute 1 visibility) $row[] = $attribute_visible; // Vlastnost 1 globální (Attribute 1 global) $row[] = $attribute_global; // Vlastnost 1 varianta (Attribute 1 variation) $row[] = $attribute_variation; // Povolit zákaznické recenze? (Enable reviews) $row[] = $product->get_reviews_allowed() ? '1' : '0'; // Minimum Quantity $min_qty = $product->get_meta('_minimum_quantity'); $row[] = $min_qty ? $min_qty : ''; // Maximum Quantity $max_qty = $product->get_meta('_maximum_quantity'); $row[] = $max_qty ? $max_qty : ''; // Format as CSV row $formatted_row = array(); foreach ($row as $value) { // Escape double quotes $value = str_replace('"', '""', $value); $formatted_row[] = '"' . $value . '"'; } return implode(',', $formatted_row) . "\n"; } /** * Get full category path (Parent > Child format for subcategories) * * @param int $category_id Category term ID * @return string Category path */ private function get_category_path($category_id) { $term = get_term($category_id, 'product_cat'); if (!$term || is_wp_error($term)) { return ''; } // Build path from child to root $path = array($term->name); $current_term = $term; while ($current_term->parent != 0) { $parent_term = get_term($current_term->parent, 'product_cat'); if (!$parent_term || is_wp_error($parent_term)) { break; } array_unshift($path, $parent_term->name); $current_term = $parent_term; } // Return full path with ">" separator if subcategory, otherwise just the name return count($path) > 1 ? implode(' > ', $path) : $path[0]; } /** * Generate CSV file and return download URL * * @param array $category_ids Array of category IDs * @return array Result with download URL or error message */ public function generate_export_file($category_ids) { // v1.7.1 — L2: ask export_products() to return both the CSV string // AND the row count it accumulated during generation. substr_count // on "\n" over-counted whenever any product description contained // a newline. $row_count = 0; $csv_content = $this->export_products($category_ids, $row_count); if (empty($csv_content)) { return array( 'success' => false, 'message' => __('No products found in selected categories', 'studiou-wc-product-cat-manage') ); } // v1.6.4 — write into the guarded export dir and prune anything older // than 24 hours. Filename gets a 32-char random token so the file is // unguessable even if directory listing leaks. $export_dir = $this->get_export_dir(); $this->prune_old_exports(); $token = bin2hex(random_bytes(16)); // 32 hex chars $filename = 'product-export-' . $token . '.csv'; $file_path = $export_dir . $filename; // Write CSV content to file with UTF-8 BOM so Excel double-click // opens it with the correct encoding (Policy A, v1.6.2 — match the // category exporter, which has always written the BOM). $result = file_put_contents($file_path, "\xEF\xBB\xBF" . $csv_content); if ($result === false) { return array( 'success' => false, 'message' => __('Failed to create export file', 'studiou-wc-product-cat-manage') ); } // Return an admin URL routed through handle_export_download(), // which checks the nonce + capability + basename. The raw public // uploads URL is no longer exposed. $download_url = add_query_arg( array( 'page' => 'studiou-product-export', 'action' => 'studiou_wcpcm_download_product_export', 'file' => $filename, '_wpnonce' => wp_create_nonce('studiou-wcpcm-product-export-download'), ), admin_url('edit.php?post_type=product') ); return array( 'success' => true, 'message' => sprintf( /* translators: %d: number of exported products */ __('Export successful! %d products exported.', 'studiou-wc-product-cat-manage'), $row_count ), 'download_url' => $download_url ); } }