add upload, previews, most of api methods

This commit is contained in:
mrilyew 2024-12-28 13:35:55 +03:00
parent de82eb8792
commit ab5bb6b459
10 changed files with 505 additions and 40 deletions

View file

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

View file

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

View file

@ -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
{

View file

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

View file

@ -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);

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\Documents;
use openvk\Web\Models\Entities\Document;
final class DocumentsPresenter extends OpenVKPresenter
{
@ -16,4 +17,55 @@ final class DocumentsPresenter extends OpenVKPresenter
{
$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)
$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();
}
}

View file

@ -0,0 +1,69 @@
{extends "../@layout.xml"}
{block title}{_document_uploading_in_general}{/block}
{block header}
{if !is_null($group)}
<a href="{$group->getURL()}">{$group->getCanonicalName()}</a>
»
<a href="/docs-{$group->getId()}">{_documents}</a>
{else}
<a href="{$thisUser->getURL()}">{$thisUser->getCanonicalName()}</a>
»
<a href="/docs">{_documents}</a>
{/if}
»
загрузка
{/block}
{block content}
<form method="post" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top"><span class="nobold">{_name}:</span></td>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_tags}:</span></td>
<td><textarea name="tags"></textarea></td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_accessbility}:</span></td>
<td>
<select name="folder">
<option value="0">Private file</option>
<option value="4">Public file</option>
</select>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<label>
<input type="checkbox" name="owner_hidden">
Owner is hidden
</label>
</td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_file}:</span></td>
<td>
<label class="button" style="">{_browse}
<input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" />
</label>
<div id="filename" style="margin-top: 10px;"></div>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="button" name="submit" value="{_upload_button}" />
</td>
</tr>
</tbody>
</table>
</form>
{/block}

View file

@ -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"

View file

@ -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" = "Остальные";

View file

@ -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: