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; }