|
|
@@ -28,6 +28,101 @@ class Studiou_WC_Product_Manage_Product_Export {
|
|
|
*/
|
|
|
public function __construct($db) {
|
|
|
$this->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, '<?php // Silence is golden.');
|
|
|
+ }
|
|
|
+ $ht = $dir . '.htaccess';
|
|
|
+ if (!file_exists($ht)) {
|
|
|
+ @file_put_contents($ht, "Deny from all\n");
|
|
|
+ }
|
|
|
+ return $dir;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Delete export files older than 24 hours. Called on every new export.
|
|
|
+ */
|
|
|
+ private function prune_old_exports() {
|
|
|
+ $dir = $this->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;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -252,17 +347,15 @@ class Studiou_WC_Product_Manage_Product_Export {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- // Create uploads directory if it doesn't exist
|
|
|
- $upload_dir = wp_upload_dir();
|
|
|
- $export_dir = $upload_dir['basedir'] . '/studiou-wc-product-exports';
|
|
|
+ // 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();
|
|
|
|
|
|
- if (!file_exists($export_dir)) {
|
|
|
- wp_mkdir_p($export_dir);
|
|
|
- }
|
|
|
-
|
|
|
- // Generate filename with timestamp
|
|
|
- $filename = 'product-export-' . date('Y-m-d-H-i-s') . '.csv';
|
|
|
- $file_path = $export_dir . '/' . $filename;
|
|
|
+ $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
|
|
|
@@ -276,14 +369,24 @@ class Studiou_WC_Product_Manage_Product_Export {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- // Generate download URL
|
|
|
- $download_url = $upload_dir['baseurl'] . '/studiou-wc-product-exports/' . $filename;
|
|
|
+ // 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(
|
|
|
__('Export successful! %d products exported.', 'studiou-wc-product-cat-manage'),
|
|
|
- substr_count($csv_content, "\n") - 1 // Subtract header row
|
|
|
+ substr_count($csv_content, "\n") - 1 // Subtract header row (note: L2 fixes the multi-line case in v1.7.1)
|
|
|
),
|
|
|
'download_url' => $download_url
|
|
|
);
|