From de82eb87929ac99227b5498724be207c25f0e734 Mon Sep 17 00:00:00 2001 From: mrilyew <99399973+mrilyew@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:35:38 +0300 Subject: [PATCH 01/16] create document entity --- VKAPI/Handlers/Docs.php | 91 +++++++++ Web/Models/Entities/Document.php | 205 ++++++++++++++++++++ Web/Models/Repositories/Documents.php | 69 +++++++ Web/Presenters/DocumentsPresenter.php | 19 ++ Web/Presenters/templates/Documents/List.xml | 11 ++ Web/di.yml | 2 + Web/routes.yml | 4 + install/sqls/00054-docs.sql | 28 +++ 8 files changed, 429 insertions(+) create mode 100644 VKAPI/Handlers/Docs.php create mode 100644 Web/Models/Entities/Document.php create mode 100644 Web/Models/Repositories/Documents.php create mode 100644 Web/Presenters/DocumentsPresenter.php create mode 100644 Web/Presenters/templates/Documents/List.xml create mode 100644 install/sqls/00054-docs.sql diff --git a/VKAPI/Handlers/Docs.php b/VKAPI/Handlers/Docs.php new file mode 100644 index 00000000..a7abc842 --- /dev/null +++ b/VKAPI/Handlers/Docs.php @@ -0,0 +1,91 @@ +requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function delete(int $owner_id, int $doc_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function restore(int $owner_id, int $doc_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function edit(int $owner_id, int $doc_id, ?string $title, ?string $tags, ?int $folder_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function get(int $count = 30, int $offset = 0, int $type = 0, int $owner_id = NULL, int $return_tags = 0): int + { + $this->requireUser(); + + return 0; + } + + function getById(string $docs, int $return_tags = 0): int + { + $this->requireUser(); + + return 0; + } + + function getTypes(?int $owner_id) + { + $this->requireUser(); + + return []; + } + + function getUploadServer(?int $group_id = NULL) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function getWallUploadServer(?int $group_id = NULL) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return 0; + } + + function save(string $file, string $title, string $tags, ?int $return_tags = 0) + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + 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 new file mode 100644 index 00000000..4a3fdc42 --- /dev/null +++ b/Web/Models/Entities/Document.php @@ -0,0 +1,205 @@ +getBaseDir() . substr($hash, 0, 2); + if(!is_dir($dir)) + mkdir($dir); + + return "$dir/$hash." . $this->getFileExtension(); + } + + protected function saveFile(string $filename, string $hash): bool + { + + return true; + } + + function getURL(): string + { + $hash = $this->getRecord()->hash; + $filetype = $this->getFileExtension(); + + switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) { + default: + case "default": + case "basic": + return "http://" . $_SERVER['HTTP_HOST'] . "/blob_" . substr($hash, 0, 2) . "/$hash.$filetype"; + break; + case "accelerated": + return "http://" . $_SERVER['HTTP_HOST'] . "/openvk-datastore/$hash.$filetype"; + break; + case "server": + $settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"]; + return ( + $settings->protocol ?? ovk_scheme() . + "://" . $settings->host . + $settings->path . + substr($hash, 0, 2) . "/$hash.$filetype" + ); + break; + } + } + + function hasPreview(): bool + { + return $this->getRecord()->preview != NULL; + } + + function isOwnerHidden(): bool + { + return (bool) $this->getRecord()->owner_hidden; + } + + function isCopy(): bool + { + return $this->getRecord()->copy_of != NULL; + } + + function isLicensed(): bool + { + return false; + } + + function isUnsafe(): bool + { + return false; + } + + function getFileExtension(): string + { + return $this->getRecord()->format; + } + + function getPrettyId(): string + { + return $this->getVirtualId() . "_" . $this->getId(); + } + + function getOriginal(): Document + { + return $this->getRecord()->copy_of; + } + + function getName(): string + { + return $this->getRecord()->name; + } + + function getOriginalName(): string + { + return $this->getRecord()->original_name; + } + + function getVKAPIType(): int + { + return $this->getRecord()->type; + } + + function getFolder(): int + { + return $this->getRecord()->folder_id; + } + + function getTags(): array + { + return explode(",", $this->getRecord()->tags); + } + + function getFilesize(): int + { + return $this->getRecord()->filesize; + } + + function getPreview(): ?RowModel + { + $preview_array = $this->getRecord()->preview; + $preview = explode(",", $this->getRecord()->preview)[0]; + $model = NULL; + $exploded = explode("_", $preview); + + switch($exploded[0]) { + case "photo": + $model = (new Photos)->get((int)$exploded[1]); + break; + } + + return $model; + } + + function getOwnerID(): int + { + return $this->getRecord()->owner; + } + + function toApiPreview(): object + { + $preview = $this->getPreview(); + if($preview instanceof Photo) { + return (object)[ + "photo" => [ + "sizes" => array_values($preview->getVkApiSizes()), + ], + ]; + } + } + + function canBeModifiedBy(User $user = NULL): bool + { + if(!$user) + return false; + + if($this->getOwnerID() < 0) + return (new Clubs)->get(abs($this->getOwnerID()))->canBeModifiedBy($user); + + return $this->getOwnerID() === $user->getId(); + } + + function toVkApiStruct(?User $user = NULL): object + { + $res = new \stdClass; + $res->id = $this->getId(); + $res->owner_id = $this->getVirtualId(); + $res->title = $this->getName(); + $res->size = $this->getFilesize(); + $res->ext = $this->getFileExtension(); + $res->url = $this->getURL(); + $res->date = $this->getPublicationTime()->timestamp(); + $res->type = $this->getVKAPIType(); + $res->is_licensed = (int) $this->isLicensed(); + $res->is_unsafe = (int) $this->isUnsafe(); + $res->folder_id = (int) $this->getFolder(); + $res->private_url = ""; + if($user) { + $res->can_manage = $this->canBeModifiedBy($user); + } + + if($this->hasPreview()) { + $res->preview = $this->toApiPreview(); + } + + return $res; + } +} diff --git a/Web/Models/Repositories/Documents.php b/Web/Models/Repositories/Documents.php new file mode 100644 index 00000000..a03afb2d --- /dev/null +++ b/Web/Models/Repositories/Documents.php @@ -0,0 +1,69 @@ +context = DatabaseConnection::i()->getContext(); + $this->documents = $this->context->table("documents"); + } + + private function toDocument(?ActiveRow $ar): ?Document + { + return is_null($ar) ? NULL : new Document($ar); + } + + function get(int $id): ?Comment + { + return $this->toDocument($this->documents->get($id)); + } + + # 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 + { + $doc = $this->documents->where(['virtual_id' => $virtual_id, 'id' => $real_id]); + + if($access_key) { + $doc->where("access_key", $access_key); + } + + $doc = $doc->fetch(); + if(!is_null($doc)) + return new Document($doc); + else + return NULL; + + } + + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream + { + $result = $this->documents->where("title LIKE ?", "%$query%")->where("deleted", 0); + $order_str = 'id'; + + switch($order['type']) { + case 'id': + $order_str = 'created ' . ($order['invert'] ? 'ASC' : 'DESC'); + break; + } + + foreach($params as $paramName => $paramValue) { + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + } + } + + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Document", $result); + } +} diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php new file mode 100644 index 00000000..da39f48d --- /dev/null +++ b/Web/Presenters/DocumentsPresenter.php @@ -0,0 +1,19 @@ +template->_template = "Documents/List.xml"; + } + + function renderListGroup(?int $gid) + { + $this->renderList($gid); + } +} diff --git a/Web/Presenters/templates/Documents/List.xml b/Web/Presenters/templates/Documents/List.xml new file mode 100644 index 00000000..edad9241 --- /dev/null +++ b/Web/Presenters/templates/Documents/List.xml @@ -0,0 +1,11 @@ +{extends "../@layout.xml"} + +{block title} + +{/block} + +{block header}{/block} + +{block content} + загрузить +{/block} diff --git a/Web/di.yml b/Web/di.yml index 81e06117..b9821fec 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -27,6 +27,7 @@ services: - openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\PollPresenter - openvk\Web\Presenters\BannedLinkPresenter + - openvk\Web\Presenters\DocumentsPresenter - openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Polls @@ -52,5 +53,6 @@ services: - openvk\Web\Models\Repositories\Aliases - openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\ChandlerGroups + - openvk\Web\Models\Repositories\Documents - openvk\Web\Presenters\MaintenancePresenter - openvk\Web\Presenters\NoSpamPresenter diff --git a/Web/routes.yml b/Web/routes.yml index 7fd0cfe7..f7150670 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -311,6 +311,10 @@ routes: handler: "Poll->view" - url: "/poll{num}/voters" handler: "Poll->voters" + - url: "/docs" + handler: "Documents->list" + - url: "/docs{num}" + handler: "Documents->listGroup" - url: "/admin" handler: "Admin->index" - url: "/admin/users" diff --git a/install/sqls/00054-docs.sql b/install/sqls/00054-docs.sql new file mode 100644 index 00000000..69059b4c --- /dev/null +++ b/install/sqls/00054-docs.sql @@ -0,0 +1,28 @@ +CREATE TABLE `documents` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `owner` BIGINT(20) UNSIGNED NOT NULL, + `virtual_id` BIGINT(20) UNSIGNED NOT NULL, + `hash` CHAR(128) NOT NULL, + `owner_hidden` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1', + `copy_of` BIGINT(20) UNSIGNED NULL DEFAULT NULL, + `created` BIGINT(20) UNSIGNED NOT NULL, + `edited` BIGINT(20) UNSIGNED NULL DEFAULT NULL, + `name` VARCHAR(256) NOT NULL, + `original_name` VARCHAR(500) NULL DEFAULT NULL, + `access_key` VARCHAR(100) NULL DEFAULT NULL, + `format` VARCHAR(20) NOT NULL DEFAULT 'gif', + `type` TINYINT(10) UNSIGNED NOT NULL DEFAULT '0', + `folder_id` TINYINT(10) UNSIGNED NOT NULL DEFAULT '0', + `preview` VARCHAR(200) NULL DEFAULT NULL, + `tags` VARCHAR(500) NULL DEFAULT NULL, + `filesize` BIGINT(20) UNSIGNED NOT NULL, + `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `unlisted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) ENGINE = InnoDB COLLATE=utf8mb4_unicode_520_ci; + +ALTER TABLE `documents` ADD INDEX (`deleted`); +ALTER TABLE `documents` ADD INDEX (`unlisted`); +ALTER TABLE `documents` ADD INDEX `virtual_id_id` (`virtual_id`, `id`); +ALTER TABLE `documents` ADD INDEX `folder_id` (`folder_id`); +ALTER TABLE `photos` ADD `system` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `anonymous`, ADD `private` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `system`; 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 02/16] 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} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{_name}:
{_tags}:
{_accessbility}: + +
+ +
{_file}: + +
+
+ + +
+
+{/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: From a91563fdf6990d06e86d85f551fb8ff6afe02e22 Mon Sep 17 00:00:00 2001 From: mrilyew <99399973+mrilyew@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:33:09 +0300 Subject: [PATCH 03/16] ui start --- VKAPI/Handlers/Wall.php | 30 ++++- Web/Models/Entities/Document.php | 2 +- Web/Models/Entities/User.php | 2 + Web/Models/Repositories/Documents.php | 10 +- Web/Presenters/DocumentsPresenter.php | 42 ++++++- Web/Presenters/SearchPresenter.php | 7 +- Web/Presenters/UserPresenter.php | 3 +- Web/Presenters/templates/@layout.xml | 6 + Web/Presenters/templates/Documents/List.xml | 46 ++++++- Web/Presenters/templates/Documents/Upload.xml | 14 +-- .../templates/Documents/components/doc.xml | 37 ++++++ Web/Presenters/templates/User/Settings.xml | 14 ++- Web/static/css/main.css | 117 +++++++++++++++++- Web/static/js/al_docs.js | 1 + bootstrap.php | 16 +++ locales/ru.strings | 24 ++++ 16 files changed, 347 insertions(+), 24 deletions(-) create mode 100644 Web/Presenters/templates/Documents/components/doc.xml create mode 100644 Web/static/js/al_docs.js diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index ab369a6b..125d62a6 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -117,6 +117,11 @@ final class Wall extends VKAPIRequestHandler "type" => "audio", "audio" => $attachment->toVkApiStruct($this->getUser()), ]; + } else if ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -333,6 +338,11 @@ final class Wall extends VKAPIRequestHandler "type" => "audio", "audio" => $attachment->toVkApiStruct($this->getUser()) ]; + } else if ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -577,7 +587,7 @@ final class Wall extends VKAPIRequestHandler if($signed == 1) $flags |= 0b01000000; - $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio']); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio', 'doc']); $final_attachments = []; $should_be_suggested = $owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2; foreach($parsed_attachments as $attachment) { @@ -670,7 +680,7 @@ final class Wall extends VKAPIRequestHandler if(preg_match('/(wall|video|photo)((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0) $this->fail(100, "One of the parameters specified was missing or invalid: object is incorrect"); - $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); $final_attachments = []; foreach($parsed_attachments as $attachment) { if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && @@ -780,6 +790,11 @@ final class Wall extends VKAPIRequestHandler "type" => "audio", "audio" => $attachment->toVkApiStruct($this->getUser()), ]; + } else if ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -867,6 +882,11 @@ final class Wall extends VKAPIRequestHandler "type" => "audio", "audio" => $attachment->toVkApiStruct($this->getUser()), ]; + } else if ($attachment instanceof \openvk\Web\Models\Entities\Document) { + $attachments[] = [ + "type" => "doc", + "doc" => $attachment->toVkApiStruct($this->getUser()), + ]; } } @@ -928,7 +948,7 @@ final class Wall extends VKAPIRequestHandler if($post->getTargetWall() < 0) $club = (new ClubsRepo)->get(abs($post->getTargetWall())); - $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); $final_attachments = []; foreach($parsed_attachments as $attachment) { if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && @@ -1014,7 +1034,7 @@ final class Wall extends VKAPIRequestHandler $this->requireUser(); $this->willExecuteWriteAction(); - $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll']); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll', 'doc']); $final_attachments = []; foreach($parsed_attachments as $attachment) { if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && @@ -1083,7 +1103,7 @@ final class Wall extends VKAPIRequestHandler $this->willExecuteWriteAction(); $comment = (new CommentsRepo)->get($comment_id); - $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio']); + $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'doc']); $final_attachments = []; foreach($parsed_attachments as $attachment) { if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && diff --git a/Web/Models/Entities/Document.php b/Web/Models/Entities/Document.php index 5870548b..4b95ffa2 100644 --- a/Web/Models/Entities/Document.php +++ b/Web/Models/Entities/Document.php @@ -111,7 +111,7 @@ class Document extends Media throw new \ValueError("Invalid filesize"); $hash = hash_file("whirlpool", $file["tmp_name"]); - $this->stateChanges("original_name", $original_name); + $this->stateChanges("original_name", ovk_proc_strtr($original_name, 255)); $this->tmp_format = $file_format; $this->stateChanges("format", $file_format); $this->stateChanges("filesize", $file_size); diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index a1f650ec..7e6483cd 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -512,6 +512,7 @@ class User extends RowModel "links", "poster", "apps", + "docs", ], ])->get($id); } @@ -1117,6 +1118,7 @@ class User extends RowModel "links", "poster", "apps", + "docs", ], ])->set($id, (int) $status)->toInteger(); diff --git a/Web/Models/Repositories/Documents.php b/Web/Models/Repositories/Documents.php index f535418b..2ede28e7 100644 --- a/Web/Models/Repositories/Documents.php +++ b/Web/Models/Repositories/Documents.php @@ -21,7 +21,7 @@ class Documents return is_null($ar) ? NULL : new Document($ar); } - function get(int $id): ?Comment + function get(int $id): ?Document { return $this->toDocument($this->documents->get($id)); } @@ -85,6 +85,14 @@ class Documents ]; } + if(sizeof($response) < 1) { + return [[ + "count" => 0, + "type" => 0, + "name" => tr("document_type_0"), + ]]; + } + return $response; } diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php index fbf4caf4..9c4bab73 100644 --- a/Web/Presenters/DocumentsPresenter.php +++ b/Web/Presenters/DocumentsPresenter.php @@ -1,6 +1,6 @@ template->_template = "Documents/List.xml"; + if($owner_id > 0) + $this->notFound(); + + if($owner_id < 0) { + $owner = (new Clubs)->get(abs($owner_id)); + if(!$owner || $owner->isBanned()) + $this->notFound(); + else + $this->template->group = $owner; + } + + if(!$owner_id) + $owner_id = $this->user->id; + + $current_tab = (int)($this->queryParam("tab") ?? 0); + $current_order = (int)($this->queryParam("order") ?? 0); + $page = (int)($this->queryParam("p") ?? 1); + $order = in_array($current_order, [0,1,2]) ? $current_order : 0; + $tab = in_array($current_tab, [0,1,2,3,4,5,6,7,8]) ? $current_tab : 0; + + $docs = (new Documents)->getDocumentsByOwner($owner_id, (int)$order, (int)$tab); + $this->template->tabs = (new Documents)->getTypes($owner_id); + $this->template->current_tab = $tab; + $this->template->count = $docs->size(); + $this->template->docs = iterator_to_array($docs->page($page, OPENVK_DEFAULT_PER_PAGE)); + $this->template->locale_string = "you_have_x_documents"; + if($owner_id < 0) { + $this->template->locale_string = "group_has_x_documents"; + } elseif($current_tab != 0) { + $this->template->locale_string = "x_documents_in_tab"; + } + + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $page, + "amount" => sizeof($this->template->docs), + "perPage" => OPENVK_DEFAULT_PER_PAGE, + ]; } function renderListGroup(?int $gid) diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index 9e16450e..6ab03415 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,7 @@ videos = new Videos; $this->apps = new Applications; $this->audios = new Audios; + $this->documents = new Documents; parent::__construct(); } @@ -45,7 +47,8 @@ final class SearchPresenter extends OpenVKPresenter "videos" => "videos", "audios" => "audios", "apps" => "apps", - "audios_playlists" => "audios" + "audios_playlists" => "audios", + "docs" => "documents" ]; $parameters = [ "ignore_private" => true, diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index dc8fb02c..df3abb82 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -611,7 +611,8 @@ final class UserPresenter extends OpenVKPresenter "menu_novajoj" => "news", "menu_ligiloj" => "links", "menu_standardo" => "poster", - "menu_aplikoj" => "apps" + "menu_aplikoj" => "apps", + "menu_doxc" => "docs", ]; foreach($settings as $checkbox => $setting) $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 245ccfb0..732c527c 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -198,6 +198,11 @@ {_my_apps} {_my_settings} + {if $thisUser->getLeftMenuItemStatus('docs')} + + {_my_documents} + {/if} + {var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {var $canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} @@ -397,6 +402,7 @@ {script "js/scroll.js"} {script "js/player.js"} {script "js/al_wall.js"} + {script "js/al_docs.js"} {script "js/al_api.js"} {script "js/al_mentions.js"} {script "js/al_polls.js"} diff --git a/Web/Presenters/templates/Documents/List.xml b/Web/Presenters/templates/Documents/List.xml index edad9241..c01406e0 100644 --- a/Web/Presenters/templates/Documents/List.xml +++ b/Web/Presenters/templates/Documents/List.xml @@ -1,11 +1,51 @@ {extends "../@layout.xml"} {block title} - + {if !isset($group)} + {_my_documents_objectively} + {else} + {_documents_of_group} + {/if} {/block} -{block header}{/block} +{block header} + {if !isset($group)} + {_my_documents} + {else} + {$group->getCanonicalName()} » + {_my_documents} + {/if} +{/block} {block content} - загрузить +
+ +
+
+ +
+
+
+
+
{tr($locale_string, $count)}.
+
+
+ {if $count > 0} + {foreach $docs as $doc} + {include "components/doc.xml", doc => $doc} + {/foreach} + {else} + {include "../components/error.xml", description => tr("there_is_no_documents_alright")} + {/if} +
+
+
{/block} diff --git a/Web/Presenters/templates/Documents/Upload.xml b/Web/Presenters/templates/Documents/Upload.xml index fcab2fd0..7c91c6b2 100644 --- a/Web/Presenters/templates/Documents/Upload.xml +++ b/Web/Presenters/templates/Documents/Upload.xml @@ -19,18 +19,18 @@ {block content}
- +
- + - + - + - + - + - + - + + + + + +
{_name}:{_name}:
{_tags}:{_tags}:
{_accessbility}:{_accessbility}:
{_file}:{_file}:
diff --git a/Web/Presenters/templates/Documents/components/doc.xml b/Web/Presenters/templates/Documents/components/doc.xml new file mode 100644 index 00000000..baf1b0b8 --- /dev/null +++ b/Web/Presenters/templates/Documents/components/doc.xml @@ -0,0 +1,37 @@ +{var $preview = $doc->hasPreview() ? $doc->getPreview() : NULL} +{var $tags = $doc->getTags()} +{var $copied = $doc->isCopiedBy($thisUser)} +{var $modifiable = $doc->canBeModifiedBy($thisUser)} + +
+ + {if $preview} + document_preview + {else} +
+ {$doc->getFileExtension()} +
+ {/if} +
+
+ {$doc->getName()} + + +
+
+
+
+
+
+
+
diff --git a/Web/Presenters/templates/User/Settings.xml b/Web/Presenters/templates/User/Settings.xml index e7f4b5ac..5ae6ee4b 100644 --- a/Web/Presenters/templates/User/Settings.xml +++ b/Web/Presenters/templates/User/Settings.xml @@ -684,7 +684,19 @@
{_my_apps}
+ + + {_my_documents} +
div { + width: 20px; + height: 20px; + background-color: gray; +} + +.docListViewItem:hover .doc_volume { + display: flex; +} diff --git a/Web/static/js/al_docs.js b/Web/static/js/al_docs.js new file mode 100644 index 00000000..1aca33cf --- /dev/null +++ b/Web/static/js/al_docs.js @@ -0,0 +1 @@ +u() diff --git a/bootstrap.php b/bootstrap.php index e964af81..601129b3 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -261,6 +261,10 @@ function parseAttachments($attachments, array $allow_types = ['photo', 'video', 'method' => 'get', 'onlyId' => true, ], + 'doc' => [ + 'repo' => 'openvk\Web\Models\Repositories\Documents', + 'method' => 'getDocumentById', + ] ]; foreach($exploded_attachments as $attachment_string) { @@ -371,6 +375,18 @@ function escape_html(string $unsafe): string return htmlspecialchars($unsafe, ENT_DISALLOWED | ENT_XHTML); } +function readable_filesize($bytes, $precision = 2): string +{ + $units = ['B', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb']; + + $bytes = max($bytes, 0); + $power = $bytes > 0 ? floor(log($bytes, 1024)) : 0; + $power = min($power, count($units) - 1); + $bytes /= pow(1024, $power); + + return round($bytes, $precision) . $units[$power]; +} + return (function() { _ovk_check_environment(); require __DIR__ . "/vendor/autoload.php"; diff --git a/locales/ru.strings b/locales/ru.strings index f905fe7e..af30d2ec 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -2206,6 +2206,10 @@ /* Documents */ +"my_documents" = "Документы"; +"my_documents_objectively" = "Мои Документы"; +"documents_of_group" = "Документы группы"; +"search_by_documents" = "Поиск по документам..."; "documents" = "Документы"; "document_uploading_in_general" = "Загрузка документа"; "file" = "Файл"; @@ -2221,3 +2225,23 @@ "document_type_6" = "Видео"; "document_type_7" = "Книги"; "document_type_8" = "Остальные"; + +"you_have_x_documents_one" = "У Вас $1 документ"; +"you_have_x_documents_few" = "У Вас $1 документа"; +"you_have_x_documents_many" = "У Вас $1 документов"; +"you_have_x_documents_other" = "У Вас $1 документов"; +"you_have_x_documents_zero" = "У Вас $1 документов"; + +"group_has_x_documents_one" = "У этой группы $1 документ"; +"group_has_x_documents_few" = "У этой группы $1 документа"; +"group_has_x_documents_many" = "У этой группы $1 документов"; +"group_has_x_documents_other" = "У этой группы $1 документов"; +"group_has_x_documents_zero" = "У этой группы $1 документов"; + +"x_documents_in_tab_one" = "В этой вкладке $1 документ"; +"x_documents_in_tab_few" = "В этой вкладке $1 документа"; +"x_documents_in_tab_many" = "В этой вкладке $1 документов"; +"x_documents_in_tab_other" = "В этой вкладке $1 документов"; +"x_documents_in_tab_zero" = "В этой вкладке $1 документов"; + +"there_is_no_documents_alright" = "Здесь нет документов."; From 36560555b6f89a0e5480c6606a77ed59ce852017 Mon Sep 17 00:00:00 2001 From: mrilyew <99399973+mrilyew@users.noreply.github.com> Date: Sat, 28 Dec 2024 22:54:32 +0300 Subject: [PATCH 04/16] better ui, search, uploading (icons by myslivets) Co-Authored-By: Daniel <60743585+myslivets@users.noreply.github.com> --- VKAPI/Handlers/Docs.php | 5 +- Web/Models/Entities/Club.php | 8 + Web/Models/Entities/Document.php | 35 ++++ Web/Models/Entities/Report.php | 3 +- Web/Models/Repositories/Documents.php | 4 +- Web/Presenters/DocumentsPresenter.php | 15 +- Web/Presenters/ReportPresenter.php | 4 +- Web/Presenters/templates/@layout.xml | 9 +- Web/Presenters/templates/Documents/List.xml | 13 +- .../templates/Documents/components/doc.xml | 10 +- Web/Presenters/templates/Report/Tabs.xml | 3 + .../templates/Report/ViewContent.xml | 2 + Web/Presenters/templates/Search/Index.xml | 26 +++ Web/static/css/main.css | 36 +++- Web/static/img/docs_controls.png | Bin 0 -> 5482 bytes Web/static/js/al_docs.js | 161 +++++++++++++++++- install/sqls/00054-docs.sql | 2 +- locales/ru.strings | 19 +++ 18 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 Web/static/img/docs_controls.png diff --git a/VKAPI/Handlers/Docs.php b/VKAPI/Handlers/Docs.php index 271e64cd..11938ba7 100644 --- a/VKAPI/Handlers/Docs.php +++ b/VKAPI/Handlers/Docs.php @@ -36,6 +36,9 @@ final class Docs extends VKAPIRequestHandler if(!$doc->canBeModifiedBy($this->getUser())) $this->fail(1153, "Access to document is denied"); + + $doc->delete(); + return 1; } @@ -161,7 +164,7 @@ final class Docs extends VKAPIRequestHandler $params["tags"] = $tags; if($search_own === 1) - $params["owner_id"] = $this->getUser()->getId(); + $params["from_me"] = $this->getUser()->getId(); $documents = (new Documents)->find($q, $params, $o_order); $res = (object)[ diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 6121b69d..2e90937f 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -433,6 +433,14 @@ class Club extends RowModel return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); } + function canUploadDocs(?User $user): bool + { + if(!$user) + return false; + + return $this->canBeModifiedBy($user); + } + function getAudiosCollectionSize() { return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); diff --git a/Web/Models/Entities/Document.php b/Web/Models/Entities/Document.php index 4b95ffa2..71588a59 100644 --- a/Web/Models/Entities/Document.php +++ b/Web/Models/Entities/Document.php @@ -151,6 +151,11 @@ class Document extends Media return false; } + function isAnonymous(): bool + { + return false; + } + function isCopiedBy(User $user): bool { if($user->getId() === $this->getOwnerID()) @@ -159,11 +164,20 @@ class Document extends Media return DatabaseConnection::i()->getContext()->table("documents")->where([ "owner" => $user->getId(), "copy_of" => $this->getId(), + "deleted" => 0, ])->count() > 0; } function copy(User $user): Document { + $item = DatabaseConnection::i()->getContext()->table("documents")->where([ + "owner" => $user->getId(), + "copy_of" => $this->getId(), + ]); + if($item->count() > 0) { + $older = new Document($item->fetch()); + } + $this_document_array = $this->getRecord()->toArray(); $new_document = new Document; @@ -186,6 +200,22 @@ class Document extends Media return $new_document; } + function setTags(?string $tags): bool + { + if(!$tags) { + return false; + } + + $parsed = explode(",", $tags); + $result = ""; + foreach($parsed as $tag) { + $result .= mb_trim($tag) . ($tag != end($parsed) ? "," : ''); + } + + $this->stateChanges("tags", $result); + return true; + } + function getFileExtension(): string { if($this->tmp_format) { @@ -200,6 +230,11 @@ class Document extends Media return $this->getVirtualId() . "_" . $this->getId(); } + function getPrettiestId(): string + { + return $this->getVirtualId() . "_" . $this->getId() . "_" . $this->getAccessKey(); + } + function getOriginal(): Document { return $this->getRecord()->copy_of; diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index 33bf84f7..3bb6b083 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\Club; use Chandler\Database\DatabaseConnection; -use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs}; +use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Documents, Users, Posts, Photos, Videos, Clubs}; use Chandler\Database\DatabaseConnection as DB; use Nette\InvalidStateException as ISE; use Nette\Database\Table\Selection; @@ -75,6 +75,7 @@ class Report extends RowModel else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId()); + else if ($this->getContentType() == "doc") return (new Documents)->get($this->getContentId()); else return null; } diff --git a/Web/Models/Repositories/Documents.php b/Web/Models/Repositories/Documents.php index 2ede28e7..3482924b 100644 --- a/Web/Models/Repositories/Documents.php +++ b/Web/Models/Repositories/Documents.php @@ -75,7 +75,7 @@ class Documents $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; + if($res->count < 1 || $res->type == 0) continue; $name = tr("document_type_".$res->type); $response[] = [ @@ -118,7 +118,7 @@ class Documents case "tags": $result->where("tags", $paramValue); break; - case "owner_id": + case "from_me": $result->where("owner", $paramValue); break; } diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php index 9c4bab73..29eae0ce 100644 --- a/Web/Presenters/DocumentsPresenter.php +++ b/Web/Presenters/DocumentsPresenter.php @@ -34,6 +34,7 @@ final class DocumentsPresenter extends OpenVKPresenter $docs = (new Documents)->getDocumentsByOwner($owner_id, (int)$order, (int)$tab); $this->template->tabs = (new Documents)->getTypes($owner_id); $this->template->current_tab = $tab; + $this->template->order = $order; $this->template->count = $docs->size(); $this->template->docs = iterator_to_array($docs->page($page, OPENVK_DEFAULT_PER_PAGE)); $this->template->locale_string = "you_have_x_documents"; @@ -42,7 +43,8 @@ final class DocumentsPresenter extends OpenVKPresenter } elseif($current_tab != 0) { $this->template->locale_string = "x_documents_in_tab"; } - + + $this->template->canUpload = $owner_id == $this->user->id || $this->template->group->canBeModifiedBy($this->user->identity); $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => $page, @@ -68,7 +70,7 @@ final class DocumentsPresenter extends OpenVKPresenter if(!is_null($this->queryParam("gid"))) { $gid = (int) $this->queryParam("gid"); $group = (new Clubs)->get($gid); - if(!$group) + if(!$group || $group->isBanned()) $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); if(!$group->canUploadDocs($this->user->identity)) @@ -105,5 +107,14 @@ final class DocumentsPresenter extends OpenVKPresenter ]); $document->save(); + + if(!$isAjax) { + $this->redirect("/docs" . (isset($group) ? $group->getRealId() : "")); + } else { + $this->returnJson([ + "success" => true, + "redirect" => "/docs" . (isset($group) ? $group->getRealId() : ""), + ]); + } } } diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index dfd2b962..ae9a6e75 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter if ($_SERVER["REQUEST_METHOD"] === "POST") $this->assertNoCSRF(); - $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL; + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"]) ? $this->queryParam("act") : NULL; if (!$this->queryParam("orig")) { $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); @@ -93,7 +93,7 @@ final class ReportPresenter extends OpenVKPresenter if ($this->queryParam("type") === "user" && $id === $this->user->id) exit(json_encode([ "error" => "You can't report yourself" ])); - if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { $report = new Report; $report->setUser_id($this->user->id); diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 732c527c..69afe7c9 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -142,6 +142,7 @@ +