feat(documents) (#1205)

* create document entity

* add upload, previews, most of api methods

* ui start

* better ui, search, uploading (icons by myslivets)

Co-Authored-By: Daniel <60743585+myslivets@users.noreply.github.com>

* add editing functions

* add viewer and gallery

* preparations for picker

* things

* add counter on tab

* add tags

* fix gif processing

* fix png processing

* picker

* addd search

* add fast uploader

* openvk midn. support, change midn.photomodal color

* fix low register format chekc

* add gif play on click

* unauthorized limitations

---------

Co-authored-by: Daniel <60743585+myslivets@users.noreply.github.com>
This commit is contained in:
mrilyew 2025-01-22 17:05:28 +03:00 committed by GitHub
parent 504801520b
commit 2d83003b6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 2550 additions and 37 deletions

215
VKAPI/Handlers/Docs.php Normal file
View file

@ -0,0 +1,215 @@
<?php declare(strict_types=1);
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): string
{
$this->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;
}
}

View file

@ -117,6 +117,11 @@ final class Wall extends VKAPIRequestHandler
"type" => "audio", "type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()), "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) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -333,6 +338,11 @@ final class Wall extends VKAPIRequestHandler
"type" => "audio", "type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()) "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) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -577,7 +587,7 @@ final class Wall extends VKAPIRequestHandler
if($signed == 1) if($signed == 1)
$flags |= 0b01000000; $flags |= 0b01000000;
$parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio']); $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'poll', 'audio', 'doc']);
$final_attachments = []; $final_attachments = [];
$should_be_suggested = $owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2; $should_be_suggested = $owner_id < 0 && !$wallOwner->canBeModifiedBy($this->getUser()) && $wallOwner->getWallType() == 2;
foreach($parsed_attachments as $attachment) { 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) 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"); $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 = []; $final_attachments = [];
foreach($parsed_attachments as $attachment) { foreach($parsed_attachments as $attachment) {
if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) &&
@ -780,6 +790,11 @@ final class Wall extends VKAPIRequestHandler
"type" => "audio", "type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()), "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", "type" => "audio",
"audio" => $attachment->toVkApiStruct($this->getUser()), "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) if($post->getTargetWall() < 0)
$club = (new ClubsRepo)->get(abs($post->getTargetWall())); $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 = []; $final_attachments = [];
foreach($parsed_attachments as $attachment) { foreach($parsed_attachments as $attachment) {
if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) &&
@ -1014,7 +1034,7 @@ final class Wall extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll']); $parsed_attachments = parseAttachments($attachments, ['photo', 'video', 'note', 'audio', 'poll', 'doc']);
$final_attachments = []; $final_attachments = [];
foreach($parsed_attachments as $attachment) { foreach($parsed_attachments as $attachment) {
if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) &&
@ -1083,7 +1103,7 @@ final class Wall extends VKAPIRequestHandler
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id); $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 = []; $final_attachments = [];
foreach($parsed_attachments as $attachment) { foreach($parsed_attachments as $attachment) {
if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) && if($attachment && !$attachment->isDeleted() && $attachment->canBeViewedBy($this->getUser()) &&

View file

@ -433,6 +433,14 @@ class Club extends RowModel
return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user);
} }
function canUploadDocs(?User $user): bool
{
if(!$user)
return false;
return $this->canBeModifiedBy($user);
}
function getAudiosCollectionSize() function getAudiosCollectionSize()
{ {
return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this);

View file

@ -0,0 +1,413 @@
<?php declare(strict_types=1);
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;
const VKAPI_TYPE_GIF = 3;
const VKAPI_TYPE_IMAGE = 4;
const VKAPI_TYPE_AUDIO = 5;
const VKAPI_TYPE_VIDEO = 6;
const VKAPI_TYPE_BOOKS = 7;
const VKAPI_TYPE_UNKNOWN = 8;
const VKAPI_FOLDER_PRIVATE = 0;
const VKAPI_FOLDER_STUDY = 1;
const VKAPI_FOLDER_BOOK = 2;
const VKAPI_FOLDER_PUBLIC = 3;
protected function pathFromHash(string $hash): string
{
$dir = $this->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;
}
}
}

View file

@ -97,16 +97,27 @@ class Photo extends Media
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
$image = new \Imagick; $input_image = new \Imagick;
$image->readImage($filename); $input_image->readImage($filename);
$h = $image->getImageHeight(); $h = $input_image->getImageHeight();
$w = $image->getImageWidth(); $w = $input_image->getImageWidth();
if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER))) if(($h >= ($w * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($w >= ($h * Photo::ALLOWED_SIDE_MULTIPLIER)))
throw new ISE("Invalid layout: image is too wide/short"); 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( $sizes = Image::calculateSize(
$image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT $image->getImageWidth(), $image->getImageHeight(), 8192, 4320, Image::SHRINK_ONLY | Image::FIT
); );
$image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1); $image->resizeImage($sizes[0], $sizes[1], \Imagick::FILTER_HERMITE, 1);
$image->writeImage($this->pathFromHash($hash)); $image->writeImage($this->pathFromHash($hash));
$this->saveImageResizedCopies($image, $filename, $hash); $this->saveImageResizedCopies($image, $filename, $hash);

View file

@ -106,6 +106,25 @@ abstract class Postable extends Attachable
yield $user; 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 function isAnonymous(): bool
{ {

View file

@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use Chandler\Database\DatabaseConnection; 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 Chandler\Database\DatabaseConnection as DB;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
use Nette\Database\Table\Selection; 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() == "app") return (new Applications)->get($this->getContentId());
else if ($this->getContentType() == "user") return (new Users)->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() == "audio") return (new Audios)->get($this->getContentId());
else if ($this->getContentType() == "doc") return (new Documents)->get($this->getContentId());
else return null; else return null;
} }

View file

@ -512,6 +512,7 @@ class User extends RowModel
"links", "links",
"poster", "poster",
"apps", "apps",
"docs",
], ],
])->get($id); ])->get($id);
} }
@ -1117,6 +1118,7 @@ class User extends RowModel
"links", "links",
"poster", "poster",
"apps", "apps",
"docs",
], ],
])->set($id, (int) $status)->toInteger(); ])->set($id, (int) $status)->toInteger();

View file

@ -0,0 +1,161 @@
<?php declare(strict_types=1);
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
{
private $context;
private $documents;
function __construct()
{
$this->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);
}
}

View file

@ -27,6 +27,8 @@ class Photos
$photo = $this->photos->where([ $photo = $this->photos->where([
"owner" => $owner, "owner" => $owner,
"virtual_id" => $vId, "virtual_id" => $vId,
"system" => 0,
"private" => 0,
])->fetch(); ])->fetch();
if(!$photo) return NULL; if(!$photo) return NULL;
@ -37,8 +39,10 @@ class Photos
{ {
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$photos = $this->photos->where([ $photos = $this->photos->where([
"owner" => $user->getId(), "owner" => $user->getId(),
"deleted" => 0 "deleted" => 0,
"system" => 0,
"private" => 0,
])->order("id DESC"); ])->order("id DESC");
foreach($photos->limit($limit, $offset) as $photo) { foreach($photos->limit($limit, $offset) as $photo) {
@ -49,8 +53,10 @@ class Photos
function getUserPhotosCount(User $user) function getUserPhotosCount(User $user)
{ {
$photos = $this->photos->where([ $photos = $this->photos->where([
"owner" => $user->getId(), "owner" => $user->getId(),
"deleted" => 0 "deleted" => 0,
"system" => 0,
"private" => 0,
]); ]);
return sizeof($photos); return sizeof($photos);

View file

@ -88,7 +88,7 @@ final class CommentPresenter extends OpenVKPresenter
if(!empty($this->postParam("vertical_attachments"))) { 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"]); $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) { if(sizeof($vertical_attachments_array) > 0) {
$vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']);
} }
} }

View file

@ -0,0 +1,179 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\{Documents, Clubs};
use openvk\Web\Models\Entities\Document;
use Nette\InvalidStateException as ISE;
final class DocumentsPresenter extends OpenVKPresenter
{
protected $presenterName = "documents";
protected $silent = true;
function renderList(?int $owner_id = NULL): void
{
$this->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);
}
}

View file

@ -3,7 +3,7 @@ namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Club, Photo, Post}; use openvk\Web\Models\Entities\{Club, Photo, Post};
use Nette\InvalidStateException; use Nette\InvalidStateException;
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; 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; use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter final class GroupPresenter extends OpenVKPresenter
@ -27,12 +27,15 @@ final class GroupPresenter extends OpenVKPresenter
if ($club->isBanned()) { if ($club->isBanned()) {
$this->template->_template = "Group/Banned.xml"; $this->template->_template = "Group/Banned.xml";
} else { } else {
$docs = (new Documents)->getDocumentsByOwner($club->getRealId());
$this->template->albums = (new Albums)->getClubAlbums($club, 1, 3); $this->template->albums = (new Albums)->getClubAlbums($club, 1, 3);
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
$this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topics = (new Topics)->getLastTopics($club, 3);
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
$this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId());
$this->template->audiosCount = (new Audios)->getClubCollectionSize($club); $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) { if(!is_null($this->user->identity) && $club->getWallType() == 2) {

View file

@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter
if ($_SERVER["REQUEST_METHOD"] === "POST") if ($_SERVER["REQUEST_METHOD"] === "POST")
$this->assertNoCSRF(); $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")) { if (!$this->queryParam("orig")) {
$this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); $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) if ($this->queryParam("type") === "user" && $id === $this->user->id)
exit(json_encode([ "error" => "You can't report yourself" ])); 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) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) {
$report = new Report; $report = new Report;
$report->setUser_id($this->user->id); $report->setUser_id($this->user->id);

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{User, Club}; use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Videos, Applications, Audios}; use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Videos, Applications, Audios, Documents};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
final class SearchPresenter extends OpenVKPresenter final class SearchPresenter extends OpenVKPresenter
@ -12,6 +12,7 @@ final class SearchPresenter extends OpenVKPresenter
private $videos; private $videos;
private $apps; private $apps;
private $audios; private $audios;
private $documents;
function __construct() function __construct()
{ {
@ -21,6 +22,7 @@ final class SearchPresenter extends OpenVKPresenter
$this->videos = new Videos; $this->videos = new Videos;
$this->apps = new Applications; $this->apps = new Applications;
$this->audios = new Audios; $this->audios = new Audios;
$this->documents = new Documents;
parent::__construct(); parent::__construct();
} }
@ -45,7 +47,8 @@ final class SearchPresenter extends OpenVKPresenter
"videos" => "videos", "videos" => "videos",
"audios" => "audios", "audios" => "audios",
"apps" => "apps", "apps" => "apps",
"audios_playlists" => "audios" "audios_playlists" => "audios",
"docs" => "documents"
]; ];
$parameters = [ $parameters = [
"ignore_private" => true, "ignore_private" => true,

View file

@ -611,7 +611,8 @@ final class UserPresenter extends OpenVKPresenter
"menu_novajoj" => "news", "menu_novajoj" => "news",
"menu_ligiloj" => "links", "menu_ligiloj" => "links",
"menu_standardo" => "poster", "menu_standardo" => "poster",
"menu_aplikoj" => "apps" "menu_aplikoj" => "apps",
"menu_doxc" => "docs",
]; ];
foreach($settings as $checkbox => $setting) foreach($settings as $checkbox => $setting)
$user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox)); $user->setLeftMenuItemStatus($setting, $this->checkbox($checkbox));

View file

@ -294,7 +294,7 @@ final class WallPresenter extends OpenVKPresenter
if(!empty($this->postParam("vertical_attachments"))) { 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"]); $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) { if(sizeof($vertical_attachments_array) > 0) {
$vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note']); $vertical_attachments = parseAttachments($vertical_attachments_array, ['audio', 'note', 'doc']);
} }
} }

View file

@ -142,6 +142,7 @@
<option n:attr="selected => $_REQUEST['section'] == 'apps'" value="apps">{_s_by_apps}</option> <option n:attr="selected => $_REQUEST['section'] == 'apps'" value="apps">{_s_by_apps}</option>
<option n:attr="selected => $_REQUEST['section'] == 'audios'" value="audios">{_s_by_audios}</option> <option n:attr="selected => $_REQUEST['section'] == 'audios'" value="audios">{_s_by_audios}</option>
<option n:attr="selected => $_REQUEST['section'] == 'audios_playlists'" value="audios_playlists">{_s_by_audios_playlists}</option> <option n:attr="selected => $_REQUEST['section'] == 'audios_playlists'" value="audios_playlists">{_s_by_audios_playlists}</option>
<option n:attr="selected => $_REQUEST['section'] == 'docs'" value="docs">{_s_by_documents}</option>
</select> </select>
</div> </div>
<button class="search_box_button"> <button class="search_box_button">
@ -195,9 +196,14 @@
(<b>{$thisUser->getNotificationsCount()}</b>) (<b>{$thisUser->getNotificationsCount()}</b>)
</object> </object>
</a> </a>
<a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_my_apps}</a>
<a href="/settings" class="link">{_my_settings}</a> <a href="/settings" class="link">{_my_settings}</a>
{if $thisUser->getLeftMenuItemStatus('docs') || $thisUser->getLeftMenuItemStatus('apps')}
<div class="menu_divider"></div>
<a n:if="$thisUser->getLeftMenuItemStatus('apps')" href="/apps?act=installed" class="link">{_apps}</a>
<a n:if="$thisUser->getLeftMenuItemStatus('docs')" href="/docs" class="link">{_my_documents}</a>
{/if}
{var $canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {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 $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')} {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')}
@ -397,6 +403,7 @@
{script "js/scroll.js"} {script "js/scroll.js"}
{script "js/player.js"} {script "js/player.js"}
{script "js/al_wall.js"} {script "js/al_wall.js"}
{script "js/al_docs.js"}
{script "js/al_api.js"} {script "js/al_api.js"}
{script "js/al_mentions.js"} {script "js/al_mentions.js"}
{script "js/al_polls.js"} {script "js/al_polls.js"}
@ -461,6 +468,8 @@
"current_id": {$thisUser ? $thisUser->getId() : 0}, "current_id": {$thisUser ? $thisUser->getId() : 0},
"disable_ajax": {$disable_ajax ? $disable_ajax : 0}, "disable_ajax": {$disable_ajax ? $disable_ajax : 0},
"max_add_fields": {ovkGetQuirk("users.max-fields")}, "max_add_fields": {ovkGetQuirk("users.max-fields")},
"docs_max": {\OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["maxSize"]},
"docs_allowed": {\OPENVK_ROOT_CONF["openvk"]["preferences"]["docs"]["allowedFormats"]},
} }
</script> </script>

View file

@ -0,0 +1,9 @@
<input type="hidden" name="count" value="{$count}">
<input type="hidden" name="pagesCount" value="{$pagesCount}">
<input type="hidden" name="page" value="{$page}">
{foreach $docs as $doc}
<div class='display_flex_row _content' data-attachmentdata="{$doc->getVirtualId()}_{$doc->getId()}_{$doc->getAccessKey()}" data-name='{$doc->getName()}'>
{include "components/doc.xml", doc => $doc, hideButtons => true}
</div>
{/foreach}

View file

@ -0,0 +1,80 @@
{extends "../@layout.xml"}
{block title}
{if !isset($group)}
{_my_documents_objectively}
{else}
{_documents_of_group}
{/if}
{/block}
{block header}
{if !isset($group)}
{_my_documents}
{else}
<a href="{$group->getURL()}">{$group->getCanonicalName()}</a> »
{_my_documents}
{/if}
{/block}
{block content}
{var $is_gallery = $current_tab == 3 || $current_tab == 4}
<div id="docs_page_wrapper">
<div class="docs_page_search">
<form action="/search" method="get">
<input type="hidden" name="section" value="docs">
<input type="search" name="q" class="input_with_search_icon" placeholder="{_search_by_documents}">
</form>
<input n:if="$canUpload" id="upload_entry_point" class="button" type="button" value="{_upload_button}" {if isset($group)}data-gid="{$group->getId()}"{/if}>
</div>
<div n:if="sizeof($tabs) > 1" class="docs_page_tabs">
<div class="mb_tabs">
<div class="mb_tab" n:attr="id => $current_tab == 0 ? active">
<a href="?tab=0">{_document_type_0}</a>
</div>
<div n:foreach="$tabs as $tab" class="mb_tab" n:attr="id => $tab['type'] == $current_tab ? active">
<a href="?tab={$tab['type']}">
{$tab["name"]}
<span n:if="$tab['count'] > 1" class="special_counter">{$tab["count"]}</span>
</a>
</div>
</div>
</div>
<div n:class="docs_page_content, $is_gallery ? docs_page_gallery">
<div class="summaryBar display_flex_row display_flex_space_between">
<div class="summary">{tr($locale_string, $count)}.</div>
<select n:if="$count > 3" name="docs_sort">
<option n:attr="selected => $order == 0" value="0">{_documents_sort_add}</option>
<option n:attr="selected => $order == 1" value="1">{_documents_sort_alphabet}</option>
<option n:attr="selected => $order == 2" value="2">{_documents_sort_size}</option>
</select>
</div>
<div n:attr="id => !$is_gallery && sizeof($tags) > 0 ? search_page">
<div n:class="container_white, scroll_container, !$is_gallery && sizeof($tags) > 0 ? page_wrap_content_main">
{if $count > 0}
{foreach $docs as $doc}
{if $is_gallery}
{include "components/image.xml", doc => $doc, scroll_context => true, club => isset($group) ? $group : NULL}
{else}
{include "components/doc.xml", doc => $doc, scroll_context => true, club => isset($group) ? $group : NULL}
{/if}
{/foreach}
{else}
{include "../components/error.xml", description => tr("there_is_no_documents_alright")}
{/if}
</div>
<div n:if="!$is_gallery && sizeof($tags) > 0" class='page_wrap_content_options verticalGrayTabsWrapper'>
<div class="page_wrap_content_options_list verticalGrayTabs with_padding">
<a id="used">{_documents_all}</a>
{foreach $tags as $tag}
<a href="/search?section=docs&tags={urlencode($tag)}">{$tag}</a>
{/foreach}
</div>
</div>
</div>
{include "../components/paginator.xml", conf => $paginatorConf}
</div>
</div>
{/block}

View file

