diff --git a/VKAPI/Handlers/Docs.php b/VKAPI/Handlers/Docs.php new file mode 100644 index 00000000..ae05bddf --- /dev/null +++ b/VKAPI/Handlers/Docs.php @@ -0,0 +1,215 @@ +requireUser(); + $this->willExecuteWriteAction(); + + $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)->getDocumentByIdUnsafe($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"); + + $doc->delete(); + + return 1; + } + + function restore(int $owner_id, int $doc_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + return $this->add($owner_id, $doc_id, ""); + } + + function edit(int $owner_id, int $doc_id, ?string $title = "", ?string $tags = "", ?int $folder_id = 0, int $owner_hidden = -1): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $doc = (new Documents)->getDocumentByIdUnsafe($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); + + $doc->setTags($tags); + if(in_array($folder_id, [0, 3])) + $doc->setFolder_id($folder_id); + if(in_array($owner_hidden, [0, 1])) + $doc->setOwner_hidden($owner_hidden); + + try { + $doc->setEdited(time()); + $doc->save(); + } catch(\Throwable $e) { + return 0; + } + + return 1; + } + + 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(); + + $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"); + } + + foreach($item_ids as $id) { + $splitted_id = explode("_", $id); + $doc = (new Documents)->getDocumentById((int)$splitted_id[0], (int)$splitted_id[1], $splitted_id[2]); + if(!$doc || $doc->isDeleted()) + 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, + ]; + } + + function getTags(?int $owner_id, ?int $type = 0) + { + $this->requireUser(); + if(!$owner_id) + $owner_id = $this->getUser()->getId(); + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) + $this->fail(15, "Access denied"); + + $tags = (new Documents)->getTags($owner_id, $type); + return $tags; + } + + 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["from_me"] = $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) + { + $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; + } +} 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/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 new file mode 100644 index 00000000..54d647d3 --- /dev/null +++ b/Web/Models/Entities/Document.php @@ -0,0 +1,413 @@ +getBaseDir() . substr($hash, 0, 2); + if(!is_dir($dir)) + mkdir($dir); + + return "$dir/$hash." . $this->getFileExtension(); + } + + 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; + } + } + + 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 = end(explode(".", $original_name)); + $file_size = $file["size"]; + $type = Document::detectTypeByFormat($file_format); + + if(!$file_format) + throw new \TypeError("No file format"); + + if(!in_array(mb_strtolower($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", ovk_proc_strtr($original_name, 255)); + $this->tmp_format = mb_strtolower($file_format); + $this->stateChanges("format", mb_strtolower($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; + } + + 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 isAnonymous(): bool + { + return false; + } + + function isPrivate(): bool + { + return $this->getFolder() == Document::VKAPI_FOLDER_PRIVATE; + } + + function isImage(): bool + { + return in_array($this->getVKAPIType(), [3, 4]); + } + + function isGif(): bool + { + return $this->getVKAPIType() == 3; + } + + function isCopiedBy($user = NULL): bool + { + if(!$user) + return false; + + if($user->getRealId() === $this->getOwnerID()) + return true; + + return DatabaseConnection::i()->getContext()->table("documents")->where([ + "owner" => $user->getRealId(), + "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; + $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 setTags(?string $tags): bool + { + if(is_null($tags)) { + $this->stateChanges("tags", NULL); + return true; + } + + $parsed = explode(",", $tags); + if(sizeof($parsed) < 1 || $parsed[0] == "") { + $this->stateChanges("tags", NULL); + return true; + } + + $result = ""; + foreach($parsed as $tag) { + $result .= trim($tag) . ($tag != end($parsed) ? "," : ''); + } + + $this->stateChanges("tags", ovk_proc_strtr($result, 500)); + return true; + } + + function getOwner(bool $real = false): RowModel + { + $oid = (int) $this->getRecord()->owner; + if($oid > 0) + return (new Users)->get($oid); + else + return (new Clubs)->get($oid * -1); + } + + function getFileExtension(): string + { + if($this->tmp_format) { + return $this->tmp_format; + } + + return $this->getRecord()->format; + } + + function getPrettyId(): string + { + return $this->getVirtualId() . "_" . $this->getId(); + } + + function getPrettiestId(): string + { + return $this->getVirtualId() . "_" . $this->getId() . "_" . $this->getAccessKey(); + } + + 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 + { + $tags = $this->getRecord()->tags; + if(!$tags) + return []; + + return explode(",", $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, bool $return_tags = false): 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_hidden = (int) $this->isOwnerHidden(); + $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) + $res->can_manage = $this->canBeModifiedBy($user); + + if($this->hasPreview()) + $res->preview = $this->toApiPreview(); + + if($return_tags) + $res->tags = $this->getTags(); + + return $res; + } + + function delete(bool $softly = true, bool $all_copies = false): void + { + if($all_copies) { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("documents")->where("copy_of", $this->getId())->delete(); + } + parent::delete($softly); + } + + static function detectTypeByFormat(string $format) + { + switch(mb_strtolower($format)) { + case "txt": case "docx": case "doc": case "odt": case "pptx": case "ppt": case "xlsx": case "xls": case "md": + 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": case "webp": + 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/Photo.php b/Web/Models/Entities/Photo.php index 90c532b2..bd0e65cc 100644 --- a/Web/Models/Entities/Photo.php +++ b/Web/Models/Entities/Photo.php @@ -97,16 +97,27 @@ class Photo extends Media protected function saveFile(string $filename, string $hash): bool { - $image = new \Imagick; - $image->readImage($filename); - $h = $image->getImageHeight(); - $w = $image->getImageWidth(); + $input_image = new \Imagick; + $input_image->readImage($filename); + $h = $input_image->getImageHeight(); + $w = $input_image->getImageWidth(); if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) throw new ISE("Invalid layout: image is too wide/short"); + # gif fix 10.01.2025 + if($input_image->getImageFormat() === 'GIF') + $input_image->setIteratorIndex(0); + + # png workaround (transparency to white) + $image = new \Imagick(); + $bg = new \ImagickPixel('white'); + $image->newImage($w, $h, $bg); + $image->compositeImage($input_image, \Imagick::COMPOSITE_OVER, 0, 0); + $sizes = Image::calculateSize( $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT ); + $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); $image->writeImage($this->pathFromHash($hash)); $this->saveImageResizedCopies($image, $filename, $hash); 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/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/Entities/User.php b/Web/Models/Entities/User.php index 0356e6ce..088abb24 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 new file mode 100644 index 00000000..da388cc0 --- /dev/null +++ b/Web/Models/Repositories/Documents.php @@ -0,0 +1,161 @@ +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): ?Document + { + 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): ?Document + { + $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 NULL; + + $n_doc = new Document($doc); + if(!$n_doc->checkAccessKey($access_key)) + return NULL; + + return $n_doc; + } + + function getDocumentByIdUnsafe(int $virtual_id, int $real_id): ?Document + { + $doc = $this->documents->where(['virtual_id' => $virtual_id, 'id' => $real_id]); + + $doc = $doc->fetch(); + if(is_null($doc)) + return NULL; + + $n_doc = new Document($doc); + + return $n_doc; + } + + 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` = ? AND `deleted` = 0 AND `unlisted` = 0 GROUP BY `type` ORDER BY `type`", $owner_id); + $response = []; + foreach($result as $res) { + if($res->count < 1 || $res->type == 0) continue; + + $name = tr("document_type_".$res->type); + $response[] = [ + "count" => $res->count, + "type" => $res->type, + "name" => $name, + ]; + } + + return $response; + } + + function getTags(int $owner_id, ?int $type = 0): array + { + $query = "SELECT `tags` FROM `documents` WHERE `owner` = ? AND `deleted` = 0 AND `unlisted` = 0 "; + if($type > 0 && $type < 9) { + $query .= "AND `type` = $type"; + } + + $query .= " AND `tags` IS NOT NULL ORDER BY `id`"; + $result = DatabaseConnection::i()->getConnection()->query($query, $owner_id); + $tags = []; + foreach($result as $res) { + $tags[] = $res->tags; + } + $imploded_tags = implode(",", $tags); + $exploded_tags = array_values(array_unique(explode(",", $imploded_tags))); + if($exploded_tags[0] == "") + return []; + + return array_slice($exploded_tags, 0, 50); + } + + function find(string $query, array $params = [], array $order = ['type' => 'id', 'invert' => false]): Util\EntityStream + { + $result = $this->documents->where("name LIKE ?", "%$query%")->where([ + "deleted" => 0, + "folder_id != " => 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 "type": + if($paramValue < 1 || $paramValue > 8) continue; + $result->where("type", $paramValue); + break; + case "tags": + $result->where("tags LIKE ?", "%$paramValue%"); + break; + case "from_me": + $result->where("owner", $paramValue); + break; + } + } + + if($order_str) + $result->order($order_str); + + return new Util\EntityStream("Document", $result); + } +} 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/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index 7ab74ac0..76279ad6 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -88,7 +88,7 @@ final class CommentPresenter extends OpenVKPresenter if(!empty($this->postParam("vertical_attachments"))) { $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); if(sizeof($vertical_attachments_array) > 0) { - $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']); } } diff --git a/Web/Presenters/DocumentsPresenter.php b/Web/Presenters/DocumentsPresenter.php new file mode 100644 index 00000000..92dd73b0 --- /dev/null +++ b/Web/Presenters/DocumentsPresenter.php @@ -0,0 +1,179 @@ +assertUserLoggedIn(); + + $this->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; + + $api_request = $this->queryParam("picker") == "1"; + if($api_request && $_SERVER["REQUEST_METHOD"] === "POST") { + $ctx_type = $this->postParam("context"); + $docs = NULL; + + switch($ctx_type) { + default: + case "list": + $docs = (new Documents)->getDocumentsByOwner($owner_id, (int)$order, (int)$tab); + break; + case "search": + $ctx_query = $this->postParam("ctx_query"); + $docs = (new Documents)->find($ctx_query); + break; + } + + $this->template->docs = $docs->page($page, OPENVK_DEFAULT_PER_PAGE); + $this->template->page = $page; + $this->template->count = $docs->size(); + $this->template->pagesCount = ceil($this->template->count / OPENVK_DEFAULT_PER_PAGE); + $this->template->_template = "Documents/ApiGetContext.xml"; + return; + } + + $docs = (new Documents)->getDocumentsByOwner($owner_id, (int)$order, (int)$tab); + $this->template->tabs = (new Documents)->getTypes($owner_id); + $this->template->tags = (new Documents)->getTags($owner_id, (int)$tab); + $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"; + 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->canUpload = $owner_id == $this->user->id || $this->template->group->canBeModifiedBy($this->user->identity); + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $page, + "amount" => sizeof($this->template->docs), + "perPage" => OPENVK_DEFAULT_PER_PAGE, + ]; + } + + function renderListGroup(?int $gid) + { + $this->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 || $group->isBanned()) + $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"; + + try { + $document = new Document; + $document->setOwner($owner); + $document->setName(ovk_proc_strtr($name, 255)); + $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(); + } catch(\TypeError $e) { + $this->flashFail("err", tr("forbidden"), $e->getMessage(), null, $isAjax); + } catch(ISE $e) { + $this->flashFail("err", tr("forbidden"), tr("error_file_preview"), null, $isAjax); + } catch(\ValueError $e) { + $this->flashFail("err", tr("forbidden"), $e->getMessage(), null, $isAjax); + } catch(\ImagickException $e) { + $this->flashFail("err", tr("forbidden"), tr("error_file_preview"), null, $isAjax); + } + + if(!$isAjax) { + $this->redirect("/docs" . (isset($group) ? $group->getRealId() : "")); + } else { + $this->returnJson([ + "success" => true, + "redirect" => "/docs" . (isset($group) ? $group->getRealId() : ""), + ]); + } + } + + function renderPage(int $virtual_id, int $real_id): void + { + $this->assertUserLoggedIn(); + + $access_key = $this->queryParam("key"); + $doc = (new Documents)->getDocumentById((int)$virtual_id, (int)$real_id, $access_key); + if(!$doc || $doc->isDeleted()) + $this->notFound(); + + if(!$doc->checkAccessKey($access_key)) + $this->notFound(); + + $this->template->doc = $doc; + $this->template->type = $doc->getVKAPIType(); + $this->template->is_image = $doc->isImage(); + $this->template->tags = $doc->getTags(); + $this->template->copied = $doc->isCopiedBy($this->user->identity); + $this->template->copyImportance = true; + $this->template->modifiable = $doc->canBeModifiedBy($this->user->identity); + } +} diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index fd0e9a3d..cde025b5 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios, Posts, Documents}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -27,12 +27,15 @@ final class GroupPresenter extends OpenVKPresenter if ($club->isBanned()) { $this->template->_template = "Group/Banned.xml"; } else { + $docs = (new Documents)->getDocumentsByOwner($club->getRealId()); $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); + $this->template->docsCount = $docs->size(); + $this->template->docs = $docs->offsetLimit(0, 2); } if(!is_null($this->user->identity) && $club->getWallType() == 2) { 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/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 3e211062..9d37458f 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/WallPresenter.php b/Web/Presenters/WallPresenter.php index 12067ad0..3a7605bb 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -294,7 +294,7 @@ final class WallPresenter extends OpenVKPresenter if(!empty($this->postParam("vertical_attachments"))) { $vertical_attachments_array = array_slice(explode(",", $this->postParam("vertical_attachments")), 0, OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["maxAttachments"]); if(sizeof($vertical_attachments_array) > 0) { - $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); + $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']); } } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 8b5a477b..7a029378 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -142,6 +142,7 @@ +