From 8747812a563d70965cc7dcd65fa42b75335d4219 Mon Sep 17 00:00:00 2001 From: Roman Hergenreder Date: Thu, 7 Jan 2021 15:54:19 +0100 Subject: [PATCH] FileApi start --- core/Api/ApiKeyAPI.class.php | 4 +- core/Api/ContactAPI.class.php | 12 + core/Api/FileAPI.class.php | 737 ++++++++++++++++++++ core/Api/PatchSQL.class.php | 65 ++ core/Api/Request.class.php | 1 + core/Api/SettingsAPI.class.php | 2 +- core/Configuration/CreateDatabase.class.php | 25 +- core/Configuration/DatabaseScript.class.php | 9 + core/Configuration/Patch/file_api.class.php | 72 ++ core/Documents/Files.class.php | 61 ++ core/Elements/EmptyBody.class.php | 7 + core/Elements/EmptyHead.class.php | 26 + core/Elements/View.class.php | 92 ++- core/core.php | 9 +- files/uploaded/.htaccess | 1 + js/account.js | 6 +- 16 files changed, 1111 insertions(+), 18 deletions(-) create mode 100644 core/Api/FileAPI.class.php create mode 100644 core/Api/PatchSQL.class.php create mode 100644 core/Configuration/DatabaseScript.class.php create mode 100644 core/Configuration/Patch/file_api.class.php create mode 100644 core/Documents/Files.class.php create mode 100644 core/Elements/EmptyBody.class.php create mode 100644 core/Elements/EmptyHead.class.php create mode 100644 files/uploaded/.htaccess diff --git a/core/Api/ApiKeyAPI.class.php b/core/Api/ApiKeyAPI.class.php index a75d5b8..b0ce080 100644 --- a/core/Api/ApiKeyAPI.class.php +++ b/core/Api/ApiKeyAPI.class.php @@ -162,8 +162,8 @@ namespace Api\ApiKey { $this->loginRequired = true; } - public function execute($aValues = array()) { - if(!parent::execute($aValues)) { + public function execute($values = array()) { + if(!parent::execute($values)) { return false; } diff --git a/core/Api/ContactAPI.class.php b/core/Api/ContactAPI.class.php index e0b04a0..cb4a4d7 100644 --- a/core/Api/ContactAPI.class.php +++ b/core/Api/ContactAPI.class.php @@ -53,6 +53,7 @@ namespace Api\Contact { } $this->createNotification(); + $this->sendMail(); if (!$this->success) { return $this->createError("The contact request was saved, but the server was unable to create a notification."); @@ -110,6 +111,17 @@ namespace Api\Contact { return $this->success; } + + private function sendMail() { + /*$email = $this->getParam("fromEmail"); + $settings = $this->user->getConfiguration()->getSettings(); + $request = new \Api\Mail\Send($this->user); + $this->success = $request->execute(array( + "to" => $settings->get, + "subject" => "[$siteName] Account Invitation", + "body" => $messageBody + ));*/ + } } } \ No newline at end of file diff --git a/core/Api/FileAPI.class.php b/core/Api/FileAPI.class.php new file mode 100644 index 0000000..90c2438 --- /dev/null +++ b/core/Api/FileAPI.class.php @@ -0,0 +1,737 @@ +user->getSQL(); + $res = $sql->select("directory") + ->from("UserFile") + ->where(new Compare("uid", $parentId)) + ->limit(1) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if (empty($res)) { + return $this->createError("Parent directory not found"); + } else if(!$res[0]["directory"]) { + return $this->createError("Parent file is not a directory"); + } + } + + return $this->success; + } + + protected function downloadFile($name, $path) { + if (!file_exists($path)) { + http_response_code(404); + die("404 - File does not exist anymore"); + } else if(!is_file($path) || !is_readable($path)) { + die("403 - Unable to download file."); + } else { + $mimeType = @mime_content_type($path); + if ($mimeType) { + header("Content-Type: $mimeType"); + } + + $name = trim(preg_replace('/\s\s+/', ' ', $name)); + header("Content-Disposition: attachment; filename=$name"); + + //No cache + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + + //Define file size + header('Content-Length: ' . filesize($path)); + + ob_clean(); + flush(); + readfile($path); + exit; + } + } + + protected function isInDirectory($fileId, $directoryId) { + + $sql = $this->user->getSQL(); + + + + } + } +} + +namespace Api\File { + + use Api\FileAPI; + use Api\Parameter\Parameter; + use Api\Parameter\StringType; + use Driver\SQL\Condition\Compare; + use Driver\SQL\Condition\CondIn; + use Objects\User; + + class ValidateToken extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + 'token' => new StringType('token', 36, false) + )); + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $sql = $this->user->getSQL(); + $token = $this->getParam("token"); + $res = $sql->select("UserFile.uid", "valid_until", "token_type", "maxFiles", "maxSize", "extensions", "name", "path", "directory") + ->from("UserFileToken") + ->leftJoin("UserFileTokenFile", "UserFileToken.uid", "token_id") + ->innerJoin("UserFile", "UserFile.uid", "file_id") + ->where(new Compare("token", $token)) + ->where(new Compare("valid_until", $sql->now(), ">")) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if (empty($res)) { + return $this->createError("Invalid Token"); + } else { + $row = $res[0]; + $this->result["token"] = array( + "type" => $row["token_type"], + "valid_until" => $row["valid_until"] + ); + + $this->result["files"] = array(); + foreach ($res as $row) { + $file = array( + "isDirectory" => $row["directory"], + "name" => $row["name"], + "uid" => $row["uid"] + ); + + if ($file["isDirectory"]) { + $file["items"] = array(); + } else { + $file["size"] = @filesize($row["path"]); + $file["mimeType"] = @mime_content_type($row["path"]); + } + + $this->result["files"][] = $file; + } + + if ($row["token_type"] === "upload") { + $this->result["restrictions"] = array( + "maxFiles" => $row["maxFiles"] ?? 0, + "maxSize" => $row["maxSize"] ?? 0, + "extensions" => $row["extensions"] ?? "" + ); + } + } + } + + return $this->success; + } + } + + class RevokeToken extends FileAPI { + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "token" => new StringType("token", 36) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = true; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $sql = $this->user->getSQL(); + $token = $this->getParam("token"); + $res = $sql->select($sql->count()) + ->from("UserToken") + ->where(new Compare("user_id", $this->user->getId())) + ->where(new Compare("token", $token)) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if(empty($res)) { + return $this->getParam("Invalid token"); + } else { + $res = $sql->update("UserToken") + ->set("valid_until", new \DateTime()) + ->where(new Compare("user_id", $this->user->getId())) + ->where(new Compare("token", $token)) + ->execute(); + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + } + } + + return $this->success; + } + } + + class ListFiles extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "directory" => new Parameter("directory", Parameter::TYPE_INT, true, null) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + private function &findDirectory(&$files, $id) { + + if ($id !== null) { + $id = (string)$id; + if (isset($files[$id])) { + return $files[$id]["items"]; + } else { + foreach ($files as &$dir) { + if ($dir["isDirectory"]) { + $target =& $this->findDirectory($dir["items"], $id); + if ($target !== $dir) { + return $target; + } + } + } + return $files; + } + } else { + return $files; + } + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $sql = $this->user->getSQL(); + $res = $sql->select( + "UserFile.uid", "UserFile.directory", "UserFile.path", "UserFile.name", + "UserFile.user_id", "parentTable.uid as parentId", "parentTable.name as parentName") + ->from("UserFile") + ->leftJoin("UserFile", "UserFile.parent_id", "parentTable.uid", "parentTable") + ->where(new Compare("UserFile.user_id", $this->user->getId())) + ->orderBy("UserFile.parent_id") + ->ascending() + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $files = array(); + + foreach ($res as $row) { + $fileId = (string)$row["uid"]; + $parentId = $row["parentId"]; + $fileName = $row["name"]; + $isDirectory = $row["directory"]; + $fileElement = array("uid" => $fileId, "name" => $fileName, "isDirectory" => $isDirectory); + if ($isDirectory) { + $fileElement["items"] = array(); + } else { + $fileElement["size"] = @filesize($row["path"]); + $fileElement["mimeType"] = @mime_content_type($row["path"]); + } + + $dir =& $this->findDirectory($files, $parentId); + $dir[$fileId] = $fileElement; + unset($dir); + } + + $directoryId = $this->getParam("directory"); + if (!is_null($directoryId)) { + $wantedDir =& $this->findDirectory($files, $directoryId); + if ($files === $wantedDir) { + $files = array(); + } else { + $files = $wantedDir; + } + } + + $this->result["files"] = $files; + } + + return $this->success; + } + } + + class ListTokens extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array()); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $sql = $this->user->getSQL(); + $res = $sql->select("uid","token","valid_until","token_type", "maxFiles", "maxSize", "extensions") + ->from("UserFileToken") + ->orderBy("valid_until") + ->descending() + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->result["tokens"] = array(); + foreach ($res as $row) { + $tokenType = $row["token_type"]; + if ($tokenType !== "upload") { + unset($row["maxFiles"]); + unset($row["maxSize"]); + unset($row["extensions"]); + } + $this->result["tokens"][] = $row; + } + } + + return $this->success; + } + } + + class CreateDirectory extends FileAPI { + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + 'name' => new StringType('name', 32), + 'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $sql = $this->user->getSQL(); + $name = $this->getParam('name'); + $parentId = $this->getParam("parentId"); + if (!$this->checkDirectory($parentId)) { + return $this->success; + } + + $res = $sql->insert("UserFile", array("directory", "name", "user_id", "parent_id")) + ->addRow(true, $name, $this->user->getId(), $parentId) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + return $this->success; + } + } + + class Rename extends FileAPI { } + + class Move extends FileAPI { } + + class Upload extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + 'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null), + 'token' => new StringType('token', 36, true, null) + )); + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $token = $this->getParam("token"); + if (!$this->user->isLoggedIn() && is_null($token)) { + return $this->createError("Permission denied (expected token)"); + } + + $sql = $this->user->getSQL(); + $parentId = $this->getParam("parentId"); + if (!is_null($token) && !is_null($parentId)) { + return $this->createError("Cannot upload to parent directory using token"); + } + + if (!is_null($parentId) && !$this->checkDirectory($parentId)) { + return $this->success; + } + + $fileKeys = array_keys($_FILES); + $numFilesUploaded = count($fileKeys); + + if (!is_null($token)) { + + $res = $sql->select("uid", "token_type", "maxFiles", "maxSize", "extensions", "user_id") + ->from("UserFileToken") + ->where(new Compare("token", $token)) + ->where(new Compare("valid_until", $sql->now(), ">=")) + ->limit(1) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + if (!$this->success) { + return false; + } + + if (empty($res) || $res[0]["token_type"] !== "upload") { + return $this->createError("Permission denied (token)"); + } + + $tokenId = $res[0]["uid"]; + $maxFiles = $res[0]["maxFiles"] ?? 0; + $maxSize = $res[0]["maxSize"] ?? 0; + $userId = $res[0]["user_id"]; + $extensions = explode(",", strtolower($res[0]["extensions"] ?? "")); + + $res = $sql->select($sql->count()) + ->from("UserFileTokenFile") + ->innerJoin("UserFileToken", "token_id", "UserFileToken.uid") + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + if (!$this->success) { + return false; + } + + $count = $res[0]["count"]; + if ($maxFiles > 0 && $numFilesUploaded > 0 && $numFilesUploaded + $count > $maxFiles) { + return $this->createError("File limit exceeded. Currently uploaded $count / $maxFiles count"); + } + + if ($maxSize > 0 || !empty($extensions)) { + foreach ($_FILES as $file) { + $name = $file["name"]; + if ($maxSize > 0 && $file["size"] > $maxSize) { + return $this->createError("File Size limit of $maxSize bytes exceeded for file $name"); + } + + $dotPos = strrpos($name, "."); + $ext = ($dotPos !== false ? strtolower(substr($name, $dotPos + 1)) : false); + if (!empty($extensions) && $ext !== false && !in_array($ext, $extensions)) { + return $this->createError("File '$name' has prohibited extension. Allowed extensions: " . implode(",", $extensions)); + } + } + } + } else { + $userId = $this->user->getId(); + } + + if ($numFilesUploaded === 0) { + return $this->createError("No file uploaded"); + } + + $uploadDir = realpath($_SERVER["DOCUMENT_ROOT"] . "/files/uploaded/"); + if (!is_writable($uploadDir)) { + return $this->createError("Upload directory is not writable"); + } + + $fileIds = array(); + foreach ($_FILES as $key => $file) { + $fileName = $file["name"]; + $tmpPath = $file["tmp_name"]; + $md5Hash = hash_file('md5', $tmpPath); + $sha1Hash = hash_file('sha1', $tmpPath); + $filePath = $uploadDir . "/" . $md5Hash . $sha1Hash; + if (move_uploaded_file($tmpPath, $filePath)) { + $res = $sql->insert("UserFile", array("name", "directory", "path", "user_id", "parent_id")) + ->addRow($fileName, false, $filePath, $userId, $parentId) + ->returning("uid") + ->execute(); + + if ($res === false) { + $this->lastError = $sql->getLastError(); + $this->success = false; + return false; + } else { + $fileIds[] = $sql->getLastInsertId(); + } + } else { + return $this->createError("Could not create file: " . $fileName); + } + } + + if (!is_null($token)) { + $query = $sql->insert("UserFileTokenFile", array("file_id", "token_id")); + foreach ($fileIds as $fileId) { + $query->addRow($fileId, $tokenId); + } + + $res = $query->execute(); + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + } + + return $this->success; + } + } + + class Download extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "id" => new Parameter("id", Parameter::TYPE_INT), + "token" => new StringType("token", 36, true, null) + )); + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $token = $this->getParam("token"); + if (!$this->user->isLoggedIn() && is_null($token)) { + return $this->createError("Permission denied (expected token)"); + } + + $sql = $this->user->getSQL(); + $fileId = $this->getParam("id"); + $query = $sql->select("path", "name") + ->from("UserFile") + ->where(new Compare("UserFile.uid", $fileId)); + + if (is_null($token)) { + $query->where(new Compare("user_id", $this->user->getId())); + } else { + $query->innerJoin("UserFileTokenFile", "UserFile.uid", "file_id") + ->innerJoin("UserFileToken", "UserFileToken.uid", "token_id") + ->where(new Compare("token", $token)); + } + + $res = $query->execute(); + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + if (empty($res)) { + if (is_null($token)) { + return $this->createError("File not found"); + } else { + return $this->createError("Permission denied (token)"); + } + } else { + if ($res[0]["directory"]) { + return $this->createError("Cannot download directory (yet)"); + } else { + $path = $res[0]["path"]; + $name = $res[0]["name"]; + $this->downloadFile($name, $path); + } + } + } + + return $this->success; + } + } + + class Delete extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "id" => new Parameter("id", Parameter::TYPE_INT), + "token" => new StringType("token", 36, true, null) + )); + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + + return $this->success; + } + } + + class CreateUploadToken extends FileAPI { + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "maxFiles" => new Parameter("maxFiles", Parameter::TYPE_INT, true, 1), + "maxSize" => new Parameter("maxSize", Parameter::TYPE_INT, true, null), + "extensions" => new StringType("extensions", 64, true, null), + "durability" => new Parameter("durability", Parameter::TYPE_INT, true, 60*24*2) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $maxFiles = $this->getParam("maxFiles"); + $maxSize = $this->getParam("maxSize"); + $extensions = $this->getParam("extensions"); + $durability = $this->getParam("durability"); + + if (!is_null($maxFiles) && $maxFiles < 0) { + return $this->createError("Invalid number of maximum files."); + } + + if (!is_null($maxSize) && $maxSize < 0) { + return $this->createError("Invalid maximum size for uploaded files."); + } + + if (!is_null($durability) && $durability < 0) { + return $this->createError("Invalid durability."); + } + + if (!is_null($extensions)) { + $extensions = explode(",",$extensions); + foreach ($extensions as $i => $ext) { + if (strlen($ext) === 0 || (strlen($ext) === 1 && $ext[0] === ".")) { + unset ($extensions[$i]); + } else if ($ext[0] === ".") { + $extensions[$i] = substr($ext, 1); + } + } + $extensions = implode(",", $extensions); + } + + $sql = $this->user->getSQL(); + $token = generateRandomString(36); + $validUntil = (new \DateTime())->modify("+$durability HOURS"); + + $res = $sql->insert("UserFileToken", + array("token", "token_type", "maxSize", "maxFiles", "extensions", "valid_until", "user_id")) + ->addRow($token, "upload", $maxSize, $maxFiles, $extensions, $validUntil, $this->user->getId()) + ->returning("uid") + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->result["token"] = $token; + $this->result["tokenId"] = $sql->getLastInsertId(); + } + + return $this->success; + } + } + + class CreateDownloadToken extends FileAPI { + + public function __construct(User $user, bool $externalCall = false) { + parent::__construct($user, $externalCall, array( + "durability" => new Parameter("durability", Parameter::TYPE_INT, true, 60*24*2), + "files" => new Parameter("files", Parameter::TYPE_ARRAY, false) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $durability = $this->getParam("durability"); + $fileIds = $this->getParam("files"); + + if (!is_null($durability) && $durability < 0) { + return $this->createError("Invalid durability."); + } + + foreach ($fileIds as $fileId) { + if (!is_int($fileId) && is_numeric($fileId)) { + $fileId = intval($fileId); + } + if (!is_int($fileId) || $fileId < 1) { + return $this->createError("Invalid file id: $fileId"); + } + } + + $fileIds = array_unique($fileIds); + + // Check for files: + $sql = $this->user->getSQL(); + $res = $sql->select("uid") + ->from("UserFile") + ->where(new CondIn("uid", $fileIds)) + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + if (!$this->success) { + return false; + } + + if (count($res) !== count($fileIds)) { + $foundFiles = array_map(function ($row) { return $row["uid"]; }, $res); + foreach ($fileIds as $fileId) { + if (!in_array($fileId, $foundFiles)) { + return $this->createError("File not found: $fileId"); + } + } + } + + // Insert + $token = generateRandomString(36); + $validUntil = (new \DateTime())->modify("+$durability HOURS"); + $res = $sql->insert("UserFileToken", array("token_type", "valid_until", "user_id", "token")) + ->addRow("download", $validUntil, $this->user->getId(), $token) + ->returning("uid") + ->execute(); + + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + if (!$this->success) { + return false; + } + + $tokenId = $sql->getLastInsertId(); + $query = $sql->insert("UserFileTokenFile", array("token_id", "file_id")); + foreach ($fileIds as $fileId) { + $query->addRow($tokenId, $fileId); + } + + $res = $query->execute(); + $this->success = ($res !== false); + $this->lastError = $sql->getLastError(); + + if ($this->success) { + $this->result["token"] = $token; + } + + return $this->success; + } + } +} diff --git a/core/Api/PatchSQL.class.php b/core/Api/PatchSQL.class.php new file mode 100644 index 0000000..a985a32 --- /dev/null +++ b/core/Api/PatchSQL.class.php @@ -0,0 +1,65 @@ + new StringType("className", 64) + )); + $this->loginRequired = true; + $this->csrfTokenRequired = false; + } + + public function execute($values = array()) { + if (!parent::execute($values)) { + return false; + } + + $className = $this->getParam("className"); + $fullClassName = "\\Configuration\\Patch\\" . $className; + $path = getClassPath($fullClassName, true); + if (!file_exists($path)) { + return $this->createError("File not found"); + } + + if(!class_exists($fullClassName)) { + return $this->createError("Class not found."); + } + + try { + $reflection = new \ReflectionClass($fullClassName); + if (!$reflection->isInstantiable()) { + return $this->createError("Class is not instantiable"); + } + + if (!$reflection->isSubclassOf(DatabaseScript::class)) { + return $this->createError("Not a database script."); + } + + $sql = $this->user->getSQL(); + $obj = $reflection->newInstance(); + $queries = $obj->createQueries($sql); + if (!is_array($queries)) { + return $this->createError("Database script returned invalid values"); + } + + foreach($queries as $query) { + if (!$query->execute()) { + return $this->createError("Query error: " . $sql->getLastError()); + } + } + + $this->success = true; + } catch (\ReflectionException $e) { + return $this->createError("Error reflecting class: " . $e->getMessage()); + } + + return $this->success; + } +} \ No newline at end of file diff --git a/core/Api/Request.class.php b/core/Api/Request.class.php index d6e58dd..9a6e18f 100644 --- a/core/Api/Request.class.php +++ b/core/Api/Request.class.php @@ -187,6 +187,7 @@ class Request { public function success() { return $this->success; } public function loginRequired() { return $this->loginRequired; } public function isExternalCall() { return $this->externalCall; } + public function clearError() { $this->success = true; $this->lastError = ""; } private function getMethod() { $class = str_replace("\\", "/", get_class($this)); diff --git a/core/Api/SettingsAPI.class.php b/core/Api/SettingsAPI.class.php index 3f1b568..cecb991 100644 --- a/core/Api/SettingsAPI.class.php +++ b/core/Api/SettingsAPI.class.php @@ -39,7 +39,7 @@ namespace Api\Settings { $query = $sql->select("name", "value") ->from("Settings"); - if (!is_null($key) && !empty($key)) { + if (!is_null($key)) { $query->where(new CondRegex(new Column("name"), $key)); } diff --git a/core/Configuration/CreateDatabase.class.php b/core/Configuration/CreateDatabase.class.php index 35be6fb..6567751 100755 --- a/core/Configuration/CreateDatabase.class.php +++ b/core/Configuration/CreateDatabase.class.php @@ -6,7 +6,7 @@ use Driver\SQL\SQL; use \Driver\SQL\Strategy\SetNullStrategy; use \Driver\SQL\Strategy\CascadeStrategy; -class CreateDatabase { +class CreateDatabase extends DatabaseScript { // NOTE: // explicit serial ids removed due to postgres' serial implementation @@ -192,7 +192,10 @@ class CreateDatabase { ->addRow("User/edit", array(USER_GROUP_ADMIN), "Allows users to edit details and group memberships of any user") ->addRow("User/delete", array(USER_GROUP_ADMIN), "Allows users to delete any other user") ->addRow("Permission/fetch", array(USER_GROUP_ADMIN), "Allows users to list all API permissions") - ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics"); + ->addRow("Visitors/stats", array(USER_GROUP_ADMIN, USER_GROUP_SUPPORT), "Allows users to see visitor statistics") + ->addRow("PatchSQL", array(USER_GROUP_ADMIN), "Allows users to import database patches"); + + self::loadPatches($queries, $sql); return $queries; } @@ -225,4 +228,22 @@ class CreateDatabase { "Best Regards
" . "{{site_name}} Administration"; } + + private static function loadPatches(&$queries, $sql) { + $patchDirectory = './core/Configuration/Patch/'; + if (file_exists($patchDirectory) && is_dir($patchDirectory)) { + $scan_arr = scandir($patchDirectory); + $files_arr = array_diff($scan_arr, array('.','..')); + foreach ($files_arr as $file) { + $suffix = ".class.php"; + if (endsWith($file, $suffix)) { + $className = substr($file, 0, strlen($file) - strlen($suffix)); + $className = "\\Configuration\\Patch\\$className"; + $method = "$className::createQueries"; + $patchQueries = call_user_func($method, $sql); + foreach($patchQueries as $query) $queries[] = $query; + } + } + } + } } diff --git a/core/Configuration/DatabaseScript.class.php b/core/Configuration/DatabaseScript.class.php new file mode 100644 index 0000000..90d3018 --- /dev/null +++ b/core/Configuration/DatabaseScript.class.php @@ -0,0 +1,9 @@ +insert("ApiPermission", array("method", "groups", "description")) + ->onDuplicateKeyStrategy(new UpdateStrategy(array("method"), array("method" => new Column("method")))) + ->addRow("File/Download", array(), "Allows users to download files when logged in, or using a given token") + ->addRow("File/Upload", array(), "Allows users to upload files when logged in, or using a given token") + ->addRow("File/ValidateToken", array(), "Allows users to validate a given token") + ->addRow("File/RevokeToken", array(USER_GROUP_ADMIN), "Allows users to revoke a token") + ->addRow("File/ListFiles", array(), "Allows users to list all files assigned to an account") + ->addRow("File/ListTokens", array(USER_GROUP_ADMIN), "Allows users to list all tokens assigned to the virtual filesystem of an account") + ->addRow("File/CreateDirectory", array(), "Allows users to create a virtual directory") + ->addRow("File/Rename", array(), "Allows users to rename files in the virtual filesystem") + ->addRow("File/Move", array(), "Allows users to move files in the virtual filesystem") + ->addRow("File/Delete", array(), "Allows users to delete files in the virtual filesystem") + ->addRow("File/CreateUploadToken", array(USER_GROUP_ADMIN), "Allows users to create a token to upload files to the virtual filesystem assigned to the users account") + ->addRow("File/CreateDownloadToken", array(USER_GROUP_ADMIN), "Allows users to create a token to download files from the virtual filesystem assigned to the users account"); + + $queries[] = $sql->, array("request", "action", "target", "extra")) + ->onDuplicateKeyStrategy(new UpdateStrategy(array("request"), array("request" => new Column("request")))) + ->addRow("^/files(/.*)?$", "dynamic", "\\Documents\\Files", NULL); + + $queries[] = $sql->createTable("UserFile") + ->onlyIfNotExists() + ->addSerial("uid") + ->addBool("directory") + ->addString("name", 64, false) + ->addString("path", 512, true) + ->addInt("parent_id", true) + ->addInt("user_id", true) + ->primaryKey("uid") + ->unique("parent_id", "name") + ->foreignKey("parent_id", "UserFile", "uid", new CascadeStrategy()) + ->foreignKey("user_id", "User", "uid", new CascadeStrategy()); + + $queries[] = $sql->createTable("UserFileToken") + ->onlyIfNotExists() + ->addSerial("uid") + ->addString("token", 36, false) + ->addDateTime("valid_until", true) + ->addEnum("token_type", array("download", "upload")) + ->addInt("user_id") + # upload only: + ->addInt("maxFiles", true) + ->addInt("maxSize", true) + ->addString("extensions", 64, true) + ->primaryKey("uid") + ->foreignKey("user_id", "User", "uid"); + + $queries[] = $sql->createTable("UserFileTokenFile") + ->addInt("file_id") + ->addInt("token_id") + ->unique("file_id", "token_id") + ->foreignKey("file_id", "UserFile", "uid") + ->foreignKey("token_id", "UserFileToken", "uid"); + + return $queries; + } +} diff --git a/core/Documents/Files.class.php b/core/Documents/Files.class.php new file mode 100644 index 0000000..9d9ec25 --- /dev/null +++ b/core/Documents/Files.class.php @@ -0,0 +1,61 @@ +loadBootstrap(); + $this->loadFontawesome(); + } + + protected function initMetas() { + return array( + array('name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0'), + array('name' => 'format-detection', 'content' => 'telephone=yes'), + array('charset' => 'utf-8'), + array("http-equiv" => 'expires', 'content' => '0'), + array("name" => 'robots', 'content' => 'noarchive') + ); + } + + protected function initRawFields() { + return array(); + } + + protected function initTitle() { + return "File Control Panel"; + } + } + + class FilesBody extends SimpleBody { + + public function __construct($document) { + parent::__construct($document); + } + + protected function getContent() { + $html = ""; + $html .= "
"; + return $html; + } + } + +} diff --git a/core/Elements/EmptyBody.class.php b/core/Elements/EmptyBody.class.php new file mode 100644 index 0000000..f762407 --- /dev/null +++ b/core/Elements/EmptyBody.class.php @@ -0,0 +1,7 @@ +createList($items, "ul"); } - protected function createLink($link, $title=null) { + protected function createLink($link, $title=null, $classes="") { if(is_null($title)) $title=$link; - return "$title"; + if(!empty($classes)) $classes = " class=\"$classes\""; + return "$title"; } protected function createExternalLink($link, $title=null) { @@ -123,14 +122,91 @@ abstract class View extends StaticView { return $this->createStatusText("info", $text, $id, $hidden); } - protected function createStatusText($type, $text, $id="", $hidden=false) { + protected function createStatusText($type, $text, $id="", $hidden=false, $classes="") { if(strlen($id) > 0) $id = " id=\"$id\""; - $hidden = ($hidden?" hidden" : ""); - return "
$text
"; + if($hidden) $classes .= " hidden"; + if(strlen($classes) > 0) $classes = " $classes"; + return "
$text
"; } protected function createBadge($type, $text) { $text = htmlspecialchars($text); return "$text"; } -} \ No newline at end of file + + protected function createJumbotron(string $content, bool $fluid=false, $class="") { + $jumbotronClass = "jumbotron" . ($fluid ? "-fluid" : ""); + if (!empty($class)) $jumbotronClass .= " $class"; + + return " +
+
+
+ $content +
+
+
"; + } + + public function createSimpleParagraph(string $content, string $class="") { + if($class) $class = " class=\"$class\""; + return "$content

"; + } + + public function createParagraph($title, $id, $content) { + $id = replaceCssSelector($id); + $iconId = urlencode("$id-icon"); + return " +
+
+

$title

+
+ $content +
+
"; + } + + protected function createBootstrapTable($data, string $classes="") { + $classes = empty($classes) ? "" : " $classes"; + $code = "
"; + foreach($data as $row) { + $code .= "
"; + $columnCount = count($row); + if($columnCount > 0) { + $remainingSize = 12; + $columnSize = 12 / $columnCount; + foreach($row as $col) { + $size = ($columnSize <= $remainingSize ? $columnSize : $remainingSize); + $content = $col; + $class = ""; + $code .= " $val) { + if(strcmp($key, "content") === 0) { + $content = $val; + } else if(strcmp($key, "class") === 0) { + $class = " " . $col["class"]; + } else if(strcmp($key, "cols") === 0 && is_numeric($val)) { + $size = intval($val); + } else { + $code .= " $key=\"$val\""; + } + } + + if(isset($col["class"])) $class = " " . $col["class"]; + } + + if($size <= 6) $class .= " col-md-" . intval($size * 2); + $code .= " class=\"col-lg-$size$class\">$content
"; + $remainingSize -= $size; + } + } + $code .= "
"; + } + + $code .= ""; + return $code; + } +} diff --git a/core/core.php b/core/core.php index f456348..a38a4d9 100644 --- a/core/core.php +++ b/core/core.php @@ -1,6 +1,6 @@ ]~", "_", preg_replace("~[:\-]~", "", $sel)); } +function urlId($str) { + return urlencode(htmlspecialchars(preg_replace("[: ]","-", $str))); +} + function getClassPath($class, $suffix = true) { $path = str_replace('\\', '/', $class); $path = array_values(array_filter(explode("/", $path))); @@ -134,7 +138,6 @@ function serveStatic(string $webRoot, string $file) { $length = $size; if (isset($_SERVER['HTTP_RANGE'])) { - $partialContent = true; preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches); $offset = intval($matches[1]); $length = intval($matches[2]) - $offset; @@ -166,4 +169,4 @@ function parseClass($class) { $parts = explode("\\", $class); $parts = array_map('ucfirst', $parts); return implode("\\", $parts); -} \ No newline at end of file +} diff --git a/files/uploaded/.htaccess b/files/uploaded/.htaccess new file mode 100644 index 0000000..d3223d4 --- /dev/null +++ b/files/uploaded/.htaccess @@ -0,0 +1 @@ +DENY FROM ALL diff --git a/js/account.js b/js/account.js index 830dae8..fe585b7 100644 --- a/js/account.js +++ b/js/account.js @@ -27,6 +27,8 @@ $(document).ready(function () { } // Login + $("#username").keypress(function (e) { if(e.which == 13) $("#password").focus(); }); + $("#password").keypress(function (e) { if(e.which == 13) $("#btnLogin").click(); }); $("#btnLogin").click(function() { const username = $("#username").val(); const password = $("#password").val(); @@ -45,7 +47,7 @@ $(document).ready(function () { btn.prop("disabled", false); $("#password").val(""); createdDiv.hide(); - showAlert(res.msg); + showAlert("danger", res.msg); } }); }); @@ -172,4 +174,4 @@ $(document).ready(function () { }); } }); -}); \ No newline at end of file +});