@ -0,0 +1,74 @@
{extends "../@layout.xml"}
{block title}
{_document} "{ovk_proc_strtr($doc->getName(), 20)}"
{/block}
{block header}
{$doc->getName()}
{/block}
{block content}
<style>
.sidebar, .page_header, .page_footer {
opacity: 0;
pointer-events: none;
}
.page_body {
margin-top: -45px;
}
</style>
<div class='media-page-wrapper photo-page-wrapper'>
<div class='photo-page-wrapper-photo'>
{if $is_image}
<img alt="doc image" src="{$doc->getURL()}" />
{else}
<a href="{$doc->getURL()}" download="{downloadable_name($doc->getName())}">
<input class="button" type="button" value="{_download_file}">
</a>
{/if}
</div>
<div class='ovk-photo-details'>
<div class='media-page-wrapper-description'>
<p n:if='sizeof($tags) > 0'>
{foreach $tags as $tag}
<a href="/search?section=docs&tags={urlencode($tag)}">
{$tag}{if $tag != $tags[sizeof($tags) - 1]},{/if}
</a>
{/foreach}
</p>
<div class='upload_time'>
{_info_upload_date}: {$doc->getPublicationTime()}
</div>
</div>
<hr/>
<div class="media-page-wrapper-details">
<div class='media-page-wrapper-comments'></div>
<div class='media-page-wrapper-actions docMainItem' data-context="page" data-id="{$doc->getPrettiestId()}">
{if !$doc->isOwnerHidden()}
{var $owner = $doc->getOwner()}
<a href="{$owner->getURL()}" class='media-page-author-block'>
<img class='cCompactAvatars' src="{$owner->getAvatarURL('miniscule')}">
<div class='media-page-author-block-name'>
<b>{$owner->getCanonicalName()}</b>
</div>
</a>
{/if}
{if isset($thisUser)}
<a n:if="$modifiable" class="profile_link" style="display:block;width:96%;" id="edit_icon">{_edit}</a>
<a n:if="$modifiable" class="profile_link" style="display:block;width:96%;" id="report_icon">{_report}</a>
<a n:if="!$copied || $copied && $copyImportance" class="profile_link" style="display:block;width:96%;" id="add_icon">{_add}</a>
<a n:if="$copied && !$copyImportance" class="profile_link" style="display:block;width:96%;" id="remove_icon">{_remove}</a>
{/if}
</div>
</div>
</div>
</div>
{/block}

View file

@ -0,0 +1,68 @@
{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}
»
Non-AJAX Document upload
{/block}
{block content}
<form method="post" enctype="multipart/form-data">
<table width="600">
<tbody>
<tr>
<td><span class="nobold">{_name}:</span></td>
<td><input type="text" name="name" /></td>
</tr>
<tr>
<td><span class="nobold">{_tags}:</span></td>
<td><textarea name="tags"></textarea></td>
</tr>
<tr>
<td><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></td>
<td>
<label>
<input type="checkbox" name="owner_hidden">
Owner is hidden
</label>
</td>
</tr>
<tr>
<td><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></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

@ -0,0 +1,37 @@
{var $preview = $doc->hasPreview() ? $doc->getPreview() : NULL}
{var $tags = $doc->getTags()}
{var $copied = !isset($club) ? $doc->isCopiedBy($thisUser) : $doc->isCopiedBy($club)}
{var $modifiable = $doc->canBeModifiedBy($thisUser)}
<div n:class="docMainItem, docListViewItem, $scroll_context ? scroll_node" data-id="{$doc->getPrettiestId()}">
<a class="viewerOpener" href="/doc{$doc->getPrettyId()}?key={$doc->getAccessKey()}">
{if $preview}
<img class="doc_icon" alt="document_preview" src="{$preview->getURLBySizeId('tiny')}">
{else}
<div class="doc_icon no_image">
<span>{$doc->getFileExtension()}</span>
</div>
{/if}
</a>
<div class="doc_content noOverflow">
<a class="viewerOpener noOverflow" href="/doc{$doc->getPrettyId()}?key={$doc->getAccessKey()}"><b class="noOverflow doc_name">{$doc->getName()}</b></a>
<div class="doc_content_info">
<span>{$doc->getPublicationTime()}</span>,
<span>{readable_filesize($doc->getFilesize())}</span>{if sizeof($tags) > 0} -
<span n:if="!$noTags" class="doc_tags" style="text-wrap: wrap;">
{foreach $tags as $tag}
<a href="/search?section=docs&tags={urlencode($tag)}">
{$tag}{if $tag != $tags[sizeof($tags) - 1]},{/if}
</a>
{/foreach}
</span>{/if}
</div>
</div>
<div class="doc_volume" n:if="!$hideButtons && $thisUser">
<div n:if="!$modifiable" id="report_icon"></div>
<div n:if="$modifiable" id="edit_icon"></div>
<div n:if="!$copied || $copied && $copyImportance" id="add_icon"></div>
<div n:if="$copied && !$copyImportance" id="remove_icon"></div>
</div>
</div>

View file

@ -0,0 +1,25 @@
{var $preview = $doc->hasPreview() ? $doc->getPreview() : NULL}
{var $copied = !isset($club) ? $doc->isCopiedBy($thisUser) : $doc->isCopiedBy($club)}
{var $modifiable = $doc->canBeModifiedBy($thisUser)}
<a href="/doc{$doc->getPrettyId()}?key={$doc->getAccessKey()}" n:class="docMainItem, viewerOpener, docGalleryItem, $scroll_context ? scroll_node, $embed ? embeddable" data-id="{$doc->getPrettiestId()}">
<img class="docGalleryItem_main_preview" loading="lazy" src="{$preview->getURLBySizeId('medium')}" alt="gallery photo">
{if $embed}
<div class="play-button">
<div class="play-button-ico"></div>
</div>
<img class="docGalleryItem_gif_preview" loading="lazy" src="{$doc->getURL()}" alt="gif photo view">
{/if}
<div class="doc_top_panel doc_shown_by_hover" n:if="$thisUser">
<div class="doc_volume_action" n:if="!$modifiable" id="report_icon"></div>
<div class="doc_volume_action" n:if="$modifiable" id="edit_icon"></div>
<div class="doc_volume_action" n:if="!$copied || $copied && $copyImportance" id="add_icon"></div>
<div class="doc_volume_action" n:if="$copied && !$copyImportance" id="remove_icon"></div>
</div>
<div n:class="doc_bottom_panel, doc_shown_by_hover, doc_content, $showInfo ? info_shown">
<span class="doc_bottom_panel_name noOverflow doc_name">{$doc->getName()}</span>
<span class="doc_bottom_panel_size">{readable_filesize($doc->getFilesize())}</span>
</div>
</a>

View file

@ -43,7 +43,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div n:if="$club->getFollowersCount() > 0"> <div n:if="$thisUser && $club->getFollowersCount() > 0">
{var $followersCount = $club->getFollowersCount()} {var $followersCount = $club->getFollowersCount()}
<div class="content_title_expanded" onclick="hidePanel(this, {$followersCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$followersCount});">
@ -91,7 +91,7 @@
</div> </div>
</div> </div>
<div> <div n:if="$thisUser">
<div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$audiosCount});">
{_audios} {_audios}
</div> </div>
@ -291,6 +291,24 @@
</div> </div>
</div> </div>
</div> </div>
<div n:if="($thisUser && $docsCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser)))">
<div class="content_title_expanded" onclick="hidePanel(this, {$topicsCount});">
{_documents}
</div>
<div>
<div class="content_subtitle">
{tr("documents", $docsCount)}
<div style="float: right;">
<a href="/docs{$club->getRealId()}">{_all_title}</a>
</div>
</div>
<div>
{foreach $docs as $doc}
{include "../Documents/components/doc.xml", doc => $doc, hideButtons => true, noTags => true}
{/foreach}
</div>
</div>
</div>
</div> </div>
{/block} {/block}

View file

@ -49,6 +49,9 @@
<div n:attr="id => ($mode === 'audio' ? 'activetabs' : 'ki')" class="tab" mode="audio"> <div n:attr="id => ($mode === 'audio' ? 'activetabs' : 'ki')" class="tab" mode="audio">
<a n:attr="id => ($mode === 'audio' ? 'act_tab_a' : 'ki')">{_audios}</a> <a n:attr="id => ($mode === 'audio' ? 'act_tab_a' : 'ki')">{_audios}</a>
</div> </div>
<div n:attr="id => ($mode === 'docs' ? 'activetabs' : 'ki')" class="tab" mode="doc">
<a n:attr="id => ($mode === 'docs' ? 'act_tab_a' : 'ki')">{_documents}</a>
</div>
</center> </center>
<script> <script>

View file

@ -25,6 +25,8 @@
{/if} {/if}
{elseif $type == "audio"} {elseif $type == "audio"}
{include "../Audio/player.xml", audio => $object} {include "../Audio/player.xml", audio => $object}
{elseif $type == "doc"}
{include "../Documents/components/doc.xml", doc => $object}
{else} {else}
{include "../components/error.xml", description => tr("version_incompatibility")} {include "../components/error.xml", description => tr("version_incompatibility")}
{/if} {/if}

View file

@ -284,6 +284,18 @@
highlightText({$query}, '.page_wrap_content_main', [".playlistName", ".playlistDesc"]) highlightText({$query}, '.page_wrap_content_main', [".playlistName", ".playlistDesc"])
} }
__scrollHook()
</script>
{elseif $section === 'docs'}
<div class='scroll_node search_content' n:foreach="$data as $dat">
{include "../Documents/components/doc.xml", doc => $dat, copyImportance => true}
</div>
<script n:if="$count > 0 && !empty($query)">
function __scrollHook(page) {
highlightText({$query}, '.page_wrap_content_main', [".doc_content .noOverflow"])
}
__scrollHook() __scrollHook()
</script> </script>
{/if} {/if}
@ -300,6 +312,7 @@
<a n:attr="id => $section === 'apps' ? 'used'" href="/search?section=apps&q={urlencode($query)}"> {_s_apps}</a> <a n:attr="id => $section === 'apps' ? 'used'" href="/search?section=apps&q={urlencode($query)}"> {_s_apps}</a>
<a n:attr="id => $section === 'audios' ? 'used'" href="/search?section=audios&q={urlencode($query)}"> {_s_audios}</a> <a n:attr="id => $section === 'audios' ? 'used'" href="/search?section=audios&q={urlencode($query)}"> {_s_audios}</a>
<a n:attr="id => $section === 'audios_playlists' ? 'used'" href="/search?section=audios_playlists&q={urlencode($query)}">{_s_audios_playlists}</a> <a n:attr="id => $section === 'audios_playlists' ? 'used'" href="/search?section=audios_playlists&q={urlencode($query)}">{_s_audios_playlists}</a>
<a n:attr="id => $section === 'docs' ? 'used'" href="/search?section=docs&q={urlencode($query)}">{_s_documents}</a>
</div> </div>
<div class='page_search_options'> <div class='page_search_options'>
@ -415,6 +428,19 @@
</label> </label>
</div> </div>
</div> </div>
<div n:if="$section == 'docs'" class="search_option">
<div class="search_option_name">
<div class='search_option_name_ico'></div>
{_s_type}
</div>
<div class="search_option_content">
<select name="type" form="search_form" data-default='0'>
<option n:foreach="range(0, 8) as $i" value="{$i}" n:attr="selected => $_REQUEST['type'] == $i">
{tr("document_type_".$i)}
</option>
</select>
</div>
</div>
<div n:if="$section == 'audios'" class="search_option"> <div n:if="$section == 'audios'" class="search_option">
<div class="search_option_name"> <div class="search_option_name">
<div class='search_option_name_ico'></div> <div class='search_option_name_ico'></div>

View file

@ -684,7 +684,19 @@
<td> <td>
<span class="nobold">{_my_apps}</span> <span class="nobold">{_my_apps}</span>
</td> </td>
</tr><tr n:if="sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0"> </tr>
<tr>
<td width="120" valign="top" align="right" align="right">
<input
n:attr="checked => $user->getLeftMenuItemStatus('docs')"
type="checkbox"
name="menu_doxc" />
</td>
<td>
<span class="nobold">{_my_documents}</span>
</td>
</tr>
<tr n:if="sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0">
<td width="120" valign="top" align="right" align="right"> <td width="120" valign="top" align="right" align="right">
<input <input
n:attr="checked => $user->getLeftMenuItemStatus('links')" n:attr="checked => $user->getLeftMenuItemStatus('links')"

View file

@ -65,6 +65,24 @@
<div style="width:100%;" data-att_type='audio' data-att_id="{$attachment->getPrettyId()}"> <div style="width:100%;" data-att_type='audio' data-att_id="{$attachment->getPrettyId()}">
{include "../Audio/player.xml", audio => $attachment} {include "../Audio/player.xml", audio => $attachment}
</div> </div>
{elseif $attachment instanceof \openvk\Web\Models\Entities\Document}
<div style="width:100%;">
<div style="display:none" data-att_type="doc" data-att_id="{$attachment->getPrettiestId()}">
<div class="docMainItem attachment_doc attachment_note">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 10"><polygon points="0 0 0 10 8 10 8 4 4 4 4 0 0 0"/><polygon points="5 0 5 3 8 3 5 0"/></svg>
<div class='attachment_note_content'>
<span class="attachment_note_text">{_document}</span>
<span class="attachment_note_name"><a href="/doc{$attachment->getPrettyId()}">{ovk_proc_strtr($attachment->getName(), 40)}</a></span>
</div>
</div>
</div>
{if $attachment->isImage()}
{include "../Documents/components/image.xml", doc => $attachment, copyImportance => true, showInfo => true, embed => $attachment->isGif()}
{else}
{include "../Documents/components/doc.xml", doc => $attachment, copyImportance => true, noTags => true}
{/if}
</div>
{else} {else}
<span style="color:red;">{_version_incompatibility}</span> <span style="color:red;">{_version_incompatibility}</span>
{/if} {/if}

View file

@ -86,6 +86,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" />
{_audio} {_audio}
</a> </a>
<a n:if="$docs ?? true" id="__documentAttachment" {if !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)}data-club="{$club->getRealId()}"{/if}>
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-octet-stream.png" />
{_document}
</a>
<a n:if="$notes ?? false" id="__notesAttachment"> <a n:if="$notes ?? false" id="__notesAttachment">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" />
{_note} {_note}

View file

@ -10,7 +10,7 @@
<div class="insertThere" id="postz"></div> <div class="insertThere" id="postz"></div>
<div id="underHeader"> <div id="underHeader">
<div n:if="$canPost" class="content_subtitle"> <div n:if="$canPost" class="content_subtitle">
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true, hasSource => true, geo => true} {include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true, hasSource => true, geo => true, docs => true}
</div> </div>
<div class="content scroll_container"> <div class="content scroll_container">

View file

@ -27,6 +27,7 @@ services:
- openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\VKAPIPresenter
- openvk\Web\Presenters\PollPresenter - openvk\Web\Presenters\PollPresenter
- openvk\Web\Presenters\BannedLinkPresenter - openvk\Web\Presenters\BannedLinkPresenter
- openvk\Web\Presenters\DocumentsPresenter
- openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Users
- openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Posts
- openvk\Web\Models\Repositories\Polls - openvk\Web\Models\Repositories\Polls
@ -52,5 +53,6 @@ services:
- openvk\Web\Models\Repositories\Aliases - openvk\Web\Models\Repositories\Aliases
- openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Models\Repositories\ChandlerGroups - openvk\Web\Models\Repositories\ChandlerGroups
- openvk\Web\Models\Repositories\Documents
- openvk\Web\Presenters\MaintenancePresenter - openvk\Web\Presenters\MaintenancePresenter
- openvk\Web\Presenters\NoSpamPresenter - openvk\Web\Presenters\NoSpamPresenter

View file

@ -311,6 +311,14 @@ routes:
handler: "Poll->view" handler: "Poll->view"
- url: "/poll{num}/voters" - url: "/poll{num}/voters"
handler: "Poll->voters" handler: "Poll->voters"
- url: "/docs"
handler: "Documents->list"
- url: "/docs{num}"
handler: "Documents->listGroup"
- url: "/docs/upload"
handler: "Documents->upload"
- url: "/doc{num}_{num}"
handler: "Documents->page"
- url: "/admin" - url: "/admin"
handler: "Admin->index" handler: "Admin->index"
- url: "/admin/users" - url: "/admin/users"

View file

