"Invalid folder name."]; } if ($parent !== "" && !preg_match(REGEX_FOLDER_NAME, $parent)) { return ["error" => "Invalid parent folder name."]; } $baseDir = rtrim(UPLOAD_DIR, '/\\'); if ($parent !== "" && strtolower($parent) !== "root") { $fullPath = $baseDir . DIRECTORY_SEPARATOR . $parent . DIRECTORY_SEPARATOR . $folderName; $relativePath = $parent . "/" . $folderName; } else { $fullPath = $baseDir . DIRECTORY_SEPARATOR . $folderName; $relativePath = $folderName; } // Check if the folder already exists. if (file_exists($fullPath)) { return ["error" => "Folder already exists."]; } // Attempt to create the folder. if (mkdir($fullPath, 0755, true)) { // Create an empty metadata file for the new folder. $metadataFile = self::getMetadataFilePath($relativePath); if (file_put_contents($metadataFile, json_encode([], JSON_PRETTY_PRINT)) === false) { return ["error" => "Folder created but failed to create metadata file."]; } return ["success" => true]; } else { return ["error" => "Failed to create folder."]; } } /** * Generates the metadata file path for a given folder. * * @param string $folder The relative folder path. * @return string The metadata file path. */ private static function getMetadataFilePath(string $folder): string { if (strtolower($folder) === 'root' || trim($folder) === '') { return META_DIR . "root_metadata.json"; } return META_DIR . str_replace(['/', '\\', ' '], '-', trim($folder)) . '_metadata.json'; } /** * Deletes a folder if it is empty and removes its corresponding metadata. * * @param string $folder The folder name (relative to the upload directory). * @return array An associative array with "success" on success or "error" on failure. */ public static function deleteFolder(string $folder): array { // Prevent deletion of "root". if (strtolower($folder) === 'root') { return ["error" => "Cannot delete root folder."]; } // Validate folder name. if (!preg_match(REGEX_FOLDER_NAME, $folder)) { return ["error" => "Invalid folder name."]; } // Build the full folder path. $baseDir = rtrim(UPLOAD_DIR, '/\\'); $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; // Check if the folder exists and is a directory. if (!file_exists($folderPath) || !is_dir($folderPath)) { return ["error" => "Folder does not exist."]; } // Prevent deletion if the folder is not empty. $items = array_diff(scandir($folderPath), array('.', '..')); if (count($items) > 0) { return ["error" => "Folder is not empty."]; } // Attempt to delete the folder. if (rmdir($folderPath)) { // Remove corresponding metadata file. $metadataFile = self::getMetadataFilePath($folder); if (file_exists($metadataFile)) { unlink($metadataFile); } return ["success" => true]; } else { return ["error" => "Failed to delete folder."]; } } /** * Renames a folder and updates related metadata files. * * @param string $oldFolder The current folder name (relative to UPLOAD_DIR). * @param string $newFolder The new folder name. * @return array Returns an associative array with "success" on success or "error" on failure. */ public static function renameFolder(string $oldFolder, string $newFolder): array { // Sanitize and trim folder names. $oldFolder = trim($oldFolder, "/\\ "); $newFolder = trim($newFolder, "/\\ "); // Validate folder names. if (!preg_match(REGEX_FOLDER_NAME, $oldFolder) || !preg_match(REGEX_FOLDER_NAME, $newFolder)) { return ["error" => "Invalid folder name(s)."]; } // Build the full folder paths. $baseDir = rtrim(UPLOAD_DIR, '/\\'); $oldPath = $baseDir . DIRECTORY_SEPARATOR . $oldFolder; $newPath = $baseDir . DIRECTORY_SEPARATOR . $newFolder; // Validate that the old folder exists and new folder does not. if ((realpath($oldPath) === false) || (realpath(dirname($newPath)) === false) || strpos(realpath($oldPath), realpath($baseDir)) !== 0 || strpos(realpath(dirname($newPath)), realpath($baseDir)) !== 0 ) { return ["error" => "Invalid folder path."]; } if (!file_exists($oldPath) || !is_dir($oldPath)) { return ["error" => "Folder to rename does not exist."]; } if (file_exists($newPath)) { return ["error" => "New folder name already exists."]; } // Attempt to rename the folder. if (rename($oldPath, $newPath)) { // Update metadata: Rename all metadata files that have the old folder prefix. $oldPrefix = str_replace(['/', '\\', ' '], '-', $oldFolder); $newPrefix = str_replace(['/', '\\', ' '], '-', $newFolder); $metadataFiles = glob(META_DIR . $oldPrefix . '*_metadata.json'); foreach ($metadataFiles as $oldMetaFile) { $baseName = basename($oldMetaFile); $newBaseName = preg_replace('/^' . preg_quote($oldPrefix, '/') . '/', $newPrefix, $baseName); $newMetaFile = META_DIR . $newBaseName; rename($oldMetaFile, $newMetaFile); } return ["success" => true]; } else { return ["error" => "Failed to rename folder."]; } } /** * Recursively scans a directory for subfolders. * * @param string $dir The full path to the directory. * @param string $relative The relative path from the base directory. * @return array An array of folder paths (relative to the base). */ private static function getSubfolders(string $dir, string $relative = ''): array { $folders = []; $items = scandir($dir); $safeFolderNamePattern = REGEX_FOLDER_NAME; foreach ($items as $item) { if ($item === '.' || $item === '..') { continue; } if (!preg_match($safeFolderNamePattern, $item)) { continue; } $path = $dir . DIRECTORY_SEPARATOR . $item; if (is_dir($path)) { $folderPath = ($relative ? $relative . '/' : '') . $item; $folders[] = $folderPath; $subFolders = self::getSubfolders($path, $folderPath); $folders = array_merge($folders, $subFolders); } } return $folders; } /** * Retrieves the list of folders (including "root") along with file count metadata. * * @return array An array of folder information arrays. */ public static function getFolderList(): array { $baseDir = rtrim(UPLOAD_DIR, '/\\'); $folderInfoList = []; // Process the "root" folder. $rootMetaFile = self::getMetadataFilePath('root'); $rootFileCount = 0; if (file_exists($rootMetaFile)) { $rootMetadata = json_decode(file_get_contents($rootMetaFile), true); $rootFileCount = is_array($rootMetadata) ? count($rootMetadata) : 0; } $folderInfoList[] = [ "folder" => "root", "fileCount" => $rootFileCount, "metadataFile" => basename($rootMetaFile) ]; // Recursively scan for subfolders. if (is_dir($baseDir)) { $subfolders = self::getSubfolders($baseDir); } else { $subfolders = []; } // For each subfolder, load metadata to get file counts. foreach ($subfolders as $folder) { $metaFile = self::getMetadataFilePath($folder); $fileCount = 0; if (file_exists($metaFile)) { $metadata = json_decode(file_get_contents($metaFile), true); $fileCount = is_array($metadata) ? count($metadata) : 0; } $folderInfoList[] = [ "folder" => $folder, "fileCount" => $fileCount, "metadataFile" => basename($metaFile) ]; } return $folderInfoList; } /** * Retrieves the share folder record for a given token. * * @param string $token The share folder token. * @return array|null The share folder record, or null if not found. */ public static function getShareFolderRecord(string $token): ?array { $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return null; } $shareLinks = json_decode(file_get_contents($shareFile), true); if (!is_array($shareLinks) || !isset($shareLinks[$token])) { return null; } return $shareLinks[$token]; } /** * Retrieves shared folder data based on a share token. * * @param string $token The share folder token. * @param string|null $providedPass The provided password (if any). * @param int $page The page number for pagination. * @param int $itemsPerPage The number of files to display per page. * @return array Associative array with keys: * - 'record': the share record, * - 'folder': the shared folder (relative), * - 'realFolderPath': absolute folder path, * - 'files': array of filenames for the current page, * - 'currentPage': current page number, * - 'totalPages': total pages, * or an 'error' key on failure. */ public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array { // Load the share folder record. $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return ["error" => "Share link not found."]; } $shareLinks = json_decode(file_get_contents($shareFile), true); if (!is_array($shareLinks) || !isset($shareLinks[$token])) { return ["error" => "Share link not found."]; } $record = $shareLinks[$token]; // Check expiration. if (time() > $record['expires']) { return ["error" => "This share link has expired."]; } // If password protection is enabled and no password is provided, signal that. if (!empty($record['password']) && empty($providedPass)) { return ["needs_password" => true]; } if (!empty($record['password']) && !password_verify($providedPass, $record['password'])) { return ["error" => "Invalid password."]; } // Determine the shared folder. $folder = trim($record['folder'], "/\\ "); $baseDir = realpath(UPLOAD_DIR); if ($baseDir === false) { return ["error" => "Uploads directory not configured correctly."]; } if (!empty($folder) && strtolower($folder) !== 'root') { $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; } else { $folder = "root"; $folderPath = $baseDir; } $realFolderPath = realpath($folderPath); $uploadDirReal = realpath(UPLOAD_DIR); if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { return ["error" => "Shared folder not found."]; } // Scan for files (only files). $allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) { return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item); })); sort($allFiles); $totalFiles = count($allFiles); $totalPages = max(1, ceil($totalFiles / $itemsPerPage)); $currentPage = min($page, $totalPages); $startIndex = ($currentPage - 1) * $itemsPerPage; $filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage); return [ "record" => $record, "folder" => $folder, "realFolderPath" => $realFolderPath, "files" => $filesOnPage, "currentPage" => $currentPage, "totalPages" => $totalPages ]; } /** * Creates a share link for a folder. * * @param string $folder The folder to share (relative to UPLOAD_DIR). * @param int $expirationSeconds How many seconds until expiry. * @param string $password Optional password. * @param int $allowUpload 0 or 1 whether uploads are allowed. * @return array ["token","expires","link"] on success, or ["error"]. */ public static function createShareFolderLink(string $folder, int $expirationSeconds = 3600, string $password = "", int $allowUpload = 0): array { // Validate folder if (strtolower($folder) !== 'root' && !preg_match(REGEX_FOLDER_NAME, $folder)) { return ["error" => "Invalid folder name."]; } // Token try { $token = bin2hex(random_bytes(16)); } catch (Exception $e) { return ["error" => "Could not generate token."]; } // Expiry $expires = time() + $expirationSeconds; // Password hash $hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : ""; // Load existing $shareFile = META_DIR . "share_folder_links.json"; $links = file_exists($shareFile) ? json_decode(file_get_contents($shareFile), true) ?? [] : []; // Cleanup $now = time(); foreach ($links as $k => $v) { if (!empty($v['expires']) && $v['expires'] < $now) { unset($links[$k]); } } // Add new $links[$token] = [ "folder" => $folder, "expires" => $expires, "password" => $hashedPassword, "allowUpload" => $allowUpload ]; // Save if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) { return ["error" => "Could not save share link."]; } // Build URL $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http"; $host = $_SERVER['HTTP_HOST'] ?? gethostbyname(gethostname()); $baseUrl = $protocol . '://' . rtrim($host, '/'); $link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token); return ["token" => $token, "expires" => $expires, "link" => $link]; } /** * Retrieves information for a shared file from a shared folder link. * * @param string $token The share folder token. * @param string $file The requested file name. * @return array An associative array with keys: * - "error": error message, if any, * - "realFilePath": the absolute path to the file, * - "mimeType": the detected MIME type. */ public static function getSharedFileInfo(string $token, string $file): array { // Load the share folder record. $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return ["error" => "Share link not found."]; } $shareLinks = json_decode(file_get_contents($shareFile), true); if (!is_array($shareLinks) || !isset($shareLinks[$token])) { return ["error" => "Share link not found."]; } $record = $shareLinks[$token]; // Check if the link has expired. if (time() > $record['expires']) { return ["error" => "This share link has expired."]; } // Determine the shared folder. $folder = trim($record['folder'], "/\\ "); $baseDir = realpath(UPLOAD_DIR); if ($baseDir === false) { return ["error" => "Uploads directory not configured correctly."]; } if (!empty($folder) && strtolower($folder) !== 'root') { $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folder; } else { $folderPath = $baseDir; } $realFolderPath = realpath($folderPath); $uploadDirReal = realpath(UPLOAD_DIR); if ($realFolderPath === false || strpos($realFolderPath, $uploadDirReal) !== 0 || !is_dir($realFolderPath)) { return ["error" => "Shared folder not found."]; } // Sanitize the file name to prevent path traversal. if (strpos($file, "/") !== false || strpos($file, "\\") !== false) { return ["error" => "Invalid file name."]; } $file = basename($file); // Build the full file path. $filePath = $realFolderPath . DIRECTORY_SEPARATOR . $file; $realFilePath = realpath($filePath); if ($realFilePath === false || strpos($realFilePath, $realFolderPath) !== 0 || !is_file($realFilePath)) { return ["error" => "File not found."]; } $mimeType = mime_content_type($realFilePath); return [ "realFilePath" => $realFilePath, "mimeType" => $mimeType ]; } /** * Handles uploading a file to a shared folder. * * @param string $token The share folder token. * @param array $fileUpload The $_FILES['fileToUpload'] array. * @return array An associative array with "success" on success or "error" on failure. */ public static function uploadToSharedFolder(string $token, array $fileUpload): array { // Define maximum file size and allowed extensions. $maxSize = 50 * 1024 * 1024; // 50 MB $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'mp4', 'webm', 'mp3', 'mkv']; // Load the share folder record. $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return ["error" => "Share record not found."]; } $shareLinks = json_decode(file_get_contents($shareFile), true); if (!is_array($shareLinks) || !isset($shareLinks[$token])) { return ["error" => "Invalid share token."]; } $record = $shareLinks[$token]; // Check expiration. if (time() > $record['expires']) { return ["error" => "This share link has expired."]; } // Check whether uploads are allowed. if (empty($record['allowUpload']) || $record['allowUpload'] != 1) { return ["error" => "File uploads are not allowed for this share."]; } // Validate file upload presence. if ($fileUpload['error'] !== UPLOAD_ERR_OK) { return ["error" => "File upload error. Code: " . $fileUpload['error']]; } if ($fileUpload['size'] > $maxSize) { return ["error" => "File size exceeds allowed limit."]; } $uploadedName = basename($fileUpload['name']); $ext = strtolower(pathinfo($uploadedName, PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExtensions)) { return ["error" => "File type not allowed."]; } // Determine the target folder from the share record. $folderName = trim($record['folder'], "/\\"); $targetFolder = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR; if (!empty($folderName) && strtolower($folderName) !== 'root') { $targetFolder .= $folderName; } // Verify target folder exists. $realTargetFolder = realpath($targetFolder); $uploadDirReal = realpath(UPLOAD_DIR); if ($realTargetFolder === false || strpos($realTargetFolder, $uploadDirReal) !== 0 || !is_dir($realTargetFolder)) { return ["error" => "Shared folder not found."]; } // Generate a new filename (using uniqid and sanitizing the original name). $newFilename = uniqid() . "_" . preg_replace('/[^A-Za-z0-9_\-\.]/', '_', $uploadedName); $targetPath = $realTargetFolder . DIRECTORY_SEPARATOR . $newFilename; // Move the uploaded file. if (!move_uploaded_file($fileUpload['tmp_name'], $targetPath)) { return ["error" => "Failed to move the uploaded file."]; } // --- Metadata Update --- // Determine metadata file. $metadataKey = (empty($folderName) || strtolower($folderName) === "root") ? "root" : $folderName; $metadataFileName = str_replace(['/', '\\', ' '], '-', $metadataKey) . '_metadata.json'; $metadataFile = META_DIR . $metadataFileName; $metadataCollection = []; if (file_exists($metadataFile)) { $data = file_get_contents($metadataFile); $metadataCollection = json_decode($data, true); if (!is_array($metadataCollection)) { $metadataCollection = []; } } $uploadedDate = date(DATE_TIME_FORMAT); $uploader = "Outside Share"; // As per your original implementation. // Update metadata with the new file's info. $metadataCollection[$newFilename] = [ "uploaded" => $uploadedDate, "uploader" => $uploader ]; file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT)); return ["success" => "File uploaded successfully.", "newFilename" => $newFilename]; } public static function getAllShareFolderLinks(): array { $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return []; } $links = json_decode(file_get_contents($shareFile), true); return is_array($links) ? $links : []; } public static function deleteShareFolderLink(string $token): bool { $shareFile = META_DIR . "share_folder_links.json"; if (!file_exists($shareFile)) { return false; } $links = json_decode(file_get_contents($shareFile), true); if (!is_array($links) || !isset($links[$token])) { return false; } unset($links[$token]); file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)); return true; } }