* Creates a ZIP archive of the specified files from a given folder. * * @param string $folder The folder from which to zip the files (e.g., "root" or a subfolder). * @param array $files An array of file names to include in the ZIP. * @return array An associative array with either an "error" key or a "zipPath" key. */
($folder, $files)
| 1528 | * @return array An associative array with either an "error" key or a "zipPath" key. |
| 1529 | */ |
| 1530 | public static function createZipArchive($folder, $files) |
| 1531 | { |
| 1532 | // Block ZIP creation inside encrypted folders (v1). |
| 1533 | try { |
| 1534 | if (FolderCrypto::isEncryptedOrAncestor((string)$folder)) { |
| 1535 | return ["error" => "ZIP operations are disabled inside encrypted folders."]; |
| 1536 | } |
| 1537 | } catch (\Throwable $e) { |
| 1538 | /* ignore */ |
| 1539 | } |
| 1540 | |
| 1541 | // Purge old temp zips > 6h (best-effort) |
| 1542 | $zipRoot = rtrim(self::metaRoot(), '/\\') . DIRECTORY_SEPARATOR . 'ziptmp'; |
| 1543 | $now = time(); |
| 1544 | foreach ((glob($zipRoot . DIRECTORY_SEPARATOR . 'download-*.zip') ?: []) as $zp) { |
| 1545 | if (is_file($zp) && ($now - (int)@filemtime($zp)) > 21600) { |
| 1546 | @unlink($zp); |
| 1547 | } |
| 1548 | } |
| 1549 | |
| 1550 | // Normalize and validate target folder |
| 1551 | $folder = trim((string)$folder) ?: 'root'; |
| 1552 | $baseDir = realpath(self::uploadRoot()); |
| 1553 | if ($baseDir === false) { |
| 1554 | return ["error" => "Uploads directory not configured correctly."]; |
| 1555 | } |
| 1556 | |
| 1557 | if (strtolower($folder) === 'root' || $folder === "") { |
| 1558 | $folderPathReal = $baseDir; |
| 1559 | } else { |
| 1560 | if (strpos($folder, '..') !== false) { |
| 1561 | return ["error" => "Invalid folder name."]; |
| 1562 | } |
| 1563 | $parts = explode('/', trim($folder, "/\\ ")); |
| 1564 | foreach ($parts as $part) { |
| 1565 | if ($part === '' || !preg_match(REGEX_FOLDER_NAME, $part)) { |
| 1566 | return ["error" => "Invalid folder name."]; |
| 1567 | } |
| 1568 | } |
| 1569 | $folderPath = rtrim(self::uploadRoot(), '/\\') . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts); |
| 1570 | $folderPathReal = realpath($folderPath); |
| 1571 | if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { |
| 1572 | return ["error" => "Folder not found."]; |
| 1573 | } |
| 1574 | } |
| 1575 | |
| 1576 | // Collect files to zip (only regular files in the chosen folder) |
| 1577 | $filesToZip = []; |
| 1578 | foreach ($files as $fileName) { |
| 1579 | $fileName = basename(trim((string)$fileName)); |
| 1580 | if (!preg_match(REGEX_FILE_NAME, $fileName)) { |
| 1581 | continue; |
| 1582 | } |
| 1583 | $fullPath = $folderPathReal . DIRECTORY_SEPARATOR . $fileName; |
| 1584 | // Skip symlinks (avoid archiving outside targets via links) |
| 1585 | if (is_link($fullPath)) { |
| 1586 | continue; |
| 1587 | } |
nothing calls this directly
no test coverage detected