target_dir = rtrim($target_dir, '/') . '/'; $this->ensureDirectory(); } private function ensureDirectory() { if (!is_dir($this->target_dir)) { if (!mkdir($this->target_dir, 0755, true)) { throw new Exception("Failed to create directory: " . $this->target_dir); } } if (!is_writable($this->target_dir)) { throw new Exception("Target directory is not writable: " . $this->target_dir); } } /** * Remove files that user deleted in the UI */ public function cleanup($existing_csv, $keep_csv) { if (empty($existing_csv)) return; $existing = array_filter(array_map('trim', explode(',', $existing_csv))); $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; foreach ($existing as $file) { if (!in_array($file, $keep)) { $path = $this->target_dir . $file; if (file_exists($path)) { unlink($path); $this->deleted_files[] = $file; } } } } /** * Upload new files from $_FILES * Returns array of errors (empty = success) */ public function upload($field_name) { if (!isset($_FILES[$field_name])) return []; $errors = []; // Normalize single file to array structure $files = $_FILES[$field_name]; if (!is_array($files['name'])) { $files['name'] = [$files['name']]; $files['tmp_name'] = [$files['tmp_name']]; $files['error'] = [$files['error']]; $files['size'] = [$files['size']]; } foreach ($files['name'] as $key => $name) { $error_code = $files['error'][$key]; if ($error_code !== UPLOAD_ERR_OK) { $errors[] = "{$name}: " . $this->getUploadError($error_code); continue; } $tmp_name = $files['tmp_name'][$key]; $file_size = $files['size'][$key]; $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if ($file_size > $this->max_size) { $errors[] = "{$name}: exceeds " . ($this->max_size / 1024 / 1024) . "MB limit"; continue; } if (!in_array($extension, $this->allowed_extensions)) { $errors[] = "{$name}: .{$extension} is not allowed"; continue; } $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $tmp_name); finfo_close($finfo); if (!in_array($mime, $this->allowed_mimes)) { $errors[] = "{$name}: content type ({$mime}) is not allowed"; continue; } // FILE NAME SANITIZATION + UNIQUE ID $original = pathinfo($name, PATHINFO_FILENAME); $original = preg_replace('/[^a-zA-Z0-9_-]/', '_', $original); // sanitize $file_id = $original . "_" . uniqid() . "." . $extension; $destination = $this->target_dir . $file_id; if (move_uploaded_file($tmp_name, $destination)) { $this->uploaded_files[] = $file_id; chmod($destination, 0644); } else { $errors[] = "{$name}: failed to save"; } } return $errors; } /** * Build final CSV string for DB */ public function buildFileString($keep_csv) { $keep = !empty($keep_csv) ? array_filter(array_map('trim', explode(',', $keep_csv))) : []; $final = array_merge($keep, $this->uploaded_files); return implode(",", array_filter($final)); } /** * Build final CSV string for DB as single file */ public function getUploadedFiles() { return $this->uploaded_files; } /** * Rollback uploaded files if DB fails */ public function rollbackUploads() { foreach ($this->uploaded_files as $file) { $path = $this->target_dir . $file; if (file_exists($path)) { unlink($path); } } $this->uploaded_files = []; } private function getUploadError($code) { $messages = [ UPLOAD_ERR_INI_SIZE => "exceeds server max upload size (" . ini_get('upload_max_filesize') . ")", UPLOAD_ERR_FORM_SIZE => "exceeds form max size", UPLOAD_ERR_PARTIAL => "was only partially uploaded", UPLOAD_ERR_NO_FILE => "no file was sent", UPLOAD_ERR_NO_TMP_DIR => "server missing temp folder", UPLOAD_ERR_CANT_WRITE => "server failed to write to disk", UPLOAD_ERR_EXTENSION => "blocked by server extension", ]; return $messages[$code] ?? "unknown error (code {$code})"; } } ?>