350 lines
12 KiB
PHP
350 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Sanitizes user input to be used as a part of a file path to prevent security vulnerabilities such as directory traversal.
|
|
*
|
|
* @param string $userInput The input string to be sanitized.
|
|
* @return string Returns a safe string where only alphanumeric characters, underscores, and hyphens are retained.
|
|
* Special characters and potential path traversal payloads are removed or replaced.
|
|
*/
|
|
function makePathSafe(string $userInput): string
|
|
{
|
|
// Keep only alphanumeric characters, underscores, and hyphens
|
|
$safeString = preg_replace('/[^\w\-]/', '', $userInput);
|
|
|
|
// Ensure no path traversal
|
|
$safeString = str_replace('..', '_', $safeString);
|
|
|
|
// Trim leading/trailing underscores
|
|
$safeString = trim($safeString, '_');
|
|
|
|
// Replace directory separator characters with underscores
|
|
$safeString = str_replace(['/', '\\'], '_', $safeString);
|
|
|
|
// Limit length for safety
|
|
return substr($safeString, 0, 255);
|
|
}
|
|
/**
|
|
* Automatically rotates an image based on its EXIF data to adjust its orientation.
|
|
*
|
|
* @param Imagick $imagick An Imagick object representing the image to be rotated.
|
|
* @return void
|
|
*/
|
|
function autoRotateImage(Imagick $imagick): void {
|
|
// Get the current orientation of the image
|
|
try {
|
|
$orientation = $imagick->getImageOrientation();
|
|
switch ($orientation) {
|
|
case Imagick::ORIENTATION_BOTTOMRIGHT: // upside down
|
|
$imagick->rotateimage("#000", 180); // rotate 180 degrees
|
|
break;
|
|
|
|
case Imagick::ORIENTATION_RIGHTTOP: // 90 degrees CW
|
|
$imagick->rotateimage("#000", 90); // rotate 90 degrees CW
|
|
break;
|
|
|
|
case Imagick::ORIENTATION_LEFTBOTTOM: // 90 degrees CCW
|
|
$imagick->rotateimage("#000", -90); // rotate 90 degrees CCW
|
|
break;
|
|
}
|
|
|
|
// Reset orientation to normal after the correction
|
|
$imagick->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
|
} catch (ImagickException) {
|
|
}
|
|
|
|
}
|
|
/**
|
|
* Processes the global $_FILES array to normalize the structure and filter out any files with errors.
|
|
*
|
|
* @return array Returns an array of files that are ready for further processing, structured uniformly.
|
|
*/
|
|
function getIncomingFiles(): array
|
|
{
|
|
$files = $_FILES;
|
|
$files2 = [];
|
|
foreach ($files as $infoArr) {
|
|
$filesByInput = [];
|
|
foreach ($infoArr as $key => $valueArr) {
|
|
if (is_array($valueArr)) { // file input "multiple"
|
|
foreach ($valueArr as $i => $value) {
|
|
$filesByInput[$i][$key] = $value;
|
|
}
|
|
} else { // -> string, normal file input
|
|
$filesByInput[] = $infoArr;
|
|
break;
|
|
}
|
|
}
|
|
$files2 = array_merge($files2, $filesByInput);
|
|
}
|
|
$files3 = [];
|
|
foreach ($files2 as $file) { // let's filter empty & errors
|
|
if (!$file['error']) $files3[] = $file;
|
|
}
|
|
return $files3;
|
|
}
|
|
/**
|
|
* Saves file metadata in the database.
|
|
* This creates the only record of the file existing.
|
|
*
|
|
* @param string $filePath The path where the file is stored.
|
|
* @param string $fileType The MIME type of the file.
|
|
* @param int $width The width of the image file.
|
|
* @param int $height The height of the image file.
|
|
* @return bool Returns true if the file metadata was successfully saved to the database, false otherwise.
|
|
*/
|
|
function saveUploadedFileInDatabase(string $filePath, string $fileType, int $width, int $height): bool
|
|
{
|
|
global $mysqli;
|
|
$stmt = $mysqli->prepare("INSERT INTO Files (Path, Type, UploadedBy, UploadedAt, Width, Height) VALUES (?, ?, ?, NOW(), ?, ?)");
|
|
$stmt->bind_param("ssiii", $filePath, $fileType, $_SESSION["ID"], $width, $height);
|
|
$stmt->execute();
|
|
$stat = $stmt->affected_rows > 0;
|
|
$stmt->close();
|
|
return $stat;
|
|
}
|
|
/**
|
|
* Handles the uploading process of an image, including its conversion to webp format, rotation based on orientation, and saving.
|
|
*
|
|
* @param string $inFile The temporary file path of the uploaded file.
|
|
* @param string $outFile The target file path where the processed image should be saved.
|
|
* @return bool Returns true if the file was successfully processed and saved, false otherwise.
|
|
*/
|
|
function doImageUpload(string $inFile, string $outFile): bool
|
|
{
|
|
// Create Imagick object
|
|
$width = 0;
|
|
$height = 0;
|
|
try {
|
|
$imagick = new Imagick($inFile);
|
|
$imagick->setImageFormat('webp');
|
|
autoRotateImage($imagick);
|
|
$imagick->stripImage();
|
|
$imagick->writeImage($outFile);
|
|
$width = $imagick->getImageWidth();
|
|
$height = $imagick->getImageHeight();
|
|
$imagick->destroy();
|
|
} catch (ImagickException) {
|
|
}
|
|
|
|
// Check if the reencoding was successful, if yes, save into the database.
|
|
if (file_exists($outFile)) {
|
|
return saveUploadedFileInDatabase($outFile, 'image/webp', $width, $height);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Retrieves a list of files from the database, optionally filtered to include only files uploaded by the current user.
|
|
* Access to an unfiltered list (files of all users) is only available to moderators.
|
|
*
|
|
* @param bool $onlyMine Whether to retrieve only files uploaded by the logged-in user. If false, files from all users are returned if the user is a moderator.
|
|
* @return array Returns an array containing file data along with a status message.
|
|
*/
|
|
|
|
function listFiles(bool $onlyMine = true): array
|
|
{
|
|
$output = ["Status" => "Success", "Files" => []];
|
|
require_once "lib/account.php";
|
|
if (isLoggedIn()) {
|
|
global $mysqli;
|
|
if (!$onlyMine && !isModerator()) {
|
|
$onlyMine = true;
|
|
}
|
|
$query = "SELECT Files.ID, Files.Path, Files.Type, Files.UploadedAt, Files.UploadedBy, Users.Nickname FROM Files INNER JOIN Users ON Files.UploadedBy = Users.ID ORDER BY UploadedAt DESC";
|
|
|
|
if ($onlyMine) {
|
|
$query .= " WHERE UploadedBy = ?";
|
|
}
|
|
|
|
$stmt = $mysqli->prepare($query);
|
|
if ($onlyMine) {
|
|
$stmt->bind_param("i", $_SESSION["ID"]);
|
|
}
|
|
|
|
$id = 0;
|
|
$path = "";
|
|
$type = "";
|
|
$uploadedAt = "";
|
|
$uploadedByID = 0;
|
|
$uploadedBy = "";
|
|
|
|
$stmt->bind_result($id, $path, $type, $uploadedAt, $uploadedByID, $uploadedBy);
|
|
|
|
$stmt->execute();
|
|
$files = array();
|
|
// Fetch the results into the bound variables
|
|
while ($stmt->fetch()) {
|
|
$files[] = [
|
|
'ID' => $id,
|
|
'Path' => $path,
|
|
'Type' => $type,
|
|
'UploadedAt' => $uploadedAt,
|
|
'UploadedByID' => $uploadedByID,
|
|
'UploadedBy' => $uploadedBy,
|
|
];
|
|
}
|
|
|
|
// Check if any results were fetched
|
|
$output["Files"] = $files;
|
|
|
|
$stmt->close();
|
|
}
|
|
|
|
return $output;
|
|
|
|
}
|
|
/**
|
|
* Processes incoming files from the $_FILES global (after processed by getIncomingFiles), performs checks, and attempts to upload a file based on its type.
|
|
* Currently only supports valid image files.
|
|
*
|
|
* @return array Returns an array indicating the success or failure ('Status' key) of the file processing operations.
|
|
*/
|
|
|
|
function parseIncomingFiles(): array
|
|
{
|
|
$incomingFiles = getIncomingFiles();
|
|
$success = true;
|
|
foreach ($incomingFiles as $incomingFile) {
|
|
if ($incomingFile["error"] == 0 && is_file($incomingFile["tmp_name"])) {
|
|
$type = explode("/", $incomingFile["type"]);
|
|
if ($type[0] == "image") {
|
|
$imgFname = pathinfo($incomingFile["name"], PATHINFO_FILENAME);
|
|
$uploadPath = getUploadPath("image", $imgFname);
|
|
if (!empty($uploadPath)) {
|
|
if (!doImageUpload($incomingFile["tmp_name"], $uploadPath)) {
|
|
$success = false;
|
|
}
|
|
} else {
|
|
$success = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$output = ["Status" => "Fail"];
|
|
if ($success) {
|
|
$output["Status"] = "Success";
|
|
}
|
|
return $output;
|
|
}
|
|
/**
|
|
* Generates a file path for uploading based on the type of the file, the ID of the uploader and the date and time of uploading.
|
|
*
|
|
* @param string $type The type of the file, typically used to categorize the file.
|
|
* @param string $filename The base name of the file, used in generating the final path.
|
|
* @return string Returns the full path for storing the file or an empty string if the type is not recognized.
|
|
*/
|
|
|
|
function getUploadPath(string $type = "unknown", string $filename = "hehe"): string
|
|
{
|
|
$type = makePathSafe($type);
|
|
$id = makePathSafe($_SESSION["ID"]);
|
|
$date = makePathSafe(date("YmdHis"));
|
|
$filename = makePathSafe($filename);
|
|
$extension = match ($type) {
|
|
'image' => 'webp',
|
|
default => 'dummy',
|
|
};
|
|
if ($extension != "dummy") {
|
|
$basepath = "uploads/$type/$id/$date";
|
|
mkdir($basepath, 755, true);
|
|
return $basepath . "/$filename.$extension";
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
/**
|
|
* Checks if a file with a given ID exists in the database and does permission checks.
|
|
* Access is granted to only the user's files, in order to access all files the onlyMine parameter
|
|
* must be false and the user must be a moderator.
|
|
*
|
|
* @param int $fileId The ID of the file to check.
|
|
* @param bool $onlyMine Whether to limit the search to files uploaded by the logged-in user.
|
|
* @return bool|string Returns the path of the file if it exists and meets the criteria, false otherwise.
|
|
*/
|
|
|
|
function fileExists(int $fileId, bool $onlyMine = true): bool|string
|
|
{
|
|
if (!$fileId) {
|
|
return false;
|
|
}
|
|
global $mysqli;
|
|
if (!$onlyMine && !isModerator()) {
|
|
$onlyMine = true;
|
|
}
|
|
$query = 'SELECT ID, Path FROM Files WHERE ID = ?' . ($onlyMine ? ' AND UploadedBy = ?' : '');
|
|
$stmtfileexists = $mysqli->prepare($query);
|
|
if ($onlyMine) {
|
|
$stmtfileexists->bind_param('ii', $fileId, $_SESSION['ID']);
|
|
} else {
|
|
$stmtfileexists->bind_param('i', $fileId);
|
|
}
|
|
$filePath = "";
|
|
$id = null;
|
|
$stmtfileexists->bind_result($id, $filePath);
|
|
$stmtfileexists->execute();
|
|
$stmtfileexists->fetch();
|
|
if ($id != null) {
|
|
return $filePath;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Adds a file to a specified group, if the user created the group or creates a new group if
|
|
* a group with a specified ID does not exist yet
|
|
*
|
|
* @param int $groupId The ID of the group to which the file should be added.
|
|
* @param int $fileId The ID of the file to add to the group.
|
|
* @return array Returns an associative array with a 'Status' key indicating success or failure.
|
|
*/
|
|
function addToGroup(int $groupId, int $fileId): array
|
|
{
|
|
$output = ["Status" => "Fail"];
|
|
if (!$groupId || !$fileId) {
|
|
return $output;
|
|
}
|
|
global $mysqli;
|
|
$stmtcheck = $mysqli->prepare('SELECT ID FROM FileGroups WHERE CreatorID != ? AND ID = ?');
|
|
$stmtcheck->bind_param('ii', $_SESSION['ID'], $groupId);
|
|
$stmtcheck->execute();
|
|
if ($stmtcheck->affected_rows == 0) {
|
|
if (fileExists($fileId, false)) {
|
|
$stmtadd = $mysqli->prepare('INSERT INTO FileGroups (FileID, CreatorID, ID) VALUES (?, ?, ?)');
|
|
$stmtadd->bind_param('iii', $fileId, $_SESSION['ID'], $groupId);
|
|
$stmtadd->execute();
|
|
if ($stmtadd->affected_rows > 0) {
|
|
$output["Status"] = "Success";
|
|
}
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
/**
|
|
* Deletes a file entry from the database and the file system, a user can only delete his own files,
|
|
* except when he is a moderator, in that case he can delete all files.
|
|
*
|
|
* @param int $fileID The ID of the file to be deleted.
|
|
* @return array Returns an array with a 'Status' key indicating the success or failure of the deletion operation.
|
|
*/
|
|
function deleteFile(int $fileID): array
|
|
{
|
|
global $mysqli;
|
|
$out = ["Status" => "Fail"];
|
|
if (isLoggedIn()) {
|
|
$file_location = fileExists($fileID, !isModerator());
|
|
$query = !isModerator() ? 'DELETE FROM Files WHERE ID = ? AND UploadedBy = ?' : 'DELETE FROM Files WHERE ID = ?';
|
|
$stmtDelete = $mysqli->prepare($query);
|
|
if (!isModerator()) {
|
|
$stmtDelete->bind_param('ii', $fileID, $_SESSION['ID']);
|
|
} else {
|
|
$stmtDelete->bind_param('i', $fileID);
|
|
}
|
|
$stmtDelete->execute();
|
|
if ($file_location) {
|
|
if (unlink($file_location) && $stmtDelete->affected_rows > 0) {
|
|
$out['Status'] = 'Success';
|
|
}
|
|
}
|
|
}
|
|
return $out;
|
|
} |