support custom expiration durations for file and folder shares (closes #26)
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
|
||||
require_once PROJECT_ROOT . '/config/config.php';
|
||||
|
||||
class FolderModel {
|
||||
class FolderModel
|
||||
{
|
||||
/**
|
||||
* Creates a folder under the specified parent (or in root) and creates an empty metadata file.
|
||||
*
|
||||
@@ -12,10 +13,11 @@ class FolderModel {
|
||||
* @return array Returns an array with a "success" key if the folder was created,
|
||||
* or an "error" key if an error occurred.
|
||||
*/
|
||||
public static function createFolder(string $folderName, string $parent = ""): array {
|
||||
public static function createFolder(string $folderName, string $parent = ""): array
|
||||
{
|
||||
$folderName = trim($folderName);
|
||||
$parent = trim($parent);
|
||||
|
||||
|
||||
// Validate folder name (only letters, numbers, underscores, dashes, and spaces allowed).
|
||||
if (!preg_match(REGEX_FOLDER_NAME, $folderName)) {
|
||||
return ["error" => "Invalid folder name."];
|
||||
@@ -23,7 +25,7 @@ class FolderModel {
|
||||
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;
|
||||
@@ -32,12 +34,12 @@ class FolderModel {
|
||||
$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.
|
||||
@@ -50,52 +52,54 @@ class FolderModel {
|
||||
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 {
|
||||
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 {
|
||||
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.
|
||||
@@ -109,43 +113,45 @@ class FolderModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* 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 {
|
||||
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) {
|
||||
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.
|
||||
@@ -171,7 +177,8 @@ class FolderModel {
|
||||
* @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 {
|
||||
private static function getSubfolders(string $dir, string $relative = ''): array
|
||||
{
|
||||
$folders = [];
|
||||
$items = scandir($dir);
|
||||
$safeFolderNamePattern = REGEX_FOLDER_NAME;
|
||||
@@ -198,7 +205,8 @@ class FolderModel {
|
||||
*
|
||||
* @return array An array of folder information arrays.
|
||||
*/
|
||||
public static function getFolderList(): array {
|
||||
public static function getFolderList(): array
|
||||
{
|
||||
$baseDir = rtrim(UPLOAD_DIR, '/\\');
|
||||
$folderInfoList = [];
|
||||
|
||||
@@ -240,13 +248,14 @@ class FolderModel {
|
||||
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 {
|
||||
public static function getShareFolderRecord(string $token): ?array
|
||||
{
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
if (!file_exists($shareFile)) {
|
||||
return null;
|
||||
@@ -257,8 +266,8 @@ class FolderModel {
|
||||
}
|
||||
return $shareLinks[$token];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Retrieves shared folder data based on a share token.
|
||||
*
|
||||
* @param string $token The share folder token.
|
||||
@@ -274,7 +283,8 @@ class FolderModel {
|
||||
* - 'totalPages': total pages,
|
||||
* or an 'error' key on failure.
|
||||
*/
|
||||
public static function getSharedFolderData(string $token, ?string $providedPass, int $page = 1, int $itemsPerPage = 10): array {
|
||||
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)) {
|
||||
@@ -314,7 +324,7 @@ class FolderModel {
|
||||
return ["error" => "Shared folder not found."];
|
||||
}
|
||||
// Scan for files (only files).
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function($item) use ($realFolderPath) {
|
||||
$allFiles = array_values(array_filter(scandir($realFolderPath), function ($item) use ($realFolderPath) {
|
||||
return is_file($realFolderPath . DIRECTORY_SEPARATOR . $item);
|
||||
}));
|
||||
sort($allFiles);
|
||||
@@ -323,7 +333,7 @@ class FolderModel {
|
||||
$currentPage = min($page, $totalPages);
|
||||
$startIndex = ($currentPage - 1) * $itemsPerPage;
|
||||
$filesOnPage = array_slice($allFiles, $startIndex, $itemsPerPage);
|
||||
|
||||
|
||||
return [
|
||||
"record" => $record,
|
||||
"folder" => $folder,
|
||||
@@ -334,81 +344,72 @@ class FolderModel {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a share link for a folder.
|
||||
*
|
||||
* @param string $folder The folder to share (relative to UPLOAD_DIR).
|
||||
* @param int $expirationMinutes The duration (in minutes) until the link expires.
|
||||
* @param string $password Optional password for the share.
|
||||
* @param int $allowUpload Optional flag (0 or 1) indicating whether uploads are allowed.
|
||||
* @return array An associative array with "token", "expires", and "link" on success, or "error" on failure.
|
||||
* @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 $expirationMinutes = 60, string $password = "", int $allowUpload = 0): array {
|
||||
// Validate folder name.
|
||||
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."];
|
||||
}
|
||||
|
||||
// Generate secure token.
|
||||
// Token
|
||||
try {
|
||||
$token = bin2hex(random_bytes(16)); // 32 hex characters.
|
||||
$token = bin2hex(random_bytes(16));
|
||||
} catch (Exception $e) {
|
||||
return ["error" => "Could not generate token."];
|
||||
}
|
||||
|
||||
// Calculate expiration time.
|
||||
$expires = time() + ($expirationMinutes * 60);
|
||||
// Expiry
|
||||
$expires = time() + $expirationSeconds;
|
||||
|
||||
// Hash the password if provided.
|
||||
$hashedPassword = !empty($password) ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
// Password hash
|
||||
$hashedPassword = $password !== "" ? password_hash($password, PASSWORD_DEFAULT) : "";
|
||||
|
||||
// Define the share folder links file.
|
||||
// Load existing
|
||||
$shareFile = META_DIR . "share_folder_links.json";
|
||||
$shareLinks = [];
|
||||
if (file_exists($shareFile)) {
|
||||
$data = file_get_contents($shareFile);
|
||||
$shareLinks = json_decode($data, true);
|
||||
if (!is_array($shareLinks)) {
|
||||
$shareLinks = [];
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up expired share links.
|
||||
$currentTime = time();
|
||||
foreach ($shareLinks as $key => $link) {
|
||||
if (isset($link["expires"]) && $link["expires"] < $currentTime) {
|
||||
unset($shareLinks[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new share record.
|
||||
$shareLinks[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
// Add new
|
||||
$links[$token] = [
|
||||
"folder" => $folder,
|
||||
"expires" => $expires,
|
||||
"password" => $hashedPassword,
|
||||
"allowUpload" => $allowUpload
|
||||
];
|
||||
|
||||
// Save the updated share links.
|
||||
if (file_put_contents($shareFile, json_encode($shareLinks, JSON_PRETTY_PRINT)) === false) {
|
||||
// Save
|
||||
if (file_put_contents($shareFile, json_encode($links, JSON_PRETTY_PRINT)) === false) {
|
||||
return ["error" => "Could not save share link."];
|
||||
}
|
||||
|
||||
// Determine the base URL.
|
||||
if (defined('BASE_URL') && !empty(BASE_URL) && strpos(BASE_URL, 'yourwebsite') === false) {
|
||||
$baseUrl = rtrim(BASE_URL, '/');
|
||||
} else {
|
||||
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http";
|
||||
$host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : gethostbyname($_SERVER['SERVER_ADDR'] ?? 'localhost');
|
||||
$baseUrl = $protocol . "://" . $host;
|
||||
}
|
||||
// The share URL points to the shared folder page.
|
||||
$link = $baseUrl . "/api/folder/shareFolder.php?token=" . urlencode($token);
|
||||
|
||||
// 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.
|
||||
@@ -418,7 +419,8 @@ class FolderModel {
|
||||
* - "realFilePath": the absolute path to the file,
|
||||
* - "mimeType": the detected MIME type.
|
||||
*/
|
||||
public static function getSharedFileInfo(string $token, string $file): array {
|
||||
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)) {
|
||||
@@ -457,14 +459,14 @@ class FolderModel {
|
||||
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,
|
||||
@@ -479,11 +481,12 @@ class FolderModel {
|
||||
* @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 {
|
||||
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'];
|
||||
|
||||
$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)) {
|
||||
@@ -494,55 +497,55 @@ class FolderModel {
|
||||
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;
|
||||
@@ -564,7 +567,7 @@ class FolderModel {
|
||||
"uploader" => $uploader
|
||||
];
|
||||
file_put_contents($metadataFile, json_encode($metadataCollection, JSON_PRETTY_PRINT));
|
||||
|
||||
|
||||
return ["success" => "File uploaded successfully.", "newFilename" => $newFilename];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user