@ -55,6 +55,10 @@ h1 {
flex-direction: row; flex-direction: row;
} }
.display_flex_space_between {
justify-content: space-between;
}
.layout { .layout {
width: 791px; width: 791px;
margin: 0 auto; margin: 0 auto;
@ -2248,6 +2252,12 @@ table td[width="120"] {
border-radius: 2px; border-radius: 2px;
} }
.mb_tab .special_counter {
color: #595959;
font-weight: bold;
margin: 0px 1px;
}
.mb_tab:hover { .mb_tab:hover {
background: #e2e0e0; background: #e2e0e0;
} }
@ -2265,6 +2275,10 @@ table td[width="120"] {
color: white; color: white;
} }
.mb_tab#active .special_counter {
color: #bdbdbd;
}
.border-block { .border-block {
box-shadow: inset 0 0 0 1px #b6bfca, inset 0 0 0 10px #d8dfe7; box-shadow: inset 0 0 0 1px #b6bfca, inset 0 0 0 10px #d8dfe7;
width: 300px; width: 300px;
@ -2560,7 +2574,7 @@ a.poll-retract-vote {
min-height: 63px; min-height: 63px;
} }
.post-horizontal .upload-item .play-button, .compact_video .play-button { .post-horizontal .upload-item .play-button, .compact_video .play-button, .docGalleryItem .play-button {
position: absolute; position: absolute;
height: 30px; height: 30px;
width: 30px; width: 30px;
@ -2571,7 +2585,7 @@ a.poll-retract-vote {
align-items: center; align-items: center;
} }
.post-horizontal .upload-item .play-button .play-button-ico, .compact_video .play-button .play-button-ico { .post-horizontal .upload-item .play-button .play-button-ico, .compact_video .play-button .play-button-ico, .docGalleryItem .play-button .play-button-ico {
background: url(/assets/packages/static/openvk/img/wall.png) no-repeat 1px 0; background: url(/assets/packages/static/openvk/img/wall.png) no-repeat 1px 0;
display: inline-block; display: inline-block;
height: 15px; height: 15px;
@ -2694,6 +2708,10 @@ a.poll-retract-vote {
justify-content: center; justify-content: center;
} }
.ovk-photo-view .photo_viewer_wrapper.photo_viewer_wrapper_scrollable {
height: unset;
}
.ovk-photo-view #ovk-photo-img { .ovk-photo-view #ovk-photo-img {
max-width: 100%; max-width: 100%;
max-height: 80vh; max-height: 80vh;
@ -2823,7 +2841,7 @@ a.poll-retract-vote {
height: 20px; height: 20px;
background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px;
background-color: #fff; background-color: #fff;
padding-left: 18px; padding-left: 18px !important;
width: 120px; width: 120px;
} }
@ -3964,3 +3982,284 @@ hr {
margin-bottom: -2px; margin-bottom: -2px;
fill: #7d7d7d; fill: #7d7d7d;
} }
/* Documents */
#docs_page_wrapper {
margin-left: -10px;
margin-top: -10px;
margin-bottom: -10px;
display: block;
width: 102.8%;
}
#docs_page_wrapper .docs_page_search {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 10px;
border-bottom: 1px solid #CCCCCC;
background: #F0F0F0;
}
#docs_page_wrapper .docs_page_search input[type="search"], .attachment_selector .attachment_search input {
height: 23px;
background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 5px;
background-color: #fff;
padding-left: 18px !important;
}
#docs_page_wrapper .docs_page_search input[type="search"], #docs_page_wrapper .docs_page_search form {
width: 100%;
}
#docs_page_wrapper .container_white {
display: flex;
flex-direction: column;
padding: 5px 10px;
}
#docs_page_wrapper .docs_page_content.docs_page_gallery .scroll_container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
padding: 10px 10px;
}
.docGalleryItem {
height: 200px;
cursor: pointer;
position: relative;
/*width: 200px;*/
}
.attachments .docGalleryItem.embeddable {
display: flex;
align-items: center;
justify-content: center;
/*background: black;*/
}
.attachments .docGalleryItem.embeddable .docGalleryItem_gif_preview {
object-fit: contain;
}
.attachments .docGalleryItem.embeddable.playing .play-button, .attachments .docGalleryItem.embeddable.playing .doc_bottom_panel, .docGalleryItem .docGalleryItem_gif_preview, .attachments .docGalleryItem.embeddable.playing .docGalleryItem_main_preview {
display: none;
}
.attachments .docGalleryItem.embeddable.playing .docGalleryItem_gif_preview {
display: block;
}
.docGalleryItem img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.docGalleryItem .doc_bottom_panel {
position: absolute;
bottom: 0px;
background: rgba(1, 1, 1, 0.7);
padding: 6px 6px;
width: calc(100% - 12px);
display: grid;
grid-template-columns: 1fr 0fr;
}
.docGalleryItem .doc_shown_by_hover {
transition: all 100ms ease-in;
opacity: 0;
}
.docGalleryItem:hover .doc_shown_by_hover, .doc_shown_by_hover.info_shown {
opacity: 1;
}
.docGalleryItem .doc_bottom_panel span {
color: white;
}
.docGalleryItem .doc_bottom_panel .doc_bottom_panel_size {
max-width: 49px;
width: max-content;
}
.docGalleryItem .doc_top_panel {
position: absolute;
top: 5px;
right: 5px;
background: rgba(1, 1, 1, 0.7);
padding: 6px 6px;
display: flex;
gap: 5px;
}
.docGalleryItem .doc_top_panel > div {
width: 10px;
height: 10px;
background: url('/assets/packages/static/openvk/img/docs_controls.png?v=8');
background-size: 57px;
background-position-y: -24px;
background-repeat: no-repeat;
}
#docs_page_wrapper select {
width: 150px;
}
.docListViewItem {
min-height: 38px;
display: grid;
grid-template-columns: 0fr 1fr 0fr;
gap: 10px;
padding: 7px 7px;
border-bottom: 1px solid #EDEDED;
}
.docs_page_content .docListViewItem:last-of-type {
border-bottom: unset !important;
}
.docListViewItem:hover, .attachButton:hover {
background: #f7f7f7;
}
.docListViewItem .doc_icon {
width: 50px;
height: 38px;
background: #E6E6E6;
user-select: none;
object-fit: cover;
border-radius: 2px;
}
.docListViewItem .doc_content {
display: flex;
flex-direction: column;
margin-top: 1px;
/*gap: 5px;*/
}
.docListViewItem .doc_content b {
color: black;
}
.docListViewItem .doc_content .doc_content_info, .docListViewItem .doc_content span {
color: #6d6d6d;
}
.docListViewItem .doc_icon.no_image {
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
height: 17px;
padding: 2px 0px;
}
.docListViewItem .doc_icon.no_image span {
color: #6b6b6b;
}
.docListViewItem .doc_icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.docListViewItem .doc_volume {
display: flex;
align-items: center;
gap: 5px;
visibility: hidden;
}
.docListViewItem .doc_volume > div {
width: 20px;
height: 20px;
background: url('/assets/packages/static/openvk/img/docs_controls.png?v=8');
}
.docListViewItem:hover .doc_volume {
visibility: visible;
}
.docListViewItem .doc_volume > div:hover {
cursor: pointer;
background-position-y: -21px;
}
.docListViewItem .doc_volume #report_icon {
background-position-x: -40px;
}
.docGalleryItem .doc_top_panel #report_icon {
background-position-x: -24px;
}
.docListViewItem .doc_volume #edit_icon {
background-position-x: -20px;
}
.docListViewItem .doc_volume #add_icon {
background-position-x: -60px;
}
.docGalleryItem .doc_top_panel #add_icon {
background-position-x: -35px;
}
.docGalleryItem .doc_top_panel #edit_icon {
background-position-x: -12px;
}
.docListViewItem .doc_volume #mark_icon {
background-position-x: -80px;
}
.docGalleryItem .doc_top_panel #mark_icon {
background-position-x: -46px;
}
.doc_viewer_wrapper {
overflow: hidden;
}
.attachments .docListViewItem {
min-height: 30px;
border-bottom: unset;
}
.attachments .docGalleryItem {
display: block;
min-width: 170px;
height: 170px;
width: 50%;
margin-bottom: 4px;
}
.attachButton {
width: 12%;
height: 55px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
}
.attachment_selector .attachment_search {
margin: 6px 0px;
}
.attachment_selector .attachment_search input {
height: 30px;
background-position-y: 9px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

532
Web/static/js/al_docs.js Normal file
View file

@ -0,0 +1,532 @@
function showDocumentUploadDialog(target = null, append_to_url = null, after_upload = null)
{
let file = null
const cmsg = new CMessageBox({
title: tr("document_uploading_in_general"),
body: `
<b>${tr("limits")}</b>
<ul style="margin: 5px 0px;padding-left: 20px;">
<li>${tr('limitations_file_limit_size', window.openvk.docs_max)}.</li>
<li>${tr('limitations_file_allowed_formats')}: ${window.openvk.docs_allowed.sort(() => Math.random() - 0.59).slice(0, 10).join(', ')}.</li>
<li>${tr("limitations_file_author_rights")}.</li>
</ul>
<div id="_document_upload_frame" style="text-align:center;margin: 10px 0px 2px 0px;">
<input onclick="upload_btn.click()" class="button" type="button" value="${tr("select_file_fp")}">
<input id="upload_btn" type="file" accept="${window.openvk.docs_allowed.join(",.")}" style="display:none;">
</div>
`,
buttons: [tr('close')],
callbacks: [Function.noop],
unique_name: "doc_upload_dialog",
})
cmsg.getNode().find('.ovk-diag-body').attr('style', "padding:15px;")
cmsg.getNode().attr('style', "width: 400px;")
cmsg.getNode().find('#upload_btn').on('change', (e) => {
file = e.target.files[0]
const name = file.name
const format = name.split(".")[name.split(".").length - 1]
if(window.openvk.docs_allowed.indexOf(format.toLowerCase()) == -1) {
makeError(tr("error_file_invalid_format"))
return
}
if(file.size > window.openvk.docs_max * 1024 * 1024) {
makeError(tr("error_file_too_big"))
return
}
cmsg.close()
const cmsg_2 = new CMessageBox({
title: tr("document_uploading_in_general"),
body: `
<p><b>${tr("info_name")}</b></p>
<input type="text" name="doc_name" value="${name}" placeholder="...">
<label>
<input maxlength="255" value="0" type="radio" name="doc_access" checked>
${tr("private_document")}
</label>
<br>
<label>
<input value="3" type="radio" name="doc_access">
${tr("public_document")}
</label>
<p><b>${tr("tags")}</b></p>
<input maxlength="256" type="text" name="doc_tags" placeholder="...">
<br>
<label>
<input type="checkbox" name="doc_owner" checked>
${tr("owner_is_hidden")}
</label>
`,
buttons: [tr('upload_button'), tr('cancel')],
callbacks: [async () => {
const fd = new FormData
fd.append("name", u(`input[name="doc_name"]`).nodes[0].value)
fd.append("tags", u(`input[name="doc_tags"]`).nodes[0].value)
fd.append("folder", u(`input[name="doc_access"]:checked`).nodes[0].value)
fd.append("owner_hidden", u(`input[name="doc_owner"]`).nodes[0].checked ? "on" : "off")
fd.append("blob", file)
fd.append("ajax", 1)
fd.append("hash", window.router.csrf)
const endpoint_url = `/docs/upload` + (!isNaN(append_to_url) ? "?gid="+append_to_url : '')
const fetcher = await fetch(endpoint_url, {
method: 'POST',
body: fd,
})
const json = await fetcher.json()
if(json.success) {
if(target != "search") {
window.router.route(location.href)
} else {
if(after_upload)
after_upload()
}
} else {
fastError(escapeHtml(json.flash.message))
}
}, Function.noop],
})
cmsg_2.getNode().find('.ovk-diag-body').attr('style', "padding:15px;")
cmsg_2.getNode().attr('style', "width: 400px;")
})
}
u(document).on("drop", "#_document_upload_frame", (e) => {
e.dataTransfer.dropEffect = 'move';
e.preventDefault()
u(`#_document_upload_frame #upload_btn`).nodes[0].files = e.dataTransfer.files
u("#_document_upload_frame #upload_btn").trigger("change")
})
u(document).on('click', '.docMainItem #edit_icon', async (e) => {
e.preventDefault()
if(u("#ajloader").hasClass("shown")) {
return
}
const target = u(e.target).closest("#edit_icon")
const item = target.closest('.docMainItem')
const id = item.nodes[0].dataset.id
CMessageBox.toggleLoader()
const docs = await window.OVKAPI.call('docs.getById', {docs: id, return_tags: 1})
const doc = docs[0]
if(!doc) {
fastError("(")
CMessageBox.toggleLoader()
return
}
const cmsg_2 = new CMessageBox({
unique_name: "document_edit_modal",
title: tr("document_editing_in_general"),
body: `
<p><b>${tr("info_name")}</b></p>
<input maxlength="128" type="text" name="doc_name" value="${doc.title}" placeholder="...">
<label>
<input value="0" type="radio" name="doc_access" ${doc.folder_id != 3 ? "checked" : ''}>
${tr("private_document")}
</label>
<br>
<label>
<input value="3" type="radio" name="doc_access" ${doc.folder_id == 3 ? "checked" : ''}>
${tr("public_document")}
</label>
<p><b>${tr("tags")}</b></p>
<input maxlength="256" type="text" name="doc_tags" value="${doc.tags.join(',')}" placeholder="...">
<br>
<label>
<input type="checkbox" name="doc_owner" ${doc.is_hidden ? "checked" : ''}>
${tr("owner_is_hidden")}
</label>
`,
buttons: [tr('save'), tr('cancel')],
callbacks: [async () => {
const params = {
owner_id: id.split('_')[0],
doc_id: id.split('_')[1],
title: u(`input[name='doc_name']`).nodes[0].value,
tags: u(`input[name='doc_tags']`).nodes[0].value,
folder_id: u(`input[name="doc_access"]:checked`).nodes[0].value,
owner_hidden: u(`input[name="doc_owner"]`).nodes[0].checked ? 1 : 0,
}
const edit = await window.OVKAPI.call('docs.edit', params)
if(edit == 1) {
item.find('.doc_content .doc_name').html(escapeHtml(params.title))
item.find('.doc_content .doc_tags').html(escapeHtml(params.tags))
}
}, Function.noop],
})
cmsg_2.getNode().find('.ovk-diag-body').attr('style', "padding:15px;")
cmsg_2.getNode().attr('style', "width: 400px;")
CMessageBox.toggleLoader()
})
u(document).on('click', '#upload_entry_point', (e) => {
showDocumentUploadDialog(null, Number(e.target.dataset.gid))
})
u(document).on('change', "#docs_page_wrapper select[name='docs_sort']", (e) => {
const new_url = new URL(location.href)
new_url.searchParams.set('order', e.target.value)
window.router.route(new_url.href)
})
u(document).on('click', '.docMainItem #remove_icon', async (e) => {
e.preventDefault()
const target = u(e.target).closest("#remove_icon")
const item = target.closest('.docMainItem')
const context = item.attr('data-context')
const id = item.nodes[0].dataset.id.split("_")
target.addClass('lagged')
const res = await window.OVKAPI.call('docs.delete', {owner_id: id[0], doc_id: id[1]})
target.removeClass('lagged')
if(res == 1) {
target.attr('id', 'mark_icon')
if(context == "page") {
target.html('✓')
window.router.route('/docs')
}
}
})
u(document).on('click', '.docMainItem #add_icon', async (e) => {
e.preventDefault()
const target = u(e.target).closest("#add_icon")
const item = target.closest('.docMainItem')
const id = item.nodes[0].dataset.id.split("_")
const context = item.attr('data-context')
target.addClass('lagged')
try {
const res = await window.OVKAPI.call('docs.add', {owner_id: id[0], doc_id: id[1], access_key: id[2]})
} catch(e) {
makeError(tr("error_file_adding_copied"))
target.removeClass('lagged')
return
}
target.removeClass('lagged')
target.attr('id', 'mark_icon')
if(context == "page") {
target.html('✓')
}
})
u(document).on('click', '.docMainItem #report_icon', (e) => {
e.preventDefault()
const target = u(e.target).closest("#report_icon")
const item = target.closest('.docMainItem')
const id = item.nodes[0].dataset.id.split("_")
MessageBox(tr("report_question"), `
${tr("going_to_report_doc")}
<br/>${tr("report_question_text")}
<br/><br/><b> ${tr("report_reason")}</b>: <input type='text' id='uReportMsgInput' placeholder='${tr("reason")}' />`, [tr("confirm_m"), tr("cancel")], [(function() {
res = document.querySelector("#uReportMsgInput").value;
xhr = new XMLHttpRequest();
xhr.open("GET", "/report/" + id[1] + "?reason=" + res + "&type=doc", true);
xhr.onload = (function() {
if(xhr.responseText.indexOf("reason") === -1)
MessageBox(tr("error"), tr("error_sending_report"), ["OK"], [Function.noop]);
else
MessageBox(tr("action_successfully"), tr("will_be_watched"), ["OK"], [Function.noop]);
});
xhr.send(null)
}),
Function.noop])
})
u(document).on("click", ".docListViewItem a.viewerOpener, a.docGalleryItem", async (e) => {
e.preventDefault()
if(e.target.closest('.doc_volume_action')) {
return
}
if(window.openvk.current_id == 0) {
return
}
const target = u(e.target)
const link = target.closest('a')
if(target.closest(".embeddable").length > 0) {
target.closest(".embeddable").toggleClass("playing")
return
}
CMessageBox.toggleLoader()
const url = link.nodes[0].href
const request = await fetch(url)
const body_html = await request.text()
const parser = new DOMParser
const body = parser.parseFromString(body_html, "text/html")
const preview = body.querySelector('.photo-page-wrapper-photo')
const details = body.querySelector('.ovk-photo-details')
u(preview.querySelector('img')).attr('id', 'ovk-photo-img')
const photo_viewer = new CMessageBox({
title: '',
custom_template: u(`
<div class="ovk-photo-view-dimmer">
<div class="ovk-photo-view">
<div class="photo_com_title">
<text id="photo_com_title_photos">
${tr("document")}
</text>
<div>
<a id="ovk-photo-close">${tr("close")}</a>
</div>
</div>
<div class='photo_viewer_wrapper doc_viewer_wrapper'>
${preview.innerHTML}
</div>
<div class="ovk-photo-details">
${details.innerHTML}
</div>
</div>
</div>`)
})
photo_viewer.getNode().find("#ovk-photo-close").on("click", function(e) {
photo_viewer.close()
});
CMessageBox.toggleLoader()
})
// ctx > "wall" and maybe "messages" in future
// source > "user" || "club" > source_arg
async function __docAttachment(form, ctx = "wall", source = "user", source_arg = 0) {
const per_page = 10
const msg = new CMessageBox({
title: tr('select_doc'),
custom_template: u(`
<div class="ovk-photo-view-dimmer">
<div class="ovk-photo-view">
<div class="photo_com_title">
<text id="photo_com_title_photos">
${tr("select_doc")}
</text>
<span style="display: inline-flex;gap: 7px;">
${source != "user" ? `<a id="_doc_picker_go_to_my">${tr("go_to_my_documents")}</a>`: ""}
<a id="_doc_picker_upload">${tr("upload_button")}</a>
</span>
<div>
<a id="ovk-photo-close">${tr("close")}</a>
</div>
</div>
<div class='photo_viewer_wrapper photo_viewer_wrapper_scrollable doc_viewer_wrapper'>
<div class='attachment_selector' style="width: 100%;">
<div class="attachment_search">
<input type="search" maxlength="100" name="q" class="input_with_search_icon" placeholder="${tr("search_by_documents")}">
</div>
<div id='_attachment_insert'>
<div class="docsInsert"></div>
</div>
</div>
</div>
<div class="ovk-photo-details"></div>
</div>
</div>`),
})
msg.getNode().find(".ovk-photo-view").attr('style', 'width: 400px;min-height:90vh;')
msg.getNode().find('.ovk-diag-body').attr('style', 'height:335px;padding:0px;')
docs_reciever = new class {
ctx = "my"
ctx_id = 0
stat = {
page: 0,
pagesCount: 0,
count: 0,
}
clean() {
this.stat = {
page: 0,
pagesCount: 0,
count: 0,
}
u('#gif_loader, #_attachment_insert #show_more').remove()
u("#_attachment_insert .docsInsert").html("")
}
async page(page = 1, perPage = 10) {
u('#_attachment_insert').append(`<div id='gif_loader'></div>`)
const fd = new FormData
fd.append("context", "list")
fd.append("hash", window.router.csrf)
let url = `/docs${source == "club" ? source_arg : ""}?picker=1&p=${page}`
if(this.query) {
fd.append("context", "search")
fd.append("ctx_query", this.query)
}
const req = await fetch(url, {
method: "POST",
body: fd
})
const res = await req.text()
const dom = new DOMParser
const pre = dom.parseFromString(res, "text/html")
const pagesCount = Number(pre.querySelector("input[name='pagesCount']").value)
const count = Number(pre.querySelector("input[name='count']").value)
if(count < 1) {
u('#_attachment_insert .docsInsert').append(`
<div class="information">
&nbsp; ${tr("no_documents")}.
</div>
`)
}
pre.querySelectorAll("._content").forEach(doc => {
const res = u(`${doc.outerHTML}`)
const id = res.attr("data-attachmentdata")
res.find(".docMainItem").attr("style", "width: 85%;")
res.append(`
<div class="attachButton" id='__attach_doc'>
${this.isDocAttached(id) ? tr("detach") : tr("attach")}
</div>
`)
u('#_attachment_insert .docsInsert').append(res)
})
this.stat.page = page
this.stat.pagesCount = pagesCount
this.stat.count = count
u('#gif_loader').remove()
this.showMore()
}
async search(query_string) {
this.clean()
if(query_string == "")
this.query = null
else
this.query = query_string
await this.page(1)
}
showMore() {
if(this.stat.page < this.stat.pagesCount) {
u('#_attachment_insert').append(`
<div id="show_more" data-pagesCount="${this.stat.pagesCount}">
<span>${tr('show_more')}</span>
</div>
`)
}
}
maxAttachmentsCheck() {
if(u(form).find(`.upload-item`).length > window.openvk.max_attachments) {
makeError(tr('too_many_attachments'), 'Red', 10000, 1)
return true
}
return false
}
attach(dataset, button) {
if(this.isDocAttached(dataset.attachmentdata)) {
(form.find(`.upload-item[data-type='doc'][data-id='${dataset.attachmentdata}']`)).remove()
button.html(tr('attach'))
} else {
const _url = dataset.attachmentdata.split("_")
button.html(tr('detach'))
form.find('.post-vertical').append(`
<div class="vertical-attachment upload-item" draggable="true" data-type='doc' data-id="${dataset.attachmentdata}">
<div class='vertical-attachment-content' draggable="false">
<div class="docMainItem attachment_doc attachment_note">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 10"><polygon points="0 0 0 10 8 10 8 4 4 4 4 0 0 0"/><polygon points="5 0 5 3 8 3 5 0"/></svg>
<div class='attachment_note_content'>
<span class="attachment_note_text">${tr("document")}</span>
<span class="attachment_note_name"><a href="/doc${_url[0]}_${_url[1]}?key=${_url[2]}">${ovk_proc_strtr(escapeHtml(dataset.name), 50)}</a></span>
</div>
</div>
</div>
<div class='vertical-attachment-remove'>
<div id='small_remove_button'></div>
</div>
</div>
`)
}
}
isDocAttached(attachmentdata) {
return (form.find(`.upload-item[data-type='doc'][data-id='${attachmentdata}']`)).length > 0
}
}
msg.getNode().find("#ovk-photo-close").on("click", function(e) {
msg.close()
})
msg.getNode().on("click", "#__attach_doc", async (ev) => {
if(docs_reciever.maxAttachmentsCheck() == true) {
return
}
const target = u(ev.target).closest('._content')
const button = target.find('#__attach_doc')
const dataset = target.nodes[0].dataset
docs_reciever.attach(dataset, button)
})
msg.getNode().on("click", "#show_more", async (ev) => {
const target = u(ev.target).closest('#show_more')
target.addClass('lagged')
await docs_reciever.page(docs_reciever.stat.page + 1)
target.remove()
})
msg.getNode().on("click", "#_doc_picker_go_to_my", async (e) => {
msg.close()
await __docAttachment(form, "wall")
})
msg.getNode().on("click", "#_doc_picker_upload", async (e) => {
showDocumentUploadDialog("search", source_arg >= 0 ? NaN : Math.abs(source_arg), () => {
docs_reciever.clean()
docs_reciever.page(1)
})
})
msg.getNode().on("change", ".attachment_search input", async (e) => {
await docs_reciever.search(ovk_proc_strtr(e.target.value, 100))
})
await docs_reciever.page(docs_reciever.stat.page + 1)
}
u(document).on('click', '#__documentAttachment', async (e) => {
const form = u(e.target).closest('form')
const targ = u(e.target).closest("#__documentAttachment")
let entity_source = "user"
let entity_id = 0
if(targ.attr('data-club') != null) {
entity_source = "club"
entity_id = Number(targ.attr('data-club'))
}
await __docAttachment(form, "wall", entity_source, entity_id)
})

View file

@ -999,6 +999,10 @@ u(document).on("click", "#editPost", async (e) => {
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/audio-ac3.png" />
${tr('audio')} ${tr('audio')}
</a> </a>
<a id="__documentAttachment">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-octet-stream.png" />
${tr('document')}
</a>
${type == 'post' ? `<a id="__notesAttachment"> ${type == 'post' ? `<a id="__notesAttachment">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/mimetypes/application-x-srt.png" />
${tr('note')} ${tr('note')}
@ -1035,7 +1039,10 @@ u(document).on("click", "#editPost", async (e) => {
// horizontal attachments // horizontal attachments
api_post.attachments.forEach(att => { api_post.attachments.forEach(att => {
const type = att.type const type = att.type
const aid = att[type].owner_id + '_' + att[type].id let aid = att[type].owner_id + '_' + att[type].id
if(att[type] && att[type].access_key) {
aid += "_" + att[type].access_key
}
if(type == 'video' || type == 'photo') { if(type == 'video' || type == 'photo') {
let preview = '' let preview = ''

View file

@ -309,10 +309,6 @@ u(document).on('submit', 'form', async (e) => {
return return
} }
if((localStorage.getItem('ux.disable_ajax_routing') ?? 0) == 1 || window.openvk.current_id == 0) {
return false
}
if(window.openvk.disable_ajax == 1) { if(window.openvk.disable_ajax == 1) {
return false return false
} }
@ -322,6 +318,10 @@ u(document).on('submit', 'form', async (e) => {
collect_attachments_node(target) collect_attachments_node(target)
} }
if((localStorage.getItem('ux.disable_ajax_routing') ?? 0) == 1 || window.openvk.current_id == 0) {
return false
}
u('#ajloader').addClass('shown') u('#ajloader').addClass('shown')
const form = e.target const form = e.target

View file

@ -261,6 +261,11 @@ function parseAttachments($attachments, array $allow_types = ['photo', 'video',
'method' => 'get', 'method' => 'get',
'onlyId' => true, 'onlyId' => true,
], ],
'doc' => [
'repo' => 'openvk\Web\Models\Repositories\Documents',
'method' => 'getDocumentById',
'withKey' => true,
]
]; ];
foreach($exploded_attachments as $attachment_string) { foreach($exploded_attachments as $attachment_string) {
@ -277,6 +282,14 @@ function parseAttachments($attachments, array $allow_types = ['photo', 'video',
$repository_class = $repositories[$attachment_type]['repo']; $repository_class = $repositories[$attachment_type]['repo'];
if(!$repository_class) continue; if(!$repository_class) continue;
$attachment_model = (new $repository_class)->{$repositories[$attachment_type]['method']}($attachment_id); $attachment_model = (new $repository_class)->{$repositories[$attachment_type]['method']}($attachment_id);
$output_attachments[] = $attachment_model;
} elseif($repositories[$attachment_type]['withKey']) {
[$attachment_owner, $attachment_id, $access_key] = explode('_', $attachment_ids);
$repository_class = $repositories[$attachment_type]['repo'];
if(!$repository_class) continue;
$attachment_model = (new $repository_class)->{$repositories[$attachment_type]['method']}((int)$attachment_owner, (int)$attachment_id, $access_key);
$output_attachments[] = $attachment_model; $output_attachments[] = $attachment_model;
} else { } else {
[$attachment_owner, $attachment_id] = array_map('intval', explode('_', $attachment_ids)); [$attachment_owner, $attachment_id] = array_map('intval', explode('_', $attachment_ids));
@ -371,6 +384,23 @@ function escape_html(string $unsafe): string
return htmlspecialchars($unsafe, ENT_DISALLOWED | ENT_XHTML); 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];
}
function downloadable_name(string $text): string
{
return preg_replace('/[\\/:*?"<>|]/', '_', str_replace(' ', '_', $text));
}
return (function() { return (function() {
_ovk_check_environment(); _ovk_check_environment();
require __DIR__ . "/vendor/autoload.php"; require __DIR__ . "/vendor/autoload.php";

View file

@ -0,0 +1,28 @@
CREATE TABLE `documents` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`owner` BIGINT(20) 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`;

View file

@ -711,6 +711,7 @@
"search_for_notes" = "Search for notes"; "search_for_notes" = "Search for notes";
"search_for_audios" = "Search for music"; "search_for_audios" = "Search for music";
"search_for_audios_playlists" = "Search for playlists"; "search_for_audios_playlists" = "Search for playlists";
"search_for_docs" = "Search for documents";
"search_button" = "Find"; "search_button" = "Find";
"search_placeholder" = "Start typing any name, title or word"; "search_placeholder" = "Start typing any name, title or word";
"results_zero" = "No results"; "results_zero" = "No results";
@ -1308,6 +1309,7 @@
"going_to_report_user" = "You are about to report this user."; "going_to_report_user" = "You are about to report this user.";
"going_to_report_video" = "You are about to report this video."; "going_to_report_video" = "You are about to report this video.";
"going_to_report_audio" = "You are about to report this audio."; "going_to_report_audio" = "You are about to report this audio.";
"going_to_report_doc" = "You are about to report this document.";
"going_to_report_post" = "You are about to report this post."; "going_to_report_post" = "You are about to report this post.";
"going_to_report_comment" = "You are about to report this comment."; "going_to_report_comment" = "You are about to report this comment.";
@ -2114,6 +2116,7 @@
"s_videos" = "Videos"; "s_videos" = "Videos";
"s_audios" = "Music"; "s_audios" = "Music";
"s_audios_playlists" = "Playlists"; "s_audios_playlists" = "Playlists";
"s_documents" = "Documents";
"s_by_people" = "for users"; "s_by_people" = "for users";
"s_by_groups" = "for groups"; "s_by_groups" = "for groups";
@ -2123,6 +2126,7 @@
"s_by_apps" = "for apps"; "s_by_apps" = "for apps";
"s_by_audios" = "for audios"; "s_by_audios" = "for audios";
"s_by_audios_playlists" = "for playlists"; "s_by_audios_playlists" = "for playlists";
"s_by_documents" = "for documents";
"s_order_by" = "Order by..."; "s_order_by" = "Order by...";
@ -2145,6 +2149,7 @@
"s_date_after" = "After"; "s_date_after" = "After";
"s_main" = "Main"; "s_main" = "Main";
"s_type" = "Type";
"s_now_on_site" = "now on site"; "s_now_on_site" = "now on site";
"s_with_photo" = "with photo"; "s_with_photo" = "with photo";
@ -2309,3 +2314,74 @@
"upd_in_general" = "Avatar update"; "upd_in_general" = "Avatar update";
"on_wall" = "On wall"; "on_wall" = "On wall";
"sign_short" = "Sign"; "sign_short" = "Sign";
/* Documents */
"my_documents" = "Documents";
"my_documents_objectively" = "My Documents";
"documents_of_group" = "Group's documents";
"search_by_documents" = "Search for documents..";
"documents" = "Documents";
"document_uploading_in_general" = "Upload document";
"document_editing_in_general" = "Edit document";
"file" = "File";
"tags" = "Tags";
"owner_is_hidden" = "Hide author";
"accessbility" = "Accessbility";
"download_file" = "Download file";
"remove" = "Remove";
"document" = "Document";
"documents_all" = "All documents";
"document_type_0" = "All";
"document_type_1" = "Text";
"document_type_2" = "Archives";
"document_type_3" = "GIF";
"document_type_4" = "Images";
"document_type_5" = "Audio";
"document_type_6" = "Video";
"document_type_7" = "Books";
"document_type_8" = "Another";
"documents_one" = "$1 document";
"documents_few" = "$1 documents";
"documents_many" = "$1 documents";
"documents_other" = "$1 documents";
"documents_zero" = "$1 documents";
"you_have_x_documents_one" = "You have $1 document";
"you_have_x_documents_few" = "You have $1 documents";
"you_have_x_documents_many" = "You have $1 documents";
"you_have_x_documents_other" = "You have $1 documents";
"you_have_x_documents_zero" = "You have $1 documents";
"group_has_x_documents_one" = "This group has $1 document";
"group_has_x_documents_few" = "This group has $1 documents";
"group_has_x_documents_many" = "This group has $1 documents";
"group_has_x_documents_other" = "This group has $1 documents";
"group_has_x_documents_zero" = "This group has $1 documents";
"x_documents_in_tab_one" = "$1 document at this tab";
"x_documents_in_tab_few" = "$1 documents at this tab";
"x_documents_in_tab_many" = "$1 documents at this tab";
"x_documents_in_tab_other" = "$1 documents at this tab";
"x_documents_in_tab_zero" = "$1 documents at this tab";
"there_is_no_documents_alright" = "There is no documents.";
"limitations_file_limit_size" = "File must not exceed $1 MB";
"limitations_file_allowed_formats" = "Allowed formats";
"limitations_file_author_rights" = "File must not violate copyright and site rules";
"select_file_fp" = "Select file";
"error_file_too_big" = "File is too big.";
"error_file_invalid_format" = "File format is not allowed.";
"error_file_adding_copied" = "File is already added.";
"error_file_preview" = "Error when uploading file: weird image.";
"private_document" = "Private (by link)";
"public_document" = "Public";
"documents_sort_add" = "By date";
"documents_sort_alphabet" = "A-Z";
"documents_sort_size" = "By size";
"select_doc" = "Attach document";
"no_documents" = "No documents found";
"go_to_my_documents" = "Go to own documents";

View file

@ -683,6 +683,7 @@
"search_for_notes" = "Поиск записок"; "search_for_notes" = "Поиск записок";
"search_for_audios" = "Поиск музыки"; "search_for_audios" = "Поиск музыки";
"search_for_audios_playlists" = "Поиск плейлистов"; "search_for_audios_playlists" = "Поиск плейлистов";
"search_for_docs" = "Поиск документов";
"search_button" = "Найти"; "search_button" = "Найти";
"search_placeholder" = "Начните вводить любое имя, название или слово"; "search_placeholder" = "Начните вводить любое имя, название или слово";
"results_zero" = "Ни одного результата"; "results_zero" = "Ни одного результата";
@ -1240,6 +1241,7 @@
"going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя."; "going_to_report_user" = "Вы собираетесь пожаловаться на данного пользователя.";
"going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись."; "going_to_report_video" = "Вы собираетесь пожаловаться на данную видеозапись.";
"going_to_report_audio" = "Вы собираетесь пожаловаться на данную аудиозапись."; "going_to_report_audio" = "Вы собираетесь пожаловаться на данную аудиозапись.";
"going_to_report_doc" = "Вы собираетесь пожаловаться на этот документ.";
"going_to_report_post" = "Вы собираетесь пожаловаться на данную запись."; "going_to_report_post" = "Вы собираетесь пожаловаться на данную запись.";
"going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий."; "going_to_report_comment" = "Вы собираетесь пожаловаться на данный комментарий.";
@ -2009,6 +2011,7 @@
"s_videos" = "Видео"; "s_videos" = "Видео";
"s_audios" = "Аудио"; "s_audios" = "Аудио";
"s_audios_playlists" = "Плейлисты"; "s_audios_playlists" = "Плейлисты";
"s_documents" = "Документы";
"s_by_people" = "по пользователям"; "s_by_people" = "по пользователям";
"s_by_groups" = "по группам"; "s_by_groups" = "по группам";
@ -2018,6 +2021,7 @@
"s_by_apps" = "по приложениям"; "s_by_apps" = "по приложениям";
"s_by_audios" = "по аудиозаписям"; "s_by_audios" = "по аудиозаписям";
"s_by_audios_playlists" = "по плейлистам"; "s_by_audios_playlists" = "по плейлистам";
"s_by_documents" = "по документам";
"s_order_by" = "Порядок"; "s_order_by" = "Порядок";
@ -2040,6 +2044,7 @@
"s_date_after" = "После"; "s_date_after" = "После";
"s_main" = "Основное"; "s_main" = "Основное";
"s_type" = "Тип";
"s_now_on_site" = "cейчас на сайте"; "s_now_on_site" = "cейчас на сайте";
"s_with_photo" = "с фото"; "s_with_photo" = "с фото";
@ -2204,3 +2209,74 @@
"upd_in_general" = "Обновление фотографии страницы"; "upd_in_general" = "Обновление фотографии страницы";
"on_wall" = "На стене"; "on_wall" = "На стене";
"sign_short" = "Подпись"; "sign_short" = "Подпись";
/* Documents */
"my_documents" = "Документы";
"my_documents_objectively" = "Мои Документы";
"documents_of_group" = "Документы группы";
"search_by_documents" = "Поиск по документам...";
"documents" = "Документы";
"document_uploading_in_general" = "Загрузка документа";
"document_editing_in_general" = "Редактирование документа";
"file" = "Файл";
"tags" = "Теги";
"owner_is_hidden" = "Автор скрыт";
"accessbility" = "Доступность";
"download_file" = "Скачать файл";
"remove" = "Удалить";
"document" = "Документ";
"documents_all" = "Все документы";
"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" = "Остальные";
"documents_one" = "$1 документ";
"documents_few" = "$1 документа";
"documents_many" = "$1 документов";
"documents_other" = "$1 документов";
"documents_zero" = "$1 документов";
"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" = "Здесь нет документов.";
"limitations_file_limit_size" = "Файл не должен превышать $1 МБ";
"limitations_file_allowed_formats" = "Разрешены следующие типы файлов";
"limitations_file_author_rights" = "Файл не должен нарушать авторские права и правила сайта";
"select_file_fp" = "Выбрать файл";
"error_file_too_big" = "Файл слишком большой.";
"error_file_invalid_format" = "Формат файла не разрешён.";
"error_file_adding_copied" = "Не удалось добавить файл; он уже добавлен.";
"error_file_preview" = "Не удалось загрузить файл: изображение имеет странности.";
"private_document" = "Приватный (по ссылке)";
"public_document" = "Публичный";
"documents_sort_add" = "По дате добавления";
"documents_sort_alphabet" = "A-Z/А-Я";
"documents_sort_size" = "По размеру";
"select_doc" = "Выбор документа";
"no_documents" = "Документов нет";
"go_to_my_documents" = "Перейти к своим документам";

View file

@ -22,6 +22,9 @@ openvk:
photoSaving: "quick" photoSaving: "quick"
videos: videos:
disableUploading: false disableUploading: false
docs:
maxSize: 10 # in megabytes
allowedFormats: ["jpg", "jpeg", "png", "gif", "webp", "psd", "aep", "docx", "doc", "odt", "txt", "md", "pptx", "ppt", "xls", "xlsx", "pdf", "djvu", "fb2", "ps", "apk", "zip", "7z", "mp4", "avi", "mp3", "flac"]
apps: apps:
withdrawTax: 8 withdrawTax: 8
security: security:

View file

@ -8,10 +8,14 @@ html {
body, body,
#backdropDripper, #backdropDripper,
#standaloneCommentBox, #standaloneCommentBox {
background-color: #0e0b1a;
color: #c6d2e8;
}
.ovk-photo-view, .ovk-photo-view,
.articleView { .articleView {
background-color: #0e0b1a; background-color: #100539;
color: #c6d2e8; color: #c6d2e8;
} }
@ -635,3 +639,24 @@ ul {
border: 1px solid #383052; border: 1px solid #383052;
background: #1e1b2a; background: #1e1b2a;
} }
#docs_page_wrapper .docs_page_search {
background: #1d1a27;
border-bottom: #2a2841 solid 1px;
}
.docListViewItem {
border-bottom-color: #2a2841;
}
.docListViewItem:hover, .attachButton:hover {
background: #271c48;
}
.docListViewItem .doc_icon {
background: #33255e;
}
.docListViewItem .doc_content b {
color: #7c94c5;
}