From ab5bb6b45908051c26f3b2a5041f7c643c8b12a9 Mon Sep 17 00:00:00 2001 From: mrilyew <99399973+mrilyew@users.noreply.github.com> Date: Sat, 28 Dec 2024 13:35:55 +0300 Subject: [PATCH] add upload, previews, most of api methods --- VKAPI/Handlers/Docs.php | 151 ++++++++++++++--- Web/Models/Entities/Document.php | 154 ++++++++++++++++-- Web/Models/Entities/Postable.php | 19 +++ Web/Models/Repositories/Documents.php | 63 ++++++- Web/Models/Repositories/Photos.php | 14 +- Web/Presenters/DocumentsPresenter.php | 52 ++++++ Web/Presenters/templates/Documents/Upload.xml | 69 ++++++++ Web/routes.yml | 2 + locales/ru.strings | 18 ++ openvk-example.yml | 3 + 10 files changed, 505 insertions(+), 40 deletions(-) create mode 100644 Web/Presenters/templates/Documents/Upload.xml diff --git a/VKAPI/Handlers/Docs.php b/VKAPI/Handlers/Docs.php index a7abc842..271e64cd 100644 --- a/VKAPI/Handlers/Docs.php +++ b/VKAPI/Handlers/Docs.php @@ -2,23 +2,41 @@ namespace openvk\VKAPI\Handlers; use Chandler\Database\DatabaseConnection; use openvk\Web\Models\Entities\Document; +use openvk\Web\Models\Repositories\Documents; final class Docs extends VKAPIRequestHandler { - function add(int $owner_id, int $doc_id, ?string $access_key): int + function add(int $owner_id, int $doc_id, ?string $access_key): string { $this->requireUser(); $this->willExecuteWriteAction(); - return 0; + $doc = (new Documents)->getDocumentById($owner_id, $doc_id); + if(!$doc || $doc->isDeleted()) + $this->fail(1150, "Invalid document id"); + + if(!$doc->checkAccessKey($access_key)) + $this->fail(15, "Access denied"); + + if($doc->isCopiedBy($this->getUser())) + $this->fail(100, "One of the parameters specified was missing or invalid: this document already added"); + + $new_doc = $doc->copy($this->getUser()); + + return $new_doc->getPrettyId(); } function delete(int $owner_id, int $doc_id): int { $this->requireUser(); $this->willExecuteWriteAction(); + $doc = (new Documents)->getDocumentById($owner_id, $doc_id); + if(!$doc || $doc->isDeleted()) + $this->fail(1150, "Invalid document id"); - return 0; + if(!$doc->canBeModifiedBy($this->getUser())) + $this->fail(1153, "Access to document is denied"); + return 1; } function restore(int $owner_id, int $doc_id): int @@ -26,7 +44,7 @@ final class Docs extends VKAPIRequestHandler $this->requireUser(); $this->willExecuteWriteAction(); - return 0; + return $this->add($owner_id, $doc_id, ""); } function edit(int $owner_id, int $doc_id, ?string $title, ?string $tags, ?int $folder_id): int @@ -34,28 +52,128 @@ final class Docs extends VKAPIRequestHandler $this->requireUser(); $this->willExecuteWriteAction(); - return 0; + $doc = (new Documents)->getDocumentById($owner_id, $doc_id); + if(!$doc || $doc->isDeleted()) + $this->fail(1150, "Invalid document id"); + if(!$doc->canBeModifiedBy($this->getUser())) + $this->fail(1153, "Access to document is denied"); + if(iconv_strlen($title ?? "") > 128 || iconv_strlen($title ?? "") < 0) + $this->fail(1152, "Invalid document title"); + if(iconv_strlen($tags ?? "") > 256) + $this->fail(1154, "Invalid tags"); + + if($title) + $doc->setName($title); + if($tags) + $doc->setTags($tags); + if($folder_id) { + if(in_array($folder_id, [0, 4])) + $doc->setFolder_id($folder_id); + } + + try { + $doc->setEdited(time()); + $doc->save(); + } catch(\Throwable $e) { + return 1; + } + + return 1; } - function get(int $count = 30, int $offset = 0, int $type = 0, int $owner_id = NULL, int $return_tags = 0): int + function get(int $count = 30, int $offset = 0, int $type = -1, int $owner_id = NULL, int $return_tags = 0, int $order = 0): object + { + $this->requireUser(); + if(!$owner_id) + $owner_id = $this->getUser()->getId(); + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) + $this->fail(15, "Access denied"); + + $documents = (new Documents)->getDocumentsByOwner($owner_id, $order, $type); + $res = (object)[ + "count" => $documents->size(), + "items" => [], + ]; + + foreach($documents->offsetLimit($offset, $count) as $doc) { + $res->items[] = $doc->toVkApiStruct($this->getUser(), $return_tags == 1); + } + + return $res; + } + + function getById(string $docs, int $return_tags = 0): array { $this->requireUser(); - return 0; - } + $item_ids = explode(",", $docs); + $response = []; + if(sizeof($item_ids) < 1) { + $this->fail(100, "One of the parameters specified was missing or invalid: docs is undefined"); + } - function getById(string $docs, int $return_tags = 0): int - { - $this->requireUser(); + foreach($item_ids as $id) { + $splitted_id = explode("_", $id); + $doc = (new Documents)->getDocumentById((int)$splitted_id[0], (int)$splitted_id[1]); + if(!$doc || $doc->isDeleted()) + continue; - return 0; + if(!$doc->checkAccessKey($splitted_id[2])) + continue; + + $response[] = $doc->toVkApiStruct($this->getUser(), $return_tags === 1); + } + + return $response; } function getTypes(?int $owner_id) { $this->requireUser(); + if(!$owner_id) + $owner_id = $this->getUser()->getId(); + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) + $this->fail(15, "Access denied"); + + $types = (new Documents)->getTypes($owner_id); + return [ + "count" => sizeof($types), + "items" => $types, + ]; + } - return []; + function search(string $q = "", int $search_own = -1, int $order = -1, int $count = 30, int $offset = 0, int $return_tags = 0, int $type = 0, ?string $tags = NULL): object + { + $this->requireUser(); + + $params = []; + $o_order = ["type" => "id", "invert" => false]; + + if(iconv_strlen($q) > 512) + $this->fail(100, "One of the parameters specified was missing or invalid: q should be not more 512 letters length"); + + if(in_array($type, [1,2,3,4,5,6,7,8])) + $params["type"] = $type; + + if(iconv_strlen($tags ?? "") < 512) + $params["tags"] = $tags; + + if($search_own === 1) + $params["owner_id"] = $this->getUser()->getId(); + + $documents = (new Documents)->find($q, $params, $o_order); + $res = (object)[ + "count" => $documents->size(), + "items" => [], + ]; + + foreach($documents->offsetLimit($offset, $count) as $doc) { + $res->items[] = $doc->toVkApiStruct($this->getUser(), $return_tags == 1); + } + + return $res; } function getUploadServer(?int $group_id = NULL) @@ -81,11 +199,4 @@ final class Docs extends VKAPIRequestHandler return 0; } - - function search(string $q, int $search_own = 0, int $count = 30, int $offset = 0, int $return_tags = 0, int $type = 0, ?string $tags = NULL): object - { - $this->requireUser(); - - return 0; - } } diff --git a/Web/Models/Entities/Document.php b/Web/Models/Entities/Document.php index 4a3fdc42..5870548b 100644 --- a/Web/Models/Entities/Document.php +++ b/Web/Models/Entities/Document.php @@ -2,11 +2,15 @@ namespace openvk\Web\Models\Entities; use openvk\Web\Models\Repositories\{Clubs, Users, Photos}; use openvk\Web\Models\Entities\{Photo}; +use openvk\Web\Models\RowModel; +use Nette\InvalidStateException as ISE; +use Chandler\Database\DatabaseConnection; class Document extends Media { protected $tableName = "documents"; protected $fileExtension = "gif"; + private $tmp_format = NULL; const VKAPI_TYPE_TEXT = 1; const VKAPI_TYPE_ARCHIVE = 2; @@ -31,12 +35,6 @@ class Document extends Media return "$dir/$hash." . $this->getFileExtension(); } - protected function saveFile(string $filename, string $hash): bool - { - - return true; - } - function getURL(): string { $hash = $this->getRecord()->hash; @@ -63,6 +61,71 @@ class Document extends Media } } + protected function saveFile(string $filename, string $hash): bool + { + move_uploaded_file($filename, $this->pathFromHash($hash)); + return true; + } + + protected function makePreview(string $tmp_name, string $filename, int $owner): bool + { + $preview_photo = new Photo; + $preview_photo->setOwner($owner); + $preview_photo->setDescription("internal use"); + $preview_photo->setCreated(time()); + $preview_photo->setSystem(1); + $preview_photo->setFile([ + "tmp_name" => $tmp_name, + "error" => 0, + ]); + $preview_photo->save(); + $this->stateChanges("preview", "photo_".$preview_photo->getId()); + + return true; + } + + private function updateHash(string $hash): bool + { + $this->stateChanges("hash", $hash); + + return true; + } + + function setFile(array $file): void + { + if($file["error"] !== UPLOAD_ERR_OK) + throw new ISE("File uploaded is corrupted"); + + $original_name = $file["name"]; + $file_format = explode(".", $original_name)[1]; + $file_size = $file["size"]; + $type = Document::detectTypeByFormat($file_format); + + if(!$file_format) + throw new \TypeError("No file format"); + + if(!in_array($file_format, OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["allowedFormats"])) + throw new \TypeError("Forbidden file format"); + + if($file_size < 1 || $file_size > (OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["maxSize"] * 1024 * 1024)) + throw new \ValueError("Invalid filesize"); + + $hash = hash_file("whirlpool", $file["tmp_name"]); + $this->stateChanges("original_name", $original_name); + $this->tmp_format = $file_format; + $this->stateChanges("format", $file_format); + $this->stateChanges("filesize", $file_size); + $this->stateChanges("hash", $hash); + $this->stateChanges("access_key", bin2hex(random_bytes(9))); + $this->stateChanges("type", $type); + + if(in_array($type, [3, 4])) { + $this->makePreview($file["tmp_name"], $original_name, $file["preview_owner"]); + } + + $this->saveFile($file["tmp_name"], $hash); + } + function hasPreview(): bool { return $this->getRecord()->preview != NULL; @@ -88,8 +151,47 @@ class Document extends Media return false; } + function isCopiedBy(User $user): bool + { + if($user->getId() === $this->getOwnerID()) + return true; + + return DatabaseConnection::i()->getContext()->table("documents")->where([ + "owner" => $user->getId(), + "copy_of" => $this->getId(), + ])->count() > 0; + } + + function copy(User $user): Document + { + $this_document_array = $this->getRecord()->toArray(); + + $new_document = new Document; + $new_document->setOwner($user->getId()); + $new_document->updateHash($this_document_array["hash"]); + $new_document->setOwner_hidden(1); + $new_document->setCopy_of($this->getId()); + $new_document->setName($this->getId()); + $new_document->setOriginal_name($this->getOriginalName()); + $new_document->setAccess_key(bin2hex(random_bytes(9))); + $new_document->setFormat($this_document_array["format"]); + $new_document->setType($this->getVKAPIType()); + $new_document->setFolder_id(0); + $new_document->setPreview($this_document_array["preview"]); + $new_document->setTags($this_document_array["tags"]); + $new_document->setFilesize($this_document_array["filesize"]); + + $new_document->save(); + + return $new_document; + } + function getFileExtension(): string { + if($this->tmp_format) { + return $this->tmp_format; + } + return $this->getRecord()->format; } @@ -125,7 +227,11 @@ class Document extends Media function getTags(): array { - return explode(",", $this->getRecord()->tags); + $tags = $this->getRecord()->tags; + if(!$tags) + return []; + + return explode(",", $tags ?? ""); } function getFilesize(): int @@ -177,7 +283,7 @@ class Document extends Media return $this->getOwnerID() === $user->getId(); } - function toVkApiStruct(?User $user = NULL): object + function toVkApiStruct(?User $user = NULL, bool $return_tags = false): object { $res = new \stdClass; $res->id = $this->getId(); @@ -191,15 +297,39 @@ class Document extends Media $res->is_licensed = (int) $this->isLicensed(); $res->is_unsafe = (int) $this->isUnsafe(); $res->folder_id = (int) $this->getFolder(); + $res->access_key = $this->getAccessKey(); $res->private_url = ""; - if($user) { + if($user) $res->can_manage = $this->canBeModifiedBy($user); - } - if($this->hasPreview()) { + if($this->hasPreview()) $res->preview = $this->toApiPreview(); - } + + if($return_tags) + $res->tags = $this->getTags(); return $res; } + + static function detectTypeByFormat(string $format) + { + switch($format) { + case "txt": case "docx": case "doc": case "odt": case "pptx": case "ppt": case "xlsx": case "xls": + return 1; + case "zip": case "rar": case "7z": + return 2; + case "gif": case "apng": + return 3; + case "jpg": case "jpeg": case "png": case "psd": case "ps": + return 4; + case "mp3": + return 5; + case "mp4": case "avi": + return 6; + case "pdf": case "djvu": case "epub": case "fb2": + return 7; + default: + return 8; + } + } } diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index fc6cb8ca..911ef4be 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -106,6 +106,25 @@ abstract class Postable extends Attachable yield $user; } } + + function getAccessKey(): string + { + return $this->getRecord()->access_key; + } + + function checkAccessKey(?string $access_key): bool + { + if($this->getAccessKey() === $access_key) { + return true; + } + + return !$this->isPrivate(); + } + + function isPrivate(): bool + { + return (bool) $this->getRecord()->unlisted; + } function isAnonymous(): bool { diff --git a/Web/Models/Repositories/Documents.php b/Web/Models/Repositories/Documents.php index a03afb2d..f535418b 100644 --- a/Web/Models/Repositories/Documents.php +++ b/Web/Models/Repositories/Documents.php @@ -3,6 +3,7 @@ namespace openvk\Web\Models\Repositories; use openvk\Web\Models\Entities\Document; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Repositories\Util\EntityStream; class Documents { @@ -26,7 +27,7 @@ class Documents } # By "Virtual ID" and "Absolute ID" (to not leak owner's id). - function getDocumentById(int $virtual_id, int $real_id, ?string $access_key = NULL): ?Post + function getDocumentById(int $virtual_id, int $real_id, ?string $access_key = NULL): ?Document { $doc = $this->documents->where(['virtual_id' => $virtual_id, 'id' => $real_id]); @@ -42,9 +43,57 @@ class Documents } + function getDocumentsByOwner(int $owner, int $order = 0, int $type = -1): EntityStream + { + $search = $this->documents->where([ + "owner" => $owner, + "unlisted" => 0, + "deleted" => 0, + ]); + + if(in_array($type, [1,2,3,4,5,6,7,8])) { + $search->where("type", $type); + } + + switch($order) { + case 0: + $search->order("id DESC"); + break; + case 1: + $search->order("name DESC"); + break; + case 2: + $search->order("filesize DESC"); + break; + } + + return new EntityStream("Document", $search); + } + + function getTypes(int $owner_id): array + { + $result = DatabaseConnection::i()->getConnection()->query("SELECT `type`, COUNT(*) AS `count` FROM `documents` WHERE `owner` = $owner_id GROUP BY `type` ORDER BY `type`"); + $response = []; + foreach($result as $res) { + if($res->count < 1) continue; + + $name = tr("document_type_".$res->type); + $response[] = [ + "count" => $res->count, + "type" => $res->type, + "name" => $name, + ]; + } + + return $response; + } + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream { - $result = $this->documents->where("title LIKE ?", "%$query%")->where("deleted", 0); + $result = $this->documents->where("name LIKE ?", "%$query%")->where([ + "deleted" => 0, + "folder_id != " => 0, + ]); $order_str = 'id'; switch($order['type']) { @@ -55,8 +104,14 @@ class Documents foreach($params as $paramName => $paramValue) { switch($paramName) { - case "before": - $result->where("created < ?", $paramValue); + case "type": + $result->where("type", $paramValue); + break; + case "tags": + $result->where("tags", $paramValue); + break; + case "owner_id": + $result->where("owner", $paramValue); break; } } diff --git a/Web/Models/Repositories/Photos.php b/Web/Models/Repositories/Photos.php index a024747c..21d4a806 100644 --- a/Web/Models/Repositories/Photos.php +++ b/Web/Models/Repositories/Photos.php @@ -27,6 +27,8 @@ class Photos $photo = $this->photos->where([ "owner" => $owner, "virtual_id" => $vId, + "system" => 0, + "private" => 0, ])->fetch(); if(!$photo) return NULL; @@ -37,8 +39,10 @@ class Photos { $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $photos = $this->photos->where([ - "owner" => $user->getId(), - "deleted" => 0 + "owner" => $user->getId(), + "deleted" => 0, + "system" => 0, + "private" => 0, ])->order("id DESC"); foreach($photos->limit($limit, $offset) as $photo) { @@ -49,8 +53,10 @@ class Photos function getUserPhotosCount(User $user) { $photos = $this->photos->where([ - "owner" => $user->getId(), - "deleted" => 0 + "owner" => $user->getId(), + "deleted" => 0, + "system" => 0, + "private" => 0, ]); return sizeof($photos); diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php index da39f48d..fbf4caf4 100644 --- a/Web/Presenters/DocumentsPresenter.php +++ b/Web/Presenters/DocumentsPresenter.php @@ -1,6 +1,7 @@ renderList($gid); } + + function renderUpload() + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $group = NULL; + $isAjax = $this->postParam("ajax", false) == 1; + $ref = $this->postParam("referrer", false) ?? "user"; + + if(!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs)->get($gid); + if(!$group) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$group->canUploadDocs($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + $this->template->group = $group; + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $owner = $this->user->id; + if($group) { + $owner = $group->getRealId(); + } + + $upload = $_FILES["blob"]; + $name = $this->postParam("name"); + $tags = $this->postParam("tags"); + $folder = $this->postParam("folder"); + $owner_hidden = ($this->postParam("owner_hidden") ?? "off") === "on"; + + $document = new Document; + $document->setOwner($owner); + $document->setName($name); + $document->setFolder_id($folder); + $document->setTags(empty($tags) ? NULL : $tags); + $document->setOwner_hidden($owner_hidden); + $document->setFile([ + "tmp_name" => $upload["tmp_name"], + "error" => $upload["error"], + "name" => $upload["name"], + "size" => $upload["size"], + "preview_owner" => $this->user->id, + ]); + + $document->save(); + } } diff --git a/Web/Presenters/templates/Documents/Upload.xml b/Web/Presenters/templates/Documents/Upload.xml new file mode 100644 index 00000000..fcab2fd0 --- /dev/null +++ b/Web/Presenters/templates/Documents/Upload.xml @@ -0,0 +1,69 @@ +{extends "../@layout.xml"} + +{block title}{_document_uploading_in_general}{/block} + +{block header} + {if !is_null($group)} + {$group->getCanonicalName()} + » + {_documents} + {else} + {$thisUser->getCanonicalName()} + » + {_documents} + {/if} + + » + загрузка +{/block} + +{block content} +
+{/block} diff --git a/Web/routes.yml b/Web/routes.yml index f7150670..f6bf9437 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -315,6 +315,8 @@ routes: handler: "Documents->list" - url: "/docs{num}" handler: "Documents->listGroup" + - url: "/docs/upload" + handler: "Documents->upload" - url: "/admin" handler: "Admin->index" - url: "/admin/users" diff --git a/locales/ru.strings b/locales/ru.strings index 93439550..f905fe7e 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -2203,3 +2203,21 @@ "upd_in_general" = "Обновление фотографии страницы"; "on_wall" = "На стене"; "sign_short" = "Подпись"; + +/* Documents */ + +"documents" = "Документы"; +"document_uploading_in_general" = "Загрузка документа"; +"file" = "Файл"; +"tags" = "Теги"; +"accessbility" = "Доступность"; + +"document_type_0" = "Все"; +"document_type_1" = "Текстовые"; +"document_type_2" = "Архивы"; +"document_type_3" = "GIF"; +"document_type_4" = "Изображения"; +"document_type_5" = "Аудио"; +"document_type_6" = "Видео"; +"document_type_7" = "Книги"; +"document_type_8" = "Остальные"; diff --git a/openvk-example.yml b/openvk-example.yml index b8cb899f..51578e28 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -22,6 +22,9 @@ openvk: photoSaving: "quick" videos: disableUploading: false + docs: + maxSize: 10 # in megabytes + allowedFormats: ["jpg", "jpeg", "png", "gif", "psd", "aep", "docx", "doc", "odt", "txt", "pptx", "ppt", "xls", "xlsx", "pdf", "djvu", "fb2", "ps", "apk", "zip", "7z", "mp4", "avi", "mp3", "flac"] apps: withdrawTax: 8 security: