Merge branch 'master' into feature-reports

This commit is contained in:
Ilya Prokopenko 2021-12-05 11:28:59 +07:00 committed by GitHub
commit 47cfafc4c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
188 changed files with 18733 additions and 1158 deletions

6
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "locales"] [submodule "Web/static/img/oxygen-icons"]
path = locales path = Web/static/img/oxygen-icons
url = https://github.com/openvk/locales url = https://github.com/KDE/oxygen-icons5.git

View file

@ -11,7 +11,7 @@ RUN dnf -y module enable php:remi-7.4 && \
dnf -y module enable nodejs:14 dnf -y module enable nodejs:14
#And install dependencies: #And install dependencies:
RUN dnf -y install php php-cli php-common unzip php-zip php-yaml php-gd php-pdo_mysql nodejs git RUN dnf -y --skip-broken install php php-cli php-common unzip php-zip php-yaml php-gd php-pdo_mysql nodejs git
#Don't forget about Yarn and Composer: #Don't forget about Yarn and Composer:
RUN npm i -g yarn && \ RUN npm i -g yarn && \

View file

@ -1,18 +1,19 @@
h1. <img align="right" src="https://github.com/openvk/openvk/raw/master/Web/static/img/logo.png" alt="openvk" title="openvk" width="15%">OpenVK h1. <img align="right" src="https://github.com/openvk/openvk/raw/master/Web/static/img/logo_shadow.png" alt="openvk" title="openvk" width="15%">OpenVK
*OpenVK* is an attempt to create a simple CMS that -cosplays- imitates old VK. Code provided here is not stable yet. *OpenVK* is an attempt to create a simple CMS that -cosplays- imitates old VK. Code provided here is not stable yet.
VKontakte belongs to Pavel Durov and mail.ru. VKontakte belongs to Pavel Durov and VK Group.
To be honest, we don't even know whether it even works. However, this version is maintained and we will be happy to accept your bugreports "in our bug-tracker":https://github.com/openvk/openvk/projects/1. You should also be able to submit them using "ticketing system":https://openvk.su/support?act=new (you will need an OVK account for this). To be honest, we don't even know whether it even works. However, this version is maintained and we will be happy to accept your bugreports "in our bug-tracker":https://github.com/openvk/openvk/projects/1. You should also be able to submit them using "ticketing system":https://openvk.su/support?act=new (you will need an OVK account for this).
h2. When's the release? h2. When's the release?
Please use the master branch, as it has the most changes. Please use the master branch, as it has the most changes.
Updating the source code is done with this command: @git pull@ Updating the source code is done with this command: @git pull --recurse-submodules@
h2. Instances h2. Instances
* *"openvk.su":https://openvk.su/* * *"openvk.su":https://openvk.su/*
* "social.fetbuk.ru":http://social.fetbuk.ru/
h2. Can I create my own OpenVK instance? h2. Can I create my own OpenVK instance?
@ -35,11 +36,13 @@ PHP 8 has *not* yet been tested, so you should not expect it to work.
@ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/@ @ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions/enabled/@
# Import @install/init-static-db.sql@ to *same database* you installed Chandler to # Import @install/init-static-db.sql@ to *same database* you installed Chandler to
# Import @install/init-event-db.sql@ to *separate database* # Import @install/init-event-db.sql@ to *separate database*
# Copy openvk-example.yml to openvk.yml and change options # Copy @openvk-example.yml@ to @openvk.yml@ and change options
# Run @composer install@ in OpenVK directory # Run @composer install@ in OpenVK directory
# Move to @Web/static/js@ and execute @yarn install@ # Move to @Web/static/js@ and execute @yarn install@
# Set @openvk@ as your root app in @chandler.yml@ # Set @openvk@ as your root app in @chandler.yml@
*Note*: If OVK submodules were not downloaded beforehand (i.e. @--recursive@ was not used during cloning), this command *must be* executed in the @openvk@ folder: @git submodule update --init@
Once you are done, you can login as a system administrator on the network itself (no registration required): Once you are done, you can login as a system administrator on the network itself (no registration required):
* *Login*: admin@localhost.localdomain6 * *Login*: admin@localhost.localdomain6
* *Password*: admin * *Password*: admin
@ -62,3 +65,8 @@ You may reach out to us via:
*Attention*: bug tracker and telegram chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), *please use contact us directly*: *Attention*: bug tracker and telegram chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), *please use contact us directly*:
* *Head of OpenVK Security Commitee*: stingray@jill.pl or "@id155":https://t.me/id155 * *Head of OpenVK Security Commitee*: stingray@jill.pl or "@id155":https://t.me/id155
* *Backend developer*: "@saddyteirusu":https://t.me/saddyteirusu * *Backend developer*: "@saddyteirusu":https://t.me/saddyteirusu
Codeberg repository clone:
<a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">
</a>

View file

@ -0,0 +1,83 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Latte\Engine as TemplatingEngine;
use RdKafka\{Conf as RDKConf, KafkaConsumer};
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Notifications as N};
class Notifications implements Handler
{
protected $user;
protected $notifs;
function __construct(?User $user)
{
$this->user = $user;
$this->notifs = new N;
}
function ack(callable $resolve, callable $reject): void
{
$this->user->updateNotificationOffset();
$this->user->save();
$resolve("OK");
}
function fetch(callable $resolve, callable $reject): void
{
$kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"];
if(!$kafkaConf["enable"]) {
$reject(1999, "Disabled");
return;
}
$kafkaConf = $kafkaConf["kafka"];
$conf = new RDKConf();
$conf->set("metadata.broker.list", $kafkaConf["addr"] . ":" . $kafkaConf["port"]);
$conf->set("group.id", "UserFetch-" . $this->user->getId()); # Чтобы уведы приходили только на разные устройства одного чебупелика
$conf->set("auto.offset.reset", "latest");
set_time_limit(30);
$consumer = new KafkaConsumer($conf);
$consumer->subscribe([ $kafkaConf["topic"] ]);
while(true) {
$message = $consumer->consume(30*1000);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
$descriptor = $message->payload;
[,$user,] = explode(",", $descriptor);
if(((int) $user) === $this->user->getId()) {
$data = (object) [];
$notification = $this->notifs->fromDescriptor($descriptor, $data);
if(!$notification) {
$reject(1982, "Server Error");
return;
}
$tplDir = __DIR__ . "/../Web/Presenters/templates/components/notifications/";
$tplId = "$tplDir$data->actionCode/_$data->originModelType" . "_" . $data->targetModelType . "_.xml";
$latte = new TemplatingEngine;
$latte->setTempDirectory(CHANDLER_ROOT . "/tmp/cache/templates");
$latte->addFilter("translate", fn($trId) => tr($trId));
$resolve([
"title" => tr("notif_" . $data->actionCode . "_" . $data->originModelType . "_" . $data->targetModelType),
"body" => trim(preg_replace('%(\s){2,}%', "$1", $latte->renderToString($tplId, ["notification" => $notification]))),
"ava" => $notification->getModel(1)->getAvatarUrl(),
"priority" => 1,
]);
return;
}
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
$reject(1983, "Nothing to report");
break 2;
default:
$reject(1981, "Kafka Error: " . $message->errstr());
break 2;
}
}
}
}

View file

@ -14,7 +14,7 @@ class Service implements Handler
function getTime(callable $resolve, callable $reject): void function getTime(callable $resolve, callable $reject): void
{ {
$resolve((new DateTime)->format("%e %B %G" . tr("time_at_sp") . "%X")); $resolve(trim((new DateTime)->format("%e %B %G" . tr("time_at_sp") . "%X")));
} }
function getServerVersion(callable $resolve, callable $reject): void function getServerVersion(callable $resolve, callable $reject): void

58
ServiceAPI/Wall.php Normal file
View file

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Posts;
class Wall implements Handler
{
protected $user;
protected $posts;
function __construct(?User $user)
{
$this->user = $user;
$this->posts = new Posts;
}
function getPost(int $id, callable $resolve, callable $reject): void
{
$post = $this->posts->get($id);
if(!$post || $post->isDeleted())
$reject("No post with id=$id");
$res = (object) [];
$res->id = $post->getId();
$res->wall = $post->getTargetWall();
$res->author = (($owner = $post->getOwner())) instanceof User
? ($owner->getId())
: ($owner->getId() * -1);
if($post->isSigned())
$res->signedOffBy = $post->getOwnerPost();
$res->pinned = $post->isPinned();
$res->sponsored = $post->isAd();
$res->nsfw = $post->isExplicit();
$res->text = $post->getText();
$res->likes = [
"count" => $post->getLikesCount(),
"hasLike" => $post->hasLikeFrom($this->user),
"likedBy" => [],
];
foreach($post->getLikers() as $liker) {
$res->likes["likedBy"][] = [
"id" => $liker->getId(),
"url" => $liker->getURL(),
"name" => $liker->getCanonicalName(),
"avatar" => $liker->getAvatarURL(),
];
}
$res->created = (string) $post->getPublicationTime();
$res->canPin = $post->canBePinnedBy($this->user);
$res->canEdit = $res->canDelete = $post->canBeDeletedBy($this->user);
$resolve((array) $res);
}
}

View file

@ -42,7 +42,7 @@ final class Account extends VKAPIRequestHandler
]; ];
} }
function setOnline(): object function setOnline(): int
{ {
$this->requireUser(); $this->requireUser();

22
VKAPI/Handlers/Audio.php Normal file
View file

@ -0,0 +1,22 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
final class Audio extends VKAPIRequestHandler
{
function get(): object
{
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
return (object) [
"count" => 1,
"items" => [(object) [
"id" => 1,
"owner_id" => 1,
"artist" => "В ОВК ПОКА НЕТ МУЗЫКИ",
"title" => "ЖДИТЕ :)))",
"duration" => 22,
"url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3"
]]
];
}
}

View file

@ -3,19 +3,35 @@ namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Entities\Clubs; use openvk\Web\Models\Entities\Clubs;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo; use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Post; use openvk\Web\Models\Entities\Post;
use openvk\Web\Models\Entities\Postable; use openvk\Web\Models\Entities\Postable;
use openvk\Web\Models\Repositories\Posts as PostsRepo; use openvk\Web\Models\Repositories\Posts as PostsRepo;
final class Groups extends VKAPIRequestHandler final class Groups extends VKAPIRequestHandler
{ {
function get(string $group_ids, string $fields = "", int $offset = 0, int $count = 100, bool $online = false): array function get(int $user_id = 0, string $fields = "", int $offset = 0, int $count = 6, bool $online = false): object
{ {
$this->requireUser(); $this->requireUser();
$clubs = new ClubsRepo; if ($user_id == 0) {
$clbs = explode(',', $group_ids); foreach($this->getUser()->getClubs($offset+1) as $club) {
$response; $clbs[] = $club;
}
$clbsCount = $this->getUser()->getClubCount();
} else {
$users = new UsersRepo;
$user = $users->get($user_id);
if (is_null($user)) {
$this->fail(15, "Access denied");
}
foreach($user->getClubs($offset+1) as $club) {
$clbs[] = $club;
}
$clbsCount = $user->getClubCount();
}
$rClubs;
$ic = sizeof($clbs); $ic = sizeof($clbs);
@ -24,22 +40,21 @@ final class Groups extends VKAPIRequestHandler
$clbs = array_slice($clbs, $offset * $count); $clbs = array_slice($clbs, $offset * $count);
for ($i=0; $i < $ic; $i++) { for ($i=0; $i < $ic; $i++) {
$usr = $clubs->get((int) $clbs[$i]); $usr = $clbs[$i];
if(is_null($usr)) if(is_null($usr))
{ {
$response[$i] = (object)[ $rClubs[$i] = (object)[
"id" => $clbs[$i], "id" => $clbs[$i],
"first_name" => "DELETED", "name" => "DELETED",
"last_name" => "",
"deactivated" => "deleted" "deactivated" => "deleted"
]; ];
}else if($clbs[$i] == null){ }else if($clbs[$i] == null){
}else{ }else{
$response[$i] = (object)[ $rClubs[$i] = (object)[
"id" => $usr->getId(), "id" => $usr->getId(),
"first_name" => $usr->getFirstName(), "name" => $usr->getName(),
"last_name" => $usr->getLastName(), "screen_name" => $usr->getShortCode(),
"is_closed" => false, "is_closed" => false,
"can_access_closed" => true, "can_access_closed" => true,
]; ];
@ -49,34 +64,28 @@ final class Groups extends VKAPIRequestHandler
foreach($flds as $field) { foreach($flds as $field) {
switch ($field) { switch ($field) {
case 'verified': case 'verified':
$response[$i]->verified = intval($usr->isVerified()); $rClubs[$i]->verified = intval($usr->isVerified());
break;
case 'sex':
$response[$i]->sex = $this->getUser()->isFemale() ? 1 : 2;
break; break;
case 'has_photo': case 'has_photo':
$response[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1; $rClubs[$i]->has_photo = is_null($usr->getAvatarPhoto()) ? 0 : 1;
break; break;
case 'photo_max_orig': case 'photo_max_orig':
$response[$i]->photo_max_orig = $usr->getAvatarURL(); $rClubs[$i]->photo_max_orig = $usr->getAvatarURL();
break; break;
case 'photo_max': case 'photo_max':
$response[$i]->photo_max = $usr->getAvatarURL(); $rClubs[$i]->photo_max = $usr->getAvatarURL();
break; break;
case 'members_count':
$rClubs[$i]->members_count = $usr->getFollowersCount();
break;
}
}
} }
} }
// НУЖЕН фикс - либо из-за моего дебилизма, либо из-за сегментации котлеток некоторые пользовали отображаются как онлайн, хотя лол, если зайти на страницу, то оный уже офлайн return (object) [
if($online == true && $usr->getOnline()->timestamp() + 2505600 > time()) { "count" => $clbsCount,
$response[$i]->online = 1; "items" => $rClubs
}else{ ];
$response[$i]->online = 0;
}
}
}
return $response;
} }
} }

View file

@ -5,11 +5,14 @@ use openvk\Web\Models\Repositories\Users as UsersRepo;
final class Users extends VKAPIRequestHandler final class Users extends VKAPIRequestHandler
{ {
function get(string $user_ids, string $fields = "", int $offset = 0, int $count = 100): array function get(string $user_ids = "0", string $fields = "", int $offset = 0, int $count = 100): array
{ {
$this->requireUser(); $this->requireUser();
$users = new UsersRepo; $users = new UsersRepo;
if($user_ids == "0")
$user_ids = (string) $this->getUser()->getId();
$usrs = explode(',', $user_ids); $usrs = explode(',', $user_ids);
$response; $response;
@ -60,11 +63,34 @@ final class Users extends VKAPIRequestHandler
$response[$i]->photo_max = $usr->getAvatarURL(); $response[$i]->photo_max = $usr->getAvatarURL();
break; break;
case 'status': case 'status':
if($usr->getStatus() != null)
$response[$i]->status = $usr->getStatus(); $response[$i]->status = $usr->getStatus();
break; break;
case 'screen_name': case 'screen_name':
if($usr->getShortCode() != null)
$response[$i]->screen_name = $usr->getShortCode(); $response[$i]->screen_name = $usr->getShortCode();
break; break;
case 'friend_status':
switch($usr->getSubscriptionStatus($this->getUser())) {
case 3:
case 0:
$response[$i]->friend_status = $usr->getSubscriptionStatus($this->getUser());
break;
case 1:
$response[$i]->friend_status = 2;
break;
case 2:
$response[$i]->friend_status = 1;
break;
}
break;
case 'last_seen':
if ($usr->onlineStatus() == 0) {
$response[$i]->last_seen = (object) [
"platform" => 1,
"time" => $usr->getOnline()->timestamp()
];
}
case 'music': case 'music':
$response[$i]->music = $usr->getFavoriteMusic(); $response[$i]->music = $usr->getFavoriteMusic();
break; break;
@ -86,8 +112,7 @@ final class Users extends VKAPIRequestHandler
} }
} }
// НУЖЕН фикс - либо из-за моего дебилизма, либо из-за сегментации котлеток некоторые пользовали отображаются как онлайн, хотя лол, если зайти на страницу, то оный уже офлайн if($usr->getOnline()->timestamp() + 300 > time()) {
if($usr->getOnline()->timestamp() + 2505600 > time()) {
$response[$i]->online = 1; $response[$i]->online = 1;
}else{ }else{
$response[$i]->online = 0; $response[$i]->online = 0;

View file

@ -16,20 +16,25 @@ final class Wall extends VKAPIRequestHandler
$posts = new PostsRepo; $posts = new PostsRepo;
$items = []; $items = [];
$profiles = [];
$groups = [];
$count = $posts->getPostCountOnUserWall((int) $owner_id);
foreach ($posts->getPostsFromUsersWall((int)$owner_id) as $post) { foreach ($posts->getPostsFromUsersWall((int)$owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
$items[] = (object)[ $items[] = (object)[
"id" => $post->getVirtualId(), "id" => $post->getVirtualId(),
"from_id" => $post->getOwner()->getId(), "from_id" => $from_id,
"owner_id" => $post->getTargetWall(), "owner_id" => $post->getTargetWall(),
"date" => $post->getPublicationTime()->timestamp(), "date" => $post->getPublicationTime()->timestamp(),
"post_type" => "post", "post_type" => "post",
"text" => $post->getText(), "text" => $post->getText(),
"can_edit" => 0, // TODO "can_edit" => 0, // TODO
"can_delete" => $post->canBeDeletedBy($this->getUser()), "can_delete" => $post->canBeDeletedBy($this->getUser()),
"can_pin" => 0, // TODO "can_pin" => $post->canBePinnedBy($this->getUser()),
"can_archive" => false, // TODO MAYBE "can_archive" => false, // TODO MAYBE
"is_archived" => false, "is_archived" => false,
"is_pinned" => $post->isPinned(),
"post_source" => (object)["type" => "vk"], "post_source" => (object)["type" => "vk"],
"comments" => (object)[ "comments" => (object)[
"count" => $post->getCommentsCount(), "count" => $post->getCommentsCount(),
@ -46,26 +51,66 @@ final class Wall extends VKAPIRequestHandler
"user_reposted" => 0 "user_reposted" => 0
] ]
]; ];
if ($from_id > 0)
$profiles[] = $from_id;
else
$groups[] = $from_id * -1;
} }
$profiles = [];
$groups = [];
$groups[0] = 'lol';
$groups[2] = 'cec';
if($extended == 1) if($extended == 1)
return (object)[ {
"items" => (array)$items, $profiles = array_unique($profiles);
"cock" => (array)$groups $groups = array_unique($groups);
$profilesFormatted = [];
$groupsFormatted = [];
foreach ($profiles as $prof) {
$user = (new UsersRepo)->get($prof);
$profilesFormatted[] = (object)[
"first_name" => $user->getFirstName(),
"id" => $user->getId(),
"last_name" => $user->getLastName(),
"can_access_closed" => false,
"is_closed" => false,
"sex" => $user->isFemale() ? 1 : 2,
"screen_name" => $user->getShortCode(),
"photo_50" => $user->getAvatarUrl(),
"photo_100" => $user->getAvatarUrl(),
"online" => $user->isOnline()
]; ];
}
foreach($groups as $g) {
$group = (new ClubsRepo)->get($g);
$groupsFormatted[] = (object)[
"id" => $group->getId(),
"name" => $group->getName(),
"screen_name" => $group->getShortCode(),
"is_closed" => 0,
"type" => "group",
"photo_50" => $group->getAvatarUrl(),
"photo_100" => $group->getAvatarUrl(),
"photo_200" => $group->getAvatarUrl(),
];
}
return (object)[
"count" => $count,
"items" => (array)$items,
"profiles" => (array)$profilesFormatted,
"groups" => (array)$groupsFormatted
];
}
else else
return (object)[ return (object)[
"count" => $count,
"items" => (array)$items "items" => (array)$items
]; ];
} }
function post(string $owner_id, string $message, int $from_group = 0): object function post(string $owner_id, string $message, int $from_group = 0, int $signed = 0): object
{ {
$this->requireUser(); $this->requireUser();
@ -88,6 +133,8 @@ final class Wall extends VKAPIRequestHandler
$flags = 0; $flags = 0;
if($from_group == 1) if($from_group == 1)
$flags |= 0b10000000; $flags |= 0b10000000;
if($signed == 1)
$flags |= 0b01000000;
try { try {
$post = new Post; $post = new Post;

View file

@ -90,6 +90,21 @@ class Club extends RowModel
return (new Users)->get($this->getRecord()->owner); return (new Users)->get($this->getRecord()->owner);
} }
function getOwnerComment(): string
{
return is_null($this->getRecord()->owner_comment) ? "" : $this->getRecord()->owner_comment;
}
function isOwnerHidden(): bool
{
return (bool) $this->getRecord()->owner_hidden;
}
function isOwnerClubPinned(): bool
{
return (bool) $this->getRecord()->owner_club_pinned;
}
function getDescription(): ?string function getDescription(): ?string
{ {
return $this->getRecord()->about; return $this->getRecord()->about;
@ -110,6 +125,11 @@ class Club extends RowModel
return $this->getRecord()->closed; return $this->getRecord()->closed;
} }
function getAdministratorsListDisplay(): int
{
return $this->getRecord()->administrators_list_display;
}
function getType(): int function getType(): int
{ {
return $this->getRecord()->type; return $this->getRecord()->type;
@ -259,21 +279,11 @@ class Club extends RowModel
} }
} }
function getManagers(int $page = 1): \Traversable function getManagers(int $page = 1, bool $ignoreHidden = false): \Traversable
{ {
$rels = $this->getRecord()->related("group_coadmins.club")->page($page, 6); $rels = $this->getRecord()->related("group_coadmins.club")->page($page, 6);
if($ignoreHidden)
foreach($rels as $rel) { $rels = $rels->where("hidden", false);
$rel = (new Users)->get($rel->user);
if(!$rel) continue;
yield $rel;
}
}
function getManagersWithComment(int $page = 1): \Traversable
{
$rels = $this->getRecord()->related("group_coadmins.club")->where("comment IS NOT NULL")->page($page, 10);
foreach($rels as $rel) { foreach($rels as $rel) {
$rel = (new Managers)->get($rel->id); $rel = (new Managers)->get($rel->id);
@ -283,14 +293,22 @@ class Club extends RowModel
} }
} }
function getManagersCount(): int function getManager(User $user, bool $ignoreHidden = false): ?Manager
{ {
return sizeof($this->getRecord()->related("group_coadmins.club")) + 1; $manager = (new Managers)->getByUserAndClub($user->getId(), $this->getId());
if ($ignoreHidden && $manager !== null && $manager->isHidden())
return null;
return $manager;
} }
function getManagersCountWithComment(): int function getManagersCount(bool $ignoreHidden = false): int
{ {
return sizeof($this->getRecord()->related("group_coadmins.club")->where("comment IS NOT NULL")) + 1; if($ignoreHidden)
return sizeof($this->getRecord()->related("group_coadmins.club")->where("hidden", false)) + (int) !$this->isOwnerHidden();
return sizeof($this->getRecord()->related("group_coadmins.club")) + 1;
} }
function addManager(User $user, ?string $comment = NULL): void function addManager(User $user, ?string $comment = NULL): void
@ -319,5 +337,10 @@ class Club extends RowModel
return !is_null($this->getRecord()->related("group_coadmins.club")->where("user", $id)->fetch()); return !is_null($this->getRecord()->related("group_coadmins.club")->where("user", $id)->fetch());
} }
function getWebsite(): ?string
{
return $this->getRecord()->website;
}
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -1,5 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\RowModel;
class Comment extends Post class Comment extends Post
{ {
@ -24,4 +26,19 @@ class Comment extends Post
return $entity; return $entity;
} }
/**
* May return fake owner (group), if flags are [1, (*)]
*
* @param bool $honourFlags - check flags
*/
function getOwner(bool $honourFlags = true, bool $real = false): RowModel
{
if($honourFlags && $this->isPostedOnBehalfOfGroup()) {
if($this->getTarget() instanceof Post)
return (new Clubs)->get(abs($this->getTarget()->getTargetWall()));
}
return parent::getOwner($honourFlags, $real);
}
} }

View file

@ -0,0 +1,164 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\User;
use Nette\Utils\{Image, ImageException};
class Gift extends RowModel
{
const IMAGE_MAXSIZE = 131072;
const IMAGE_BINARY = 0;
const IMAGE_BASE64 = 1;
const IMAGE_URL = 2;
const PERIOD_IGNORE = 0;
const PERIOD_SET = 1;
const PERIOD_SET_IF_NONE = 2;
protected $tableName = "gifts";
function getName(): string
{
return $this->getRecord()->internal_name;
}
function getPrice(): int
{
return $this->getRecord()->price;
}
function getUsages(): int
{
return $this->getRecord()->usages;
}
function getUsagesBy(User $user, ?int $since = NULL): int
{
$sent = $this->getRecord()
->related("gift_user_relations.gift")
->where("sender", $user->getId())
->where("sent >= ?", $since ?? $this->getRecord()->limit_period ?? 0);
return sizeof($sent);
}
function getUsagesLeft(User $user): float
{
if($this->getLimit() === INF)
return INF;
return max(0, $this->getLimit() - $this->getUsagesBy($user));
}
function getImage(int $type = 0): /* ?binary */ string
{
switch($type) {
default:
case static::IMAGE_BINARY:
return $this->getRecord()->image ?? "";
break;
case static::IMAGE_BASE64:
return "data:image/png;base64," . base64_encode($this->getRecord()->image ?? "");
break;
case static::IMAGE_URL:
return "/gift" . $this->getId() . "_" . $this->getUpdateDate()->timestamp() . ".png";
break;
}
}
function getLimit(): float
{
$limit = $this->getRecord()->limit;
return !$limit ? INF : (float) $limit;
}
function getLimitResetTime(): ?DateTime
{
return is_null($t = $this->getRecord()->limit_period) ? NULL : new DateTime($t);
}
function getUpdateDate(): DateTime
{
return new DateTime($this->getRecord()->updated);
}
function canUse(User $user): bool
{
return $this->getUsagesLeft($user) > 0;
}
function isFree(): bool
{
return $this->getPrice() === 0;
}
function used(): void
{
$this->stateChanges("usages", $this->getUsages() + 1);
$this->save();
}
function setName(string $name): void
{
$this->stateChanges("internal_name", $name);
}
function setImage(string $file): bool
{
$imgBlob;
try {
$image = Image::fromFile($file);
$image->resize(512, 512, Image::SHRINK_ONLY);
$imgBlob = $image->toString(Image::PNG);
} catch(ImageException $ex) {
return false;
}
if(strlen($imgBlob) > (2**24 - 1)) {
return false;
} else {
$this->stateChanges("updated", time());
$this->stateChanges("image", $imgBlob);
}
return true;
}
function setLimit(?float $limit = NULL, int $periodBehaviour = 0): void
{
$limit ??= $this->getLimit();
$limit = $limit === INF ? NULL : (int) $limit;
$this->stateChanges("limit", $limit);
if(!$limit) {
$this->stateChanges("limit_period", NULL);
return;
}
switch($periodBehaviour) {
default:
case static::PERIOD_IGNORE:
break;
case static::PERIOD_SET:
$this->stateChanges("limit_period", time());
break;
case static::PERIOD_SET_IF_NONE:
if(is_null($this->getRecord()) || is_null($this->getRecord()->limit_period))
$this->stateChanges("limit_period", time());
break;
}
}
function delete(bool $softly = true): void
{
$this->getRecord()->related("gift_relations.gift")->delete();
parent::delete($softly);
}
}

View file

@ -0,0 +1,156 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Gifts;
use openvk\Web\Models\RowModel;
use Transliterator;
class GiftCategory extends RowModel
{
protected $tableName = "gift_categories";
private function getLocalization(string $language): object
{
return $this->getRecord()
->related("gift_categories_locales.category")
->where("language", $language);
}
private function createLocalizationIfNotExists(string $language): void
{
if(!is_null($this->getLocalization($language)->fetch()))
return;
DB::i()->getContext()->table("gift_categories_locales")->insert([
"category" => $this->getId(),
"language" => $language,
"name" => "Sample Text",
"description" => "Sample Text",
]);
}
function getSlug(): string
{
return str_replace("ʹ", "-", Transliterator::createFromRules(
":: Any-Latin;"
. ":: NFD;"
. ":: [:Nonspacing Mark:] Remove;"
. ":: NFC;"
. ":: [:Punctuation:] Remove;"
. ":: Lower();"
. "[:Separator:] > '-'"
)->transliterate($this->getName()));
}
function getThumbnailURL(): string
{
$primeGift = iterator_to_array($this->getGifts(1, 1))[0];
$serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"];
if(!$primeGift)
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
return $primeGift->getImage(Gift::IMAGE_URL);
}
function getName(string $language = "_", bool $returnNull = false): ?string
{
$loc = $this->getLocalization($language)->fetch();
if(!$loc) {
if($returnNull)
return NULL;
return $language === "_" ? "Unlocalized" : $this->getName();
}
return $loc->name;
}
function getDescription(string $language = "_", bool $returnNull = false): ?string
{
$loc = $this->getLocalization($language)->fetch();
if(!$loc) {
if($returnNull)
return NULL;
return $language === "_" ? "Unlocalized" : $this->getDescription();
}
return $loc->description;
}
function getGifts(int $page = -1, ?int $perPage = NULL, &$count = nullptr): \Traversable
{
$gifts = $this->getRecord()->related("gift_relations.category");
if($page !== -1) {
$count = $gifts->count();
$gifts = $gifts->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
}
foreach($gifts as $rel)
yield (new Gifts)->get($rel->gift);
}
function isMagical(): bool
{
return !is_null($this->getRecord()->autoquery);
}
function hasGift(Gift $gift): bool
{
$rels = $this->getRecord()->related("gift_relations.category");
return $rels->where("gift", $gift->getId())->count() > 0;
}
function addGift(Gift $gift): void
{
if($this->hasGift($gift))
return;
DB::i()->getContext()->table("gift_relations")->insert([
"category" => $this->getId(),
"gift" => $gift->getId(),
]);
}
function removeGift(Gift $gift): void
{
if(!$this->hasGift($gift))
return;
DB::i()->getContext()->table("gift_relations")->where([
"category" => $this->getId(),
"gift" => $gift->getId(),
])->delete();
}
function setName(string $language, string $name): void
{
$this->createLocalizationIfNotExists($language);
$this->getLocalization($language)->update([
"name" => $name,
]);
}
function setDescription(string $language, string $description): void
{
$this->createLocalizationIfNotExists($language);
$this->getLocalization($language)->update([
"description" => $description,
]);
}
function setAutoQuery(?array $query = NULL): void
{
if(is_null($query)) {
$this->stateChanges("autoquery", NULL);
return;
}
$allowedColumns = ["price", "usages"];
if(array_diff_key($query, array_flip($allowedColumns)))
throw new \LogicException("Invalid query");
$this->stateChanges("autoquery", serialize($query));
}
}

View file

@ -17,7 +17,7 @@ class Manager extends RowModel
return $this->getRecord()->id; return $this->getRecord()->id;
} }
function getUserId(): string function getUserId(): int
{ {
return $this->getRecord()->user; return $this->getRecord()->user;
} }
@ -27,7 +27,7 @@ class Manager extends RowModel
return (new Users)->get($this->getRecord()->user); return (new Users)->get($this->getRecord()->user);
} }
function getClubId(): string function getClubId(): int
{ {
return $this->getRecord()->club; return $this->getRecord()->club;
} }
@ -42,5 +42,15 @@ class Manager extends RowModel
return is_null($this->getRecord()->comment) ? "" : $this->getRecord()->comment; return is_null($this->getRecord()->comment) ? "" : $this->getRecord()->comment;
} }
function isHidden(): bool
{
return (bool) $this->getRecord()->hidden;
}
function isClubPinned(): bool
{
return (bool) $this->getRecord()->club_pinned;
}
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -89,4 +89,21 @@ abstract class Media extends Postable
$this->stateChanges("hash", $hash); $this->stateChanges("hash", $hash);
} }
function delete(bool $softly = true): void
{
$deleteQuirk = ovkGetQuirk("blobs.erase-upon-deletion");
if($deleteQuirk === 2 || ($deleteQuirk === 1 && !$softly))
@unlink($this->getFileName());
parent::delete($softly);
}
function undelete(): void
{
if(ovkGetQuirk("blobs.erase-upon-deletion") === 2)
throw new \LogicException("Can't undelete model which is tied to blob, because of config constraint (quriks.yml:blobs.erase-upon-deletion)");
parent::undelete();
}
} }

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\Entities\{User, Gift};
final class GiftNotification extends Notification
{
protected $actionCode = 9601;
function __construct(User $receiver, User $sender, Gift $gift, ?string $comment)
{
parent::__construct($receiver, $gift, $sender, time(), $comment ?? "");
}
}

View file

@ -3,6 +3,7 @@ namespace openvk\Web\Models\Entities\Notifications;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User}; use openvk\Web\Models\Entities\{User};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use RdKafka\{Conf, Producer};
class Notification class Notification
{ {
@ -80,14 +81,14 @@ class Notification
return false; return false;
$data = [ $data = [
$this->recipient->getId(), "recipient" => $this->recipient->getId(),
$this->encodeType($this->originModel), "originModelType" => $this->encodeType($this->originModel),
$this->originModel->getId(), "originModelId" => $this->originModel->getId(),
$this->encodeType($this->targetModel), "targetModelType" => $this->encodeType($this->targetModel),
$this->targetModel->getId(), "targetModelId" => $this->targetModel->getId(),
$this->actionCode, "actionCode" => $this->actionCode,
$this->data, "additionalPayload" => $this->data,
$this->time, "timestamp" => $this->time,
]; ];
$edb = $e->getConnection(); $edb = $e->getConnection();
@ -96,12 +97,33 @@ class Notification
$query = <<<'QUERY' $query = <<<'QUERY'
SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?) SELECT * FROM `notifications` WHERE `recipientType`=0 AND `recipientId`=? AND `originModelType`=? AND `originModelId`=? AND `targetModelType`=? AND `targetModelId`=? AND `modelAction`=? AND `additionalData`=? AND `timestamp` > (? - ?)
QUERY; QUERY;
$result = $edb->query($query, ...array_merge($data, [ $this->threshold ])); $result = $edb->query($query, ...array_merge(array_values($data), [ $this->threshold ]));
if($result->getRowCount() > 0) if($result->getRowCount() > 0)
return false; return false;
} }
$edb->query("INSERT INTO notifications VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?)", ...$data); $edb->query("INSERT INTO notifications VALUES (0, ?, ?, ?, ?, ?, ?, ?, ?)", ...array_values($data));
$kafkaConf = OPENVK_ROOT_CONF["openvk"]["credentials"]["notificationsBroker"];
if($kafkaConf["enable"]) {
$kafkaConf = $kafkaConf["kafka"];
$brokerConf = new Conf();
$brokerConf->set("log_level", (string) LOG_DEBUG);
$brokerConf->set("debug", "all");
$producer = new Producer($brokerConf);
$producer->addBrokers($kafkaConf["addr"] . ":" . $kafkaConf["port"]);
$descriptor = implode(",", [
str_replace("\\", ".", get_class($this)),
$this->recipient->getId(),
base64_encode(serialize((object) $data)),
]);
$notifTopic = $producer->newTopic($kafkaConf["topic"]);
$notifTopic->produce(RD_KAFKA_PARTITION_UA, RD_KAFKA_MSG_F_BLOCK, $descriptor);
$producer->flush(100);
}
return true; return true;
} }

View file

@ -9,11 +9,13 @@ class Photo extends Media
protected $tableName = "photos"; protected $tableName = "photos";
protected $fileExtension = "jpeg"; protected $fileExtension = "jpeg";
const ALLOWED_SIDE_MULTIPLIER = 7;
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
$image = Image::fromFile($filename); $image = Image::fromFile($filename);
if(($image->height >= ($image->width * pi())) || ($image->width >= ($image->height * pi()))) if(($image->height >= ($image->width * Photo::ALLOWED_SIDE_MULTIPLIER)) || ($image->width >= ($image->height * Photo::ALLOWED_SIDE_MULTIPLIER)))
throw new ISE("Invalid layout: expected layout that matches (x, ?!>3x)"); throw new ISE("Invalid layout: image is too wide/short");
$image->save($this->pathFromHash($hash), 92, Image::JPEG); $image->save($this->pathFromHash($hash), 92, Image::JPEG);
@ -41,4 +43,20 @@ class Photo extends Media
DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete(); DB::i()->getContext()->table("album_relations")->where("media", $this->getRecord()->id)->delete();
} }
static function fastMake(int $owner, string $description = "", array $file, ?Album $album = NULL, bool $anon = false): Photo
{
$photo = new static;
$photo->setOwner($owner);
$photo->setDescription(iconv_substr($description, 0, 36) . "...");
$photo->setAnonymous($anon);
$photo->setCreated(time());
$photo->setFile($file);
$photo->save();
if(!is_null($album))
$album->addPhoto($photo);
return $photo;
}
} }

View file

@ -3,25 +3,47 @@ namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Notifications\LikeNotification;
class Post extends Postable class Post extends Postable
{ {
protected $tableName = "posts"; protected $tableName = "posts";
protected $upperNodeReferenceColumnName = "wall"; protected $upperNodeReferenceColumnName = "wall";
private function setLikeRecursively(bool $liked, User $user, int $depth): void
{
$searchData = [
"origin" => $user->getId(),
"model" => static::class,
"target" => $this->getRecord()->id,
];
if((sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0) !== $liked) {
if($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment)
(new LikeNotification($this->getOwner(false), $this, $user))->emit();
parent::setLike($liked, $user);
}
if($depth < ovkGetQuirk("wall.repost-liking-recursion-limit"))
foreach($this->getChildren() as $attachment)
if($attachment instanceof Post)
$attachment->setLikeRecursively($liked, $user, $depth + 1);
}
/** /**
* May return fake owner (group), if flags are [1, (*)] * May return fake owner (group), if flags are [1, (*)]
* *
* @param bool $honourFlags - check flags * @param bool $honourFlags - check flags
*/ */
function getOwner(bool $honourFlags = true): RowModel function getOwner(bool $honourFlags = true, bool $real = false): RowModel
{ {
if($honourFlags && ( ($this->getRecord()->flags & 0b10000000) > 0 )) { if($honourFlags && $this->isPostedOnBehalfOfGroup()) {
if($this->getRecord()->wall < 0) if($this->getRecord()->wall < 0)
return (new Clubs)->get(abs($this->getRecord()->wall)); return (new Clubs)->get(abs($this->getRecord()->wall));
} }
return parent::getOwner(); return parent::getOwner($real);
} }
function getPrettyId(): string function getPrettyId(): string
@ -75,7 +97,7 @@ class Post extends Postable
function getOwnerPost(): int function getOwnerPost(): int
{ {
return $this->getRecord()->owner; return $this->getOwner(false)->getId();
} }
function pin(): void function pin(): void
@ -122,6 +144,25 @@ class Post extends Postable
$this->stateChanges("content", $content); $this->stateChanges("content", $content);
} }
function toggleLike(User $user): bool
{
$liked = parent::toggleLike($user);
if($this->getOwner(false)->getId() !== $user->getId() && !($this->getOwner() instanceof Club) && !$this instanceof Comment)
(new LikeNotification($this->getOwner(false), $this, $user))->emit();
foreach($this->getChildren() as $attachment)
if($attachment instanceof Post)
$attachment->setLikeRecursively($liked, $user, 2);
return $liked;
}
function setLike(bool $liked, User $user): void
{
$this->setLikeRecursively($liked, $user, 1);
}
function deletePost(): void function deletePost(): void
{ {
$this->setDeleted(1); $this->setDeleted(1);

View file

@ -29,9 +29,12 @@ abstract class Postable extends Attachable
return DB::i()->getContext()->table($this->tableName); return DB::i()->getContext()->table($this->tableName);
} }
function getOwner(): RowModel function getOwner(bool $real = false): RowModel
{ {
$oid = (int) $this->getRecord()->owner; $oid = (int) $this->getRecord()->owner;
if(!$real && $this->isAnonymous())
$oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"];
if($oid > 0) if($oid > 0)
return (new Users)->get($oid); return (new Users)->get($oid);
else else
@ -71,9 +74,9 @@ abstract class Postable extends Attachable
return (new Comments)->getCommentsCountByTarget($this); return (new Comments)->getCommentsCountByTarget($this);
} }
function getLastComments() function getLastComments(int $count): \Traversable
{ {
return (new Comments)->getLastCommentsByTarget($this); return (new Comments)->getLastCommentsByTarget($this, $count);
} }
function getLikesCount(): int function getLikesCount(): int
@ -84,17 +87,52 @@ abstract class Postable extends Attachable
])); ]));
} }
function toggleLike(User $user): void // TODO add pagination
function getLikers(): \Traversable
{
$sel = DB::i()->getContext()->table("likes")->where([
"model" => static::class,
"target" => $this->getRecord()->id,
]);
foreach($sel as $like)
yield (new Users)->get($like->origin);
}
function isAnonymous(): bool
{
return (bool) $this->getRecord()->anonymous;
}
function toggleLike(User $user): bool
{ {
$searchData = [ $searchData = [
"origin" => $user->getId(), "origin" => $user->getId(),
"model" => static::class, "model" => static::class,
"target" => $this->getRecord()->id, "target" => $this->getRecord()->id,
]; ];
if(sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0)
if(sizeof(DB::i()->getContext()->table("likes")->where($searchData)) > 0) {
DB::i()->getContext()->table("likes")->where($searchData)->delete(); DB::i()->getContext()->table("likes")->where($searchData)->delete();
else return false;
}
DB::i()->getContext()->table("likes")->insert($searchData); DB::i()->getContext()->table("likes")->insert($searchData);
return true;
}
function setLike(bool $liked, User $user): void
{
$searchData = [
"origin" => $user->getId(),
"model" => static::class,
"target" => $this->getRecord()->id,
];
if($liked)
DB::i()->getContext()->table("likes")->insert($searchData);
else
DB::i()->getContext()->table("likes")->where($searchData)->delete();
} }
function hasLikeFrom(User $user): bool function hasLikeFrom(User $user): bool

View file

@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Repositories\Users;
class SupportAlias extends RowModel
{
protected $tableName = "support_names";
function getUser(): User
{
return (new Users)->get($this->getRecord()->agent);
}
function getName(): string
{
return $this->getRecord()->name;
}
function getIcon(): ?string
{
return $this->getRecord()->icon;
}
function shouldAppendNumber(): bool
{
return (bool) $this->getRecord()->numerate;
}
function setAgent(User $agent): void
{
$this->stateChanges("agent", $agent->getId());
}
function setNumeration(bool $numerate): void
{
$this->stateChanges("numerate", $numerate);
}
}

View file

@ -11,9 +11,10 @@ use Nette\Database\Table\Selection;
class Ticket extends RowModel class Ticket extends RowModel
{ {
protected $tableName = "tickets"; protected $tableName = "tickets";
private $overrideContentColumn = "text";
function getId(): int function getId(): int
{ {
return $this->getRecord()->id; return $this->getRecord()->id;
@ -23,11 +24,11 @@ class Ticket extends RowModel
{ {
if ($this->getRecord()->type === 0) if ($this->getRecord()->type === 0)
{ {
return 'Вопрос находится на рассмотрении.'; return tr("support_status_0");
} elseif ($this->getRecord()->type === 1) { } elseif ($this->getRecord()->type === 1) {
return 'Есть ответ.'; return tr("support_status_1");
} elseif ($this->getRecord()->type === 2) { } elseif ($this->getRecord()->type === 2) {
return 'Закрыто.'; return tr("support_status_2");
} }
} }
@ -43,7 +44,11 @@ class Ticket extends RowModel
function getContext(): string function getContext(): string
{ {
return $this->getRecord()->text; $text = $this->getRecord()->text;
$text = $this->formatLinks($text);
$text = $this->removeZalgo($text);
$text = nl2br($text);
return $text;
} }
function getTime(): DateTime function getTime(): DateTime
@ -70,4 +75,11 @@ class Ticket extends RowModel
{ {
return (new Users)->get($this->getRecord()->user_id); return (new Users)->get($this->getRecord()->user_id);
} }
function isAd(): bool /* Эх, костыли... */
{
return false;
}
use Traits\TRichText;
} }

View file

@ -4,20 +4,27 @@ use openvk\Web\Util\DateTime;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\{Users, SupportAliases};
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;
class TicketComment extends RowModel class TicketComment extends RowModel
{ {
protected $tableName = "tickets_comments"; protected $tableName = "tickets_comments";
private $overrideContentColumn = "text";
private function getSupportAlias(): ?SupportAlias
{
return (new SupportAliases)->get($this->getUser()->getId());
}
function getId(): int function getId(): int
{ {
return $this->getRecord()->id; return $this->getRecord()->id;
} }
function getUType(): int function getUType(): int
{ {
return $this->getRecord()->user_type; return $this->getRecord()->user_type;
@ -28,6 +35,33 @@ class TicketComment extends RowModel
return (new Users)->get($this->getRecord()->user_id); return (new Users)->get($this->getRecord()->user_id);
} }
function getAuthorName(): string
{
if($this->getUType() === 0)
return $this->getUser()->getCanonicalName();
$alias = $this->getSupportAlias();
if(!$alias)
return OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"] . "" . $this->getAgentNumber();
$name = $alias->getName();
if($alias->shouldAppendNumber())
$name .= "" . $this->getAgentNumber();
return $name;
}
function getAvatar(): string
{
if($this->getUType() === 0)
return $this->getUser()->getAvatarUrl();
$default = "/assets/packages/static/openvk/img/support.jpeg";
$alias = $this->getSupportAlias();
return is_null($alias) ? $default : ($alias->getIcon() ?? $default);
}
function getAgentNumber(): ?string function getAgentNumber(): ?string
{ {
if($this->getUType() === 0) if($this->getUType() === 0)
@ -46,6 +80,9 @@ class TicketComment extends RowModel
if(is_null($agent = $this->getAgentNumber())) if(is_null($agent = $this->getAgentNumber()))
return NULL; return NULL;
if(!is_null($this->getSupportAlias()))
return 0;
$agent = (int) $agent; $agent = (int) $agent;
$rotation = $agent > 500 ? ( ($agent * 360) / 999 ) : $agent; # cap at 360deg $rotation = $agent > 500 ? ( ($agent * 360) / 999 ) : $agent; # cap at 360deg
$values = [0, 45, 160, 220, 310, 345]; # good looking colors $values = [0, 45, 160, 220, 310, 345]; # good looking colors
@ -59,7 +96,11 @@ class TicketComment extends RowModel
function getContext(): string function getContext(): string
{ {
return $this->getRecord()->text; $text = $this->getRecord()->text;
$text = $this->formatLinks($text);
$text = $this->removeZalgo($text);
$text = nl2br($text);
return $text;
} }
function getTime(): DateTime function getTime(): DateTime
@ -67,4 +108,10 @@ class TicketComment extends RowModel
return new DateTime($this->getRecord()->created); return new DateTime($this->getRecord()->created);
} }
function isAd(): bool
{
return false; # Кооостыыыль!!!
}
use Traits\TRichText;
} }

View file

@ -5,7 +5,8 @@ trait TRichText
{ {
private function formatEmojis(string $text): string private function formatEmojis(string $text): string
{ {
if(iconv_strlen($this->getRecord()->content) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["emojiProcessingLimit"]) $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content";
if(iconv_strlen($this->getRecord()->{$contentColumn}) > OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["emojiProcessingLimit"])
return $text; return $text;
$emojis = \Emoji\detect_emoji($text); $emojis = \Emoji\detect_emoji($text);
@ -49,8 +50,10 @@ trait TRichText
function getText(bool $html = true): string function getText(bool $html = true): string
{ {
$text = htmlentities($this->getRecord()->content, ENT_DISALLOWED | ENT_XHTML); $contentColumn = property_exists($this, "overrideContentColumn") ? $this->overrideContentColumn : "content";
$proc = iconv_strlen($this->getRecord()->content) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"];
$text = htmlentities($this->getRecord()->{$contentColumn}, ENT_DISALLOWED | ENT_XHTML);
$proc = iconv_strlen($this->getRecord()->{$contentColumn}) <= OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["postSizes"]["processingLimit"];
if($html) { if($html) {
if($proc) { if($proc) {
$rel = $this->isAd() ? "sponsored" : "ugc"; $rel = $this->isAd() ? "sponsored" : "ugc";

View file

@ -3,8 +3,8 @@ namespace openvk\Web\Models\Entities;
use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Themes\{Themepack, Themepacks};
use openvk\Web\Util\DateTime; use openvk\Web\Util\DateTime;
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{Photo, Message, Correspondence}; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Notifications}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications};
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser; use Chandler\Security\User as ChandlerUser;
@ -204,6 +204,11 @@ class User extends RowModel
return $this->getRecord()->shortcode; return $this->getRecord()->shortcode;
} }
function getAlert(): ?string
{
return $this->getRecord()->alert;
}
function getBanReason(): ?string function getBanReason(): ?string
{ {
return $this->getRecord()->block_reason; return $this->getRecord()->block_reason;
@ -214,11 +219,19 @@ class User extends RowModel
return $this->getRecord()->type; return $this->getRecord()->type;
} }
function getCoins(): int function getCoins(): float
{ {
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
return 0.0;
return $this->getRecord()->coins; return $this->getRecord()->coins;
} }
function getRating(): int
{
return OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"] ? $this->getRecord()->rating : 0;
}
function getReputation(): int function getReputation(): int
{ {
return $this->getRecord()->reputation; return $this->getRecord()->reputation;
@ -309,6 +322,21 @@ class User extends RowModel
return $this->getRecord()->birthday; return $this->getRecord()->birthday;
} }
function getAge(): ?int
{
return (int)floor((time() - $this->getBirthday()) / mktime(0, 0, 0, 1, 1, 1971));
}
function get2faSecret(): ?string
{
return $this->getRecord()["2fa_secret"];
}
function is2faEnabled(): bool
{
return !is_null($this->get2faSecret());
}
function updateNotificationOffset(): void function updateNotificationOffset(): void
{ {
$this->stateChanges("notification_offset", time()); $this->stateChanges("notification_offset", time());
@ -392,8 +420,16 @@ class User extends RowModel
$incompleteness += 20; $incompleteness += 20;
} }
$total = max(100 - $incompleteness + $this->getRating(), 0);
if(ovkGetQuirk("profile.rating-bar-behaviour") === 0)
if ($total >= 100)
$percent = round(($total / 10**strlen(strval($total))) * 100, 0);
else
$percent = min($total, 100);
return (object) [ return (object) [
"total" => 100 - $incompleteness, "total" => $total,
"percent" => $percent,
"unfilled" => $unfilled, "unfilled" => $unfilled,
]; ];
} }
@ -433,8 +469,21 @@ class User extends RowModel
return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1]));
} }
function getClubs(int $page = 1): \Traversable function getClubs(int $page = 1, bool $admin = false): \Traversable
{ {
if($admin) {
$id = $this->getId();
$query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?";
$query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE;
$sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($sel as $target) {
$target = (new Clubs)->get($target->id);
if(!$target) continue;
yield $target;
}
} else {
$sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE); $sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE);
foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) {
$target = (new Clubs)->get($target->target); $target = (new Clubs)->get($target->target);
@ -443,14 +492,54 @@ class User extends RowModel
yield $target; yield $target;
} }
} }
}
function getClubCount(): int function getClubCount(bool $admin = false): int
{ {
if($admin) {
$id = $this->getId();
$query = "SELECT COUNT(*) AS `cnt` FROM (SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?) u0;";
return (int) DatabaseConnection::i()->getConnection()->query($query, $id, $id)->fetch()->cnt;
} else {
$sel = $this->getRecord()->related("subscriptions.follower"); $sel = $this->getRecord()->related("subscriptions.follower");
$sel = $sel->where("model", "openvk\\Web\\Models\\Entities\\Club"); $sel = $sel->where("model", "openvk\\Web\\Models\\Entities\\Club");
return sizeof($sel); return sizeof($sel);
} }
}
function getPinnedClubs(): \Traversable
{
foreach($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true) as $target) {
$target = (new Clubs)->get($target->id);
if(!$target) continue;
yield $target;
}
foreach($this->getRecord()->related("group_coadmins.user")->where("club_pinned", true) as $target) {
$target = (new Clubs)->get($target->club);
if(!$target) continue;
yield $target;
}
}
function getPinnedClubCount(): int
{
return sizeof($this->getRecord()->related("groups.owner")->where("owner_club_pinned", true)) + sizeof($this->getRecord()->related("group_coadmins.user")->where("club_pinned", true));
}
function isClubPinned(Club $club): bool
{
if($club->getOwner()->getId() === $this->getId())
return $club->isOwnerClubPinned();
$manager = $club->getManager($this);
if(!is_null($manager))
return $manager->isClubPinned();
}
function getMeetings(int $page = 1): \Traversable function getMeetings(int $page = 1): \Traversable
{ {
@ -468,6 +557,57 @@ class User extends RowModel
return sizeof($this->getRecord()->related("event_turnouts.user")); return sizeof($this->getRecord()->related("event_turnouts.user"));
} }
function getGifts(int $page = 1, ?int $perPage = NULL): \Traversable
{
$gifts = $this->getRecord()->related("gift_user_relations.receiver")->order("sent DESC")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($gifts as $rel) {
yield (object) [
"sender" => (new Users)->get($rel->sender),
"gift" => (new Gifts)->get($rel->gift),
"caption" => $rel->comment,
"anon" => $rel->anonymous,
"sent" => new DateTime($rel->sent),
];
}
}
function getGiftCount(): int
{
return sizeof($this->getRecord()->related("gift_user_relations.receiver"));
}
function get2faBackupCodes(): \Traversable
{
$sel = $this->getRecord()->related("2fa_backup_codes.owner");
foreach($sel as $target)
yield $target->code;
}
function get2faBackupCodeCount(): int
{
return sizeof($this->getRecord()->related("2fa_backup_codes.owner"));
}
function generate2faBackupCodes(): void
{
$codes = [];
for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) {
$codes[] = [
owner => $this->getId(),
code => random_int(10000000, 99999999)
];
}
if(sizeof($codes) > 0)
DatabaseConnection::i()->getContext()->table("2fa_backup_codes")->insert($codes);
}
function use2faBackupCode(int $code): bool
{
return (bool) $this->getRecord()->related("2fa_backup_codes.owner")->where("code", $code)->delete();
}
function getSubscriptionStatus(User $user): int function getSubscriptionStatus(User $user): int
{ {
$subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([ $subbed = !is_null($this->getRecord()->related("subscriptions.follower")->where([
@ -528,6 +668,11 @@ class User extends RowModel
return !is_null($this->getBanReason()); return !is_null($this->getBanReason());
} }
function isOnline(): bool
{
return time() - $this->getRecord()->online <= 300;
}
function prefersNotToSeeRating(): bool function prefersNotToSeeRating(): bool
{ {
return !((bool) $this->getRecord()->show_rating); return !((bool) $this->getRecord()->show_rating);
@ -538,6 +683,18 @@ class User extends RowModel
return !is_null($this->getPendingPhoneVerification()); return !is_null($this->getPendingPhoneVerification());
} }
function gift(User $sender, Gift $gift, ?string $comment = NULL, bool $anonymous = false): void
{
DatabaseConnection::i()->getContext()->table("gift_user_relations")->insert([
"sender" => $sender->getId(),
"receiver" => $this->getId(),
"gift" => $gift->getId(),
"comment" => $comment,
"anonymous" => $anonymous,
"sent" => time(),
]);
}
function ban(string $reason): void function ban(string $reason): void
{ {
$subs = DatabaseConnection::i()->getContext()->table("subscriptions"); $subs = DatabaseConnection::i()->getContext()->table("subscriptions");
@ -615,9 +772,11 @@ class User extends RowModel
$this->stateChanges("left_menu", $mask); $this->stateChanges("left_menu", $mask);
} }
function setShortCode(?string $code = NULL): ?bool function setShortCode(?string $code = NULL, bool $force = false): ?bool
{ {
if(!is_null($code)) { if(!is_null($code)) {
if(strlen($code) < OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["minLength"] && !$force)
return false;
if(!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code)) if(!preg_match("%^[a-z][a-z0-9\\.\\_]{0,30}[a-z0-9]$%", $code))
return false; return false;
if(in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"])) if(in_array($code, OPENVK_ROOT_CONF["openvk"]["preferences"]["shortcodes"]["forbiddenNames"]))
@ -709,6 +868,10 @@ class User extends RowModel
} }
} }
function getWebsite(): ?string
{
return $this->getRecord()->website;
}
use Traits\TSubscribable; use Traits\TSubscribable;
} }

View file

@ -1,8 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use openvk\Web\Util\Shell\Shell; use openvk\Web\Util\Shell\Shell;
use openvk\Web\Util\Shell\Shell\Exceptions\ShellUnavailableException; use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException};
use openvk\Web\Util\Shell\Shell\Exceptions\UnknownCommandException;
use openvk\Web\Models\VideoDrivers\VideoDriver; use openvk\Web\Models\VideoDrivers\VideoDriver;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
@ -18,7 +17,24 @@ class Video extends Media
protected function saveFile(string $filename, string $hash): bool protected function saveFile(string $filename, string $hash): bool
{ {
if(!Shell::commandAvailable("ffmpeg")) exit(VIDEOS_FRIENDLY_ERROR); if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe"))
exit(VIDEOS_FRIENDLY_ERROR);
$error = NULL;
$streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error);
if($error !== 0)
throw new \DomainException("$filename is not a valid video file");
else if(empty($streams) || ctype_space($streams))
throw new \DomainException("$filename does not contain any video streams");
$durations = [];
preg_match('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful video streams");
foreach($durations[1] as $duration)
if(floatval($duration) < 1.0)
throw new \DomainException("$filename does not contain any meaningful video streams");
try { try {
if(!is_dir($dirId = $this->pathFromHash($hash))) if(!is_dir($dirId = $this->pathFromHash($hash)))
@ -105,4 +121,19 @@ class Video extends Media
$this->unwire(); $this->unwire();
$this->save(); $this->save();
} }
static function fastMake(int $owner, string $description = "", array $file, bool $unlisted = true, bool $anon = false): Video
{
$video = new Video;
$video->setOwner($owner);
$video->setName("Unnamed Video.ogv");
$video->setDescription(ovk_proc_strtr($description, 300));
$video->setAnonymous($anon);
$video->setCreated(time());
$video->setFile($file);
$video->setUnlisted($unlisted);
$video->save();
return $video;
}
} }

View file

@ -0,0 +1,85 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\RowModel;
class Voucher extends RowModel
{
protected $tableName = "coin_vouchers";
function getCoins(): int
{
return $this->getRecord()->coins;
}
function getRating(): int
{
return $this->getRecord()->rating;
}
function getToken(): string
{
return $this->getRecord()->token;
}
function getFormattedToken(): string
{
$fmtTok = "";
$token = $this->getRecord()->token;
foreach(array_chunk(str_split($token), 6) as $chunk)
$fmtTok .= implode("", $chunk) . "-";
return substr($fmtTok, 0, -1);
}
function getRemainingUsages(): float
{
return (float) ($this->getRecord()->usages_left ?? INF);
}
function getUsers(int $page = -1, ?int $perPage = NULL): \Traversable
{
$relations = $this->getRecord()->related("voucher_users.voucher");
if($page !== -1)
$relations = $relations->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($relations as $relation)
yield (new Users)->get($relation->user);
}
function isExpired(): bool
{
return $this->getRemainingUsages() < 1;
}
function wasUsedBy(User $user): bool
{
$record = $this->getRecord()->related("voucher_users.voucher")->where("user", $user->getId());
return sizeof($record) > 0;
}
function willUse(User $user): bool
{
if($this->wasUsedBy($user))
return false;
if($this->isExpired())
return false;
$this->setRemainingUsages($this->getRemainingUsages() - 1);
DB::i()->getContext()->table("voucher_users")->insert([
"voucher" => $this->getId(),
"user" => $user->getId(),
]);
return true;
}
function setRemainingUsages(float $usages): void
{
$this->stateChanges("usages_left", $usages === INF ? NULL : ((int) $usages));
$this->save();
}
}

View file

@ -38,6 +38,19 @@ class Comments
yield $this->toComment($comment); yield $this->toComment($comment);
} }
function getLastCommentsByTarget(Postable $target, ?int $count = NULL): \Traversable
{
$comments = $this->comments->where([
"model" => get_class($target),
"target" => $target->getId(),
"deleted" => false,
])->page(1, $count ?? OPENVK_DEFAULT_PER_PAGE)->order("created DESC");
$comments = array_reverse(iterator_to_array($comments));
foreach($comments as $comment)
yield $this->toComment($comment);
}
function getCommentsCountByTarget(Postable $target): int function getCommentsCountByTarget(Postable $target): int
{ {
return sizeof($this->comments->where([ return sizeof($this->comments->where([

View file

@ -0,0 +1,45 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\{Gift, GiftCategory};
class Gifts
{
private $context;
private $gifts;
private $cats;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->gifts = $this->context->table("gifts");
$this->cats = $this->context->table("gift_categories");
}
function get(int $id): ?Gift
{
$gift = $this->gifts->get($id);
if(!$gift)
return NULL;
return new Gift($gift);
}
function getCat(int $id): ?GiftCategory
{
$cat = $this->cats->get($id);
if(!$cat)
return NULL;
return new GiftCategory($cat);
}
function getCategories(int $page, ?int $perPage = NULL, &$count = nullptr): \Traversable
{
$cats = $this->cats->where("deleted", false);
$count = $cats->count();
$cats = $cats->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE);
foreach($cats as $cat)
yield new GiftCategory($cat);
}
}

View file

@ -33,6 +33,15 @@ class Notes
yield new Note($album); yield new Note($album);
} }
function getNoteById(int $owner, int $note): ?Note
{
$note = $this->notes->where(['owner' => $owner, 'virtual_id' => $note])->fetch();
if(!is_null($note))
return new Note($note);
else
return null;
}
function getUserNotesCount(User $user): int function getUserNotesCount(User $user): int
{ {
return sizeof($this->notes->where("owner", $user->getId())->where("deleted", 0)); return sizeof($this->notes->where("owner", $user->getId())->where("deleted", 0));

View file

@ -43,6 +43,19 @@ class Notifications
return $query; return $query;
} }
private function assemble(int $act, int $originModelType, int $originModelId, int $targetModelType, int $targetModelId, int $recipientId, int $timestamp, $data, ?string $class = NULL): Notification
{
$class ??= 'openvk\Web\Models\Entities\Notifications\Notification';
$originModel = $this->getModel($originModelType, $originModelId);
$targetModel = $this->getModel($targetModelType, $targetModelId);
$recipient = (new Users)->get($recipientId);
$notification = new $class($recipient, $originModel, $targetModel, $timestamp, $data);
$notification->setActionCode($act);
return $notification;
}
function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int function getNotificationCountByUser(User $user, int $offset, bool $archived = false): int
{ {
$db = $this->getEDB(false); $db = $this->getEDB(false);
@ -64,13 +77,38 @@ class Notifications
$results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage)); $results = $this->getEDB()->query($this->getQuery($user, false, $offset, $archived, $page, $perPage));
foreach($results->fetchAll() as $notif) { foreach($results->fetchAll() as $notif) {
$originModel = $this->getModel($notif->originModelType, $notif->originModelId); yield $this->assemble(
$targetModel = $this->getModel($notif->targetModelType, $notif->targetModelId); $notif->modelAction,
$recipient = (new Users)->get($notif->recipientId); $notif->originModelType,
$notif->originModelId,
$notification = new Notification($recipient, $originModel, $targetModel, $notif->timestamp, $notif->additionalData); $notif->targetModelType,
$notification->setActionCode($notif->modelAction); $notif->targetModelId,
yield $notification;
$notif->recipientId,
$notif->timestamp,
$notif->additionalData
);
} }
} }
function fromDescriptor(string $descriptor, ?object &$parsedData = nullptr)
{
[$class, $recv, $data] = explode(",", $descriptor);
$class = str_replace(".", "\\", $class);
$parsedData = unserialize(base64_decode($data));
return $this->assemble(
$parsedData->actionCode,
$parsedData->originModelType,
$parsedData->originModelId,
$parsedData->targetModelType,
$parsedData->targetModelId,
$parsedData->recipient,
$parsedData->timestamp,
$parsedData->additionalPayload,
);
}
} }

View file

@ -37,12 +37,13 @@ class Posts
return $this->toPost($post); return $this->toPost($post);
} }
function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = NULL): \Traversable function getPostsFromUsersWall(int $user, int $page = 1, ?int $perPage = NULL, ?int $offset = NULL): \Traversable
{ {
$perPage ??= OPENVK_DEFAULT_PER_PAGE; $perPage ??= OPENVK_DEFAULT_PER_PAGE;
$offset = $perPage * ($page - 1); $offset ??= $perPage * ($page - 1);
$pinPost = $this->getPinnedPost($user); $pinPost = $this->getPinnedPost($user);
if(is_null($offset) || $offset == 0) {
if(!is_null($pinPost)) { if(!is_null($pinPost)) {
if($page === 1) { if($page === 1) {
$perPage--; $perPage--;
@ -52,6 +53,9 @@ class Posts
$offset--; $offset--;
} }
} }
} else if(!is_null($offset)) {
$offset--;
}
$sel = $this->posts->where([ $sel = $this->posts->where([
"wall" => $user, "wall" => $user,

View file

@ -6,8 +6,8 @@ use Nette\Database\Table\ActiveRow;
abstract class Repository abstract class Repository
{ {
private $context; protected $context;
private $table; protected $table;
protected $tableName; protected $tableName;
protected $modelName; protected $modelName;
@ -29,5 +29,18 @@ abstract class Repository
return $this->toEntity($this->table->get($id)); return $this->toEntity($this->table->get($id));
} }
function size(bool $withDeleted = false): int
{
return sizeof($this->table->where("deleted", $withDeleted));
}
function enumerate(int $page, ?int $perPage = NULL, bool $withDeleted = false): \Traversable
{
$perPage ??= OPENVK_DEFAULT_PER_PAGE;
foreach($this->table->where("deleted", $withDeleted)->page($page, $perPage) as $entity)
yield $this->toEntity($entity);
}
use \Nette\SmartObject; use \Nette\SmartObject;
} }

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
class SupportAliases extends Repository
{
protected $tableName = "support_names";
protected $modelName = "SupportAlias";
function get(int $agent)
{
return $this->toEntity($this->table->where("agent", $agent)->fetch());
}
}

View file

@ -22,7 +22,7 @@ class Tickets
function getTickets(int $state = 0, int $page = 1): \Traversable function getTickets(int $state = 0, int $page = 1): \Traversable
{ {
foreach($this->tickets->where(["deleted" => 0, "type" => $state])->page($page, OPENVK_DEFAULT_PER_PAGE) as $t) foreach($this->tickets->where(["deleted" => 0, "type" => $state])->order("created DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $t)
yield new Ticket($t); yield new Ticket($t);
} }
@ -33,7 +33,12 @@ class Tickets
function getTicketsByuId(int $user_id): \Traversable function getTicketsByuId(int $user_id): \Traversable
{ {
foreach($this->tickets->where(['user_id' => $user_id, 'deleted' => 0]) as $ticket) yield new Ticket($ticket); foreach($this->tickets->where(['user_id' => $user_id, 'deleted' => 0])->order("created DESC") as $ticket) yield new Ticket($ticket);
}
function getTicketsCountByuId(int $user_id, int $type = 0): int
{
return sizeof($this->tickets->where(['user_id' => $user_id, 'deleted' => 0, 'type' => $type]));
} }
function getRequestById(int $req_id): ?Ticket function getRequestById(int $req_id): ?Ticket

View file

@ -37,12 +37,12 @@ class Videos
function getByUser(User $user, int $page = 1, ?int $perPage = NULL): \Traversable function getByUser(User $user, int $page = 1, ?int $perPage = NULL): \Traversable
{ {
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE; $perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
foreach($this->videos->where("owner", $user->getId())->where("deleted", 0)->page($page, $perPage) as $video) foreach($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0])->page($page, $perPage)->order("created DESC") as $video)
yield new Video($video); yield new Video($video);
} }
function getUserVideosCount(User $user): int function getUserVideosCount(User $user): int
{ {
return sizeof($this->videos->where("owner", $user->getId())->where("deleted", 0)); return sizeof($this->videos->where("owner", $user->getId())->where(["deleted" => 0, "unlisted" => 0]));
} }
} }

View file

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Voucher;
class Vouchers extends Repository
{
protected $tableName = "coin_vouchers";
protected $modelName = "Voucher";
function getByToken(string $token, bool $withDeleted = false)
{
$voucher = $this->table->where([
"token" => $token,
"deleted" => $withDeleted,
])->fetch();
return $this->toEntity($voucher);
}
}

View file

@ -2,6 +2,7 @@
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Themes\Themepacks; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Repositories\{Users, Managers}; use openvk\Web\Models\Repositories\{Users, Managers};
use openvk\Web\Util\Localizator;
use Chandler\Session\Session; use Chandler\Session\Session;
final class AboutPresenter extends OpenVKPresenter final class AboutPresenter extends OpenVKPresenter
@ -62,10 +63,22 @@ final class AboutPresenter extends OpenVKPresenter
$this->template->languages = getLanguages(); $this->template->languages = getLanguages();
if(!is_null($_GET['lg'])){ if(!is_null($_GET['lg'])){
$this->assertNoCSRF();
setLanguage($_GET['lg']); setLanguage($_GET['lg']);
} }
} }
function renderExportJSLanguage($lg = NULL): void
{
$localizer = Localizator::i();
$lang = $lg;
if(is_null($lg))
$this->throwError(404, "Not found", "Language is not found");
header("Content-Type: application/javascript");
echo "window.lang = " . json_encode($localizer->export($lang)) . ";"; // привет хардкод :DDD
exit;
}
function renderSandbox(): void function renderSandbox(): void
{ {
$this->template->languages = getLanguages(); $this->template->languages = getLanguages();

View file

@ -1,21 +1,31 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User};
use openvk\Web\Models\Repositories\{Users, Clubs}; use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts};
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
{ {
private $users; private $users;
private $clubs; private $clubs;
private $vouchers;
private $gifts;
function __construct(Users $users, Clubs $clubs) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
$this->vouchers = $vouchers;
$this->gifts = $gifts;
parent::__construct(); parent::__construct();
} }
private function warnIfNoCommerce(): void
{
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flash("warn", "Коммерция отключена системным администратором", "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния.");
}
private function searchResults(object $repo, &$count) private function searchResults(object $repo, &$count)
{ {
$query = $this->queryParam("q") ?? ""; $query = $this->queryParam("q") ?? "";
@ -106,6 +116,219 @@ final class AdminPresenter extends OpenVKPresenter
} }
} }
function renderVouchers(): void
{
$this->warnIfNoCommerce();
$this->template->count = $this->vouchers->size();
$this->template->vouchers = iterator_to_array($this->vouchers->enumerate((int) ($this->queryParam("p") ?? 1)));
}
function renderVoucher(int $id): void
{
$this->warnIfNoCommerce();
$voucher = NULL;
$this->template->form = (object) [];
if($id === 0) {
$this->template->form->id = 0;
$this->template->form->token = NULL;
$this->template->form->coins = 0;
$this->template->form->rating = 0;
$this->template->form->usages = -1;
$this->template->form->users = [];
} else {
$voucher = $this->vouchers->get($id);
if(!$voucher)
$this->notFound();
$this->template->form->id = $voucher->getId();
$this->template->form->token = $voucher->getToken();
$this->template->form->coins = $voucher->getCoins();
$this->template->form->rating = $voucher->getRating();
$this->template->form->usages = $voucher->getRemainingUsages();
$this->template->form->users = iterator_to_array($voucher->getUsers());
if($this->template->form->usages === INF)
$this->template->form->usages = -1;
else
$this->template->form->usages = (int) $this->template->form->usages;
}
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$voucher ??= new Voucher;
$voucher->setCoins((int) $this->postParam("coins"));
$voucher->setRating((int) $this->postParam("rating"));
$voucher->setRemainingUsages($this->postParam("usages") === '-1' ? INF : ((int) $this->postParam("usages")));
if(!empty($tok = $this->postParam("token")) && strlen($tok) === 24)
$voucher->setToken($tok);
$voucher->save();
$this->redirect("/admin/vouchers/id" . $voucher->getId(), static::REDIRECT_TEMPORARY);
exit;
}
function renderGiftCategories(): void
{
$this->warnIfNoCommerce();
$this->template->act = $this->queryParam("act") ?? "list";
$this->template->categories = iterator_to_array($this->gifts->getCategories((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
}
function renderGiftCategory(string $slug, int $id): void
{
$this->warnIfNoCommerce();
$cat;
$gen = false;
if($id !== 0) {
$cat = $this->gifts->getCat($id);
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $slug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $id . ".meta", static::REDIRECT_TEMPORARY);
} else {
$gen = true;
$cat = new GiftCategory;
}
$this->template->form = (object) [];
$this->template->form->id = $id;
$this->template->form->languages = [];
foreach(getLanguages() as $language) {
$language = (object) $language;
$this->template->form->languages[$language->code] = (object) [];
$this->template->form->languages[$language->code]->name = $gen ? "" : ($cat->getName($language->code, true) ?? "");
$this->template->form->languages[$language->code]->description = $gen ? "" : ($cat->getDescription($language->code, true) ?? "");
}
$this->template->form->languages["master"] = (object) [
"name" => $gen ? "Unknown Name" : $cat->getName(),
"description" => $gen ? "" : $cat->getDescription(),
];
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
if($gen) {
$cat->setAutoQuery(NULL);
$cat->save();
}
$cat->setName("_", $this->postParam("name_master"));
$cat->setDescription("_", $this->postParam("description_master"));
foreach(getLanguages() as $language) {
$code = $language["code"];
if(!empty($this->postParam("name_$code") ?? NULL))
$cat->setName($code, $this->postParam("name_$code"));
if(!empty($this->postParam("description_$code") ?? NULL))
$cat->setDescription($code, $this->postParam("description_$code"));
}
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $cat->getId() . ".meta", static::REDIRECT_TEMPORARY);
}
function renderGifts(string $catSlug, int $catId): void
{
$this->warnIfNoCommerce();
$cat = $this->gifts->getCat($catId);
if(!$cat)
$this->notFound();
else if($cat->getSlug() !== $catSlug)
$this->redirect("/admin/gifts/" . $cat->getSlug() . "." . $catId . "/", static::REDIRECT_TEMPORARY);
$this->template->cat = $cat;
$this->template->gifts = iterator_to_array($cat->getGifts((int) ($this->queryParam("p") ?? 1), NULL, $this->template->count));
}
function renderGift(int $id): void
{
$this->warnIfNoCommerce();
$gift = $this->gifts->get($id);
$act = $this->queryParam("act") ?? "edit";
switch($act) {
case "delete":
$this->assertNoCSRF();
if(!$gift)
$this->notFound();
$gift->delete();
$this->flashFail("succ", "Gift moved successfully", "This gift will now be in <b>Recycle Bin</b>.");
break;
case "copy":
case "move":
$this->assertNoCSRF();
if(!$gift)
$this->notFound();
$catFrom = $this->gifts->getCat((int) ($this->queryParam("from") ?? 0));
$catTo = $this->gifts->getCat((int) ($this->queryParam("to") ?? 0));
if(!$catFrom || !$catTo || !$catFrom->hasGift($gift))
$this->badRequest();
if($act === "move")
$catFrom->removeGift($gift);
$catTo->addGift($gift);
$name = $catTo->getName();
$this->flash("succ", "Gift moved successfully", "This gift will now be in <b>$name</b>.");
$this->redirect("/admin/gifts/" . $catTo->getSlug() . "." . $catTo->getId() . "/", static::REDIRECT_TEMPORARY);
break;
default:
case "edit":
$gen = false;
if(!$gift) {
$gen = true;
$gift = new Gift;
}
$this->template->form = (object) [];
$this->template->form->id = $id;
$this->template->form->name = $gen ? "New Gift (1)" : $gift->getName();
$this->template->form->price = $gen ? 0 : $gift->getPrice();
$this->template->form->usages = $gen ? 0 : $gift->getUsages();
$this->template->form->limit = $gen ? -1 : ($gift->getLimit() === INF ? -1 : $gift->getLimit());
$this->template->form->pic = $gen ? NULL : $gift->getImage(Gift::IMAGE_URL);
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$limit = $this->postParam("limit") ?? $this->template->form->limit;
$limit = $limit == "-1" ? INF : (float) $limit;
$gift->setLimit($limit, is_null($this->postParam("reset_limit")) ? Gift::PERIOD_SET_IF_NONE : Gift::PERIOD_SET);
$gift->setName($this->postParam("name"));
$gift->setPrice((int) $this->postParam("price"));
$gift->setUsages((int) $this->postParam("usages"));
if(isset($_FILES["pic"]) && $_FILES["pic"]["error"] === UPLOAD_ERR_OK) {
if(!$gift->setImage($_FILES["pic"]["tmp_name"]))
$this->flashFail("err", "Не удалось сохранить подарок", "Изображение подарка кривое.");
} else if($gen) {
# If there's no gift pic but it's newly created
$this->flashFail("err", "Не удалось сохранить подарок", "Пожалуйста, загрузите изображение подарка.");
}
$gift->save();
if($gen && !is_null($cat = $this->postParam("_cat"))) {
$cat = $this->gifts->getCat((int) $cat);
if(!is_null($cat))
$cat->addGift($gift);
}
$this->redirect("/admin/gifts/id" . $gift->getId(), static::REDIRECT_TEMPORARY);
}
}
function renderFiles(): void function renderFiles(): void
{ {

View file

@ -10,6 +10,7 @@ use Chandler\Session\Session;
use Chandler\Security\User as ChandlerUser; use Chandler\Security\User as ChandlerUser;
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use lfkeitel\phptotp\{Base32, Totp};
final class AuthPresenter extends OpenVKPresenter final class AuthPresenter extends OpenVKPresenter
{ {
@ -89,6 +90,9 @@ final class AuthPresenter extends OpenVKPresenter
if(!$this->emailValid($this->postParam("email"))) if(!$this->emailValid($this->postParam("email")))
$this->flashFail("err", "Неверный email адрес", "Email, который вы ввели, не является корректным."); $this->flashFail("err", "Неверный email адрес", "Email, который вы ввели, не является корректным.");
if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", "Неверная дата рождения", "Дату рождения, которую вы ввели, не является корректным.");
$chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password")); $chUser = ChandlerUser::create($this->postParam("email"), $this->postParam("password"));
if(!$chUser) if(!$chUser)
$this->flashFail("err", "Не удалось зарегистрироваться", "Пользователь с таким email уже существует."); $this->flashFail("err", "Не удалось зарегистрироваться", "Пользователь с таким email уже существует.");
@ -101,6 +105,7 @@ final class AuthPresenter extends OpenVKPresenter
$user->setEmail($this->postParam("email")); $user->setEmail($this->postParam("email"));
$user->setSince(date("Y-m-d H:i:s")); $user->setSince(date("Y-m-d H:i:s"));
$user->setRegistering_Ip(CONNECTING_IP); $user->setRegistering_Ip(CONNECTING_IP);
$user->setBirthday(strtotime($this->postParam("birthday")));
$user->save(); $user->save();
if(!is_null($referer)) { if(!is_null($referer)) {
@ -128,9 +133,27 @@ final class AuthPresenter extends OpenVKPresenter
if(!$user) if(!$user)
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>"); $this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
if(!$this->authenticator->login($user->id, $this->postParam("password"))) if(!$this->authenticator->verifyCredentials($user->id, $this->postParam("password")))
$this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>"); $this->flashFail("err", "Не удалось войти", "Неверное имя пользователя или пароль. <a href='/restore.pl'>Забыли пароль?</a>");
$secret = $user->related("profiles.user")->fetch()["2fa_secret"];
$code = $this->postParam("code");
if(!is_null($secret)) {
$this->template->_template = "Auth/LoginSecondFactor.xml";
$this->template->login = $this->postParam("login");
$this->template->password = $this->postParam("password");
if(is_null($code))
return;
$ovkUser = new User($user->related("profiles.user")->fetch());
if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $ovkUser->use2faBackupCode((int) $code))) {
$this->flash("err", "Не удалось войти", tr("incorrect_2fa_code"));
return;
}
}
$this->authenticator->authenticate($user->id);
$this->redirect($redirUrl ?? "/id" . $user->related("profiles.user")->fetch()->id, static::REDIRECT_TEMPORARY); $this->redirect($redirUrl ?? "/id" . $user->related("profiles.user")->fetch()->id, static::REDIRECT_TEMPORARY);
exit; exit;
} }
@ -159,6 +182,7 @@ final class AuthPresenter extends OpenVKPresenter
function renderLogout(): void function renderLogout(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->assertNoCSRF();
$this->authenticator->logout(); $this->authenticator->logout();
Session::i()->set("_su", NULL); Session::i()->set("_su", NULL);
@ -173,7 +197,19 @@ final class AuthPresenter extends OpenVKPresenter
$this->redirect("/"); $this->redirect("/");
} }
$this->template->is2faEnabled = $request->getUser()->is2faEnabled();
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
if($request->getUser()->is2faEnabled()) {
$user = $request->getUser();
$code = $this->postParam("code");
$secret = $user->get2faSecret();
if(!($code === (new Totp)->GenerateToken(Base32::decode($secret)) || $user->use2faBackupCode((int) $code))) {
$this->flash("err", tr("error"), tr("incorrect_2fa_code"));
return;
}
}
$user = $request->getUser()->getChandlerUser(); $user = $request->getUser()->getChandlerUser();
$this->db->table("ChandlerTokens")->where("user", $user->getId())->delete(); #Logout from everywhere $this->db->table("ChandlerTokens")->where("user", $user->getId())->delete(); #Logout from everywhere

View file

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Comment, User}; use openvk\Web\Models\Entities\{Comment, Photo, Video, User};
use openvk\Web\Models\Entities\Notifications\CommentNotification; use openvk\Web\Models\Entities\Notifications\CommentNotification;
use openvk\Web\Models\Repositories\Comments; use openvk\Web\Models\Repositories\Comments;
@ -38,6 +38,41 @@ final class CommentPresenter extends OpenVKPresenter
$entity = $repo->get($eId); $entity = $repo->get($eId);
if(!$entity) $this->notFound(); if(!$entity) $this->notFound();
$flags = 0;
if($this->postParam("as_group") === "on")
$flags |= 0b10000000;
$photo = NULL;
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
try {
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"]);
} catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой.");
}
}
// TODO move to trait
try {
$photo = NULL;
$video = NULL;
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
$album = NULL;
if($wall > 0 && $wall === $this->user->id)
$album = (new Albums)->getUserWallAlbum($wallOwner);
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album);
}
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"]);
}
} catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать комментарий", "Файл медиаконтента повреждён или слишком велик.");
}
if(empty($this->postParam("text")) && !$photo && !$video)
$this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий пустой или слишком большой.");
try { try {
$comment = new Comment; $comment = new Comment;
$comment->setOwner($this->user->id); $comment->setOwner($this->user->id);
@ -45,11 +80,18 @@ final class CommentPresenter extends OpenVKPresenter
$comment->setTarget($entity->getId()); $comment->setTarget($entity->getId());
$comment->setContent($this->postParam("text")); $comment->setContent($this->postParam("text"));
$comment->setCreated(time()); $comment->setCreated(time());
$comment->setFlags($flags);
$comment->save(); $comment->save();
} catch(\LogicException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", "Не удалось опубликовать комментарий", "Нельзя опубликовать пустой комментарий."); $this->flashFail("err", "Не удалось опубликовать комментарий", "Комментарий слишком большой.");
} }
if(!is_null($photo))
$comment->attach($photo);
if(!is_null($video))
$comment->attach($video);
if($entity->getOwner()->getId() !== $this->user->identity->getId()) if($entity->getOwner()->getId() !== $this->user->identity->getId())
if(($owner = $entity->getOwner()) instanceof User) if(($owner = $entity->getOwner()) instanceof User)
(new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit(); (new CommentNotification($owner, $comment, $entity, $this->user->identity))->emit();

View file

@ -0,0 +1,140 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\{Gifts, Users};
use openvk\Web\Models\Entities\Notifications\GiftNotification;
final class GiftsPresenter extends OpenVKPresenter
{
private $gifts;
private $users;
function __construct(Gifts $gifts, Users $users)
{
$this->gifts = $gifts;
$this->users = $users;
}
function renderUserGifts(int $user): void
{
$this->assertUserLoggedIn();
$user = $this->users->get($user);
if(!$user)
$this->notFound();
$this->template->user = $user;
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$this->template->count = $user->getGiftCount();
$this->template->iterator = $user->getGifts($page);
$this->template->hideInfo = $this->user->id !== $user->getId();
}
function renderGiftMenu(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
if(!$user)
$this->notFound();
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$cats = $this->gifts->getCategories($page, NULL, $this->template->count);
$this->template->user = $user;
$this->template->iterator = $cats;
$this->template->_template = "Gifts/Menu.xml";
}
function renderGiftList(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat)
$this->flashFail("err", "Не удалось подарить", "Пользователь или набор не существуют.");
$this->template->page = $page = (int) ($this->queryParam("p") ?? 1);
$gifts = $cat->getGifts($page, null, $this->template->count);
$this->template->user = $user;
$this->template->cat = $cat;
$this->template->gifts = iterator_to_array($gifts);
$this->template->_template = "Gifts/Pick.xml";
}
function renderConfirmGift(): void
{
$user = $this->users->get((int) ($this->queryParam("user") ?? 0));
$gift = $this->gifts->get((int) ($this->queryParam("elid") ?? 0));
$cat = $this->gifts->getCat((int) ($this->queryParam("pack") ?? 0));
if(!$user || !$cat || !$gift || !$cat->hasGift($gift))
$this->flashFail("err", "Не удалось подарить", "Не удалось подтвердить права на подарок.");
if(!$gift->canUse($this->user->identity))
$this->flashFail("err", "Не удалось подарить", "У вас больше не осталось таких подарков.");
$coinsLeft = $this->user->identity->getCoins() - $gift->getPrice();
if($coinsLeft < 0)
$this->flashFail("err", "Не удалось подарить", "Ору нищ не пук.");
$this->template->_template = "Gifts/Confirm.xml";
if($_SERVER["REQUEST_METHOD"] !== "POST") {
$this->template->user = $user;
$this->template->cat = $cat;
$this->template->gift = $gift;
return;
}
$comment = empty($c = $this->postParam("comment")) ? NULL : $c;
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
$notification->emit();
$this->user->identity->setCoins($coinsLeft);
$this->user->identity->save();
$user->gift($this->user->identity, $gift, $comment, !is_null($this->postParam("anonymous")));
$gift->used();
$this->flash("succ", "Подарок отправлен", "Вы отправили подарок <b>" . $user->getFirstName() . "</b> за " . $gift->getPrice() . " голосов.");
$this->redirect($user->getURL(), static::REDIRECT_TEMPORARY);
}
function renderStub(): void
{
$this->assertUserLoggedIn();
$act = $this->queryParam("act");
switch($act) {
case "pick":
$this->renderGiftMenu();
break;
case "menu":
$this->renderGiftList();
break;
case "confirm":
$this->renderConfirmGift();
break;
default:
$this->notFound();
}
}
function renderGiftImage(int $id, int $timestamp): void
{
$gift = $this->gifts->get($id);
if(!$gift)
$this->notFound();
$image = $gift->getImage();
header("Cache-Control: no-transform, immutable");
header("Content-Length: " . strlen($image));
header("Content-Type: image/png");
exit($image);
}
function onStartup(): void
{
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flashFail("err", tr("error"), tr("feature_disabled"));
parent::onStartup();
}
}

View file

@ -84,8 +84,22 @@ final class GroupPresenter extends OpenVKPresenter
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
$this->template->club = $this->clubs->get($id); $this->template->club = $this->clubs->get($id);
$this->template->onlyShowManagers = $this->queryParam("onlyAdmins") == "1";
if($this->template->onlyShowManagers) {
$this->template->followers = null;
$this->template->managers = $this->template->club->getManagers((int) ($this->queryParam("p") ?? 1), !$this->template->club->canBeModifiedBy($this->user->identity));
if($this->template->club->canBeModifiedBy($this->user->identity) || !$this->template->club->isOwnerHidden()) {
$this->template->managers = array_merge([$this->template->club->getOwner()], iterator_to_array($this->template->managers));
}
$this->template->count = $this->template->club->getManagersCount();
} else {
$this->template->followers = $this->template->club->getFollowers((int) ($this->queryParam("p") ?? 1)); $this->template->followers = $this->template->club->getFollowers((int) ($this->queryParam("p") ?? 1));
$this->template->managers = null;
$this->template->count = $this->template->club->getFollowersCount(); $this->template->count = $this->template->club->getFollowersCount();
}
$this->template->paginatorConf = (object) [ $this->template->paginatorConf = (object) [
"count" => $this->template->count, "count" => $this->template->count,
"page" => $this->queryParam("p") ?? 1, "page" => $this->queryParam("p") ?? 1,
@ -98,6 +112,8 @@ final class GroupPresenter extends OpenVKPresenter
{ {
$user = is_null($this->queryParam("user")) ? $this->postParam("user") : $this->queryParam("user"); $user = is_null($this->queryParam("user")) ? $this->postParam("user") : $this->queryParam("user");
$comment = $this->postParam("comment"); $comment = $this->postParam("comment");
$removeComment = $this->postParam("removeComment") === "1";
$hidden = ["0" => false, "1" => true][$this->queryParam("hidden")] ?? null;
//$index = $this->queryParam("index"); //$index = $this->queryParam("index");
if(!$user) if(!$user)
$this->badRequest(); $this->badRequest();
@ -107,19 +123,56 @@ final class GroupPresenter extends OpenVKPresenter
if(!$user || !$club) if(!$user || !$club)
$this->notFound(); $this->notFound();
if(!$club->canBeModifiedBy($this->user->identity ?? NULL) && $club->getOwner()->getId() !== $user->getId()) if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
$this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс."); $this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.");
/* if(!empty($index)){ if(!is_null($hidden)) {
$manager = (new Managers)->get($index); if($club->getOwner()->getId() == $user->getId()) {
$manager->setComment($comment); $club->setOwner_Hidden($hidden);
$this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён"); $club->save();
}else{ */ } else {
if($comment) { $manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId());
$manager->setHidden($hidden);
$manager->save();
}
if($club->getManagersCount(true) == 0) {
$club->setAdministrators_List_Display(2);
$club->save();
}
if($hidden) {
$this->flashFail("succ", "Операция успешна", "Теперь " . $user->getCanonicalName() . " будет показываться как обычный подписчик всем кроме других администраторов");
} else {
$this->flashFail("succ", "Операция успешна", "Теперь все будут знать про то что " . $user->getCanonicalName() . " - администратор");
}
} elseif($removeComment) {
if($club->getOwner()->getId() == $user->getId()) {
$club->setOwner_Comment(null);
$club->save();
} else {
$manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId());
$manager->setComment(null);
$manager->save();
}
$this->flashFail("succ", "Операция успешна", "Комментарий к администратору удален");
} elseif($comment) {
if(mb_strlen($comment) > 36) {
$commentLength = (string) mb_strlen($comment);
$this->flashFail("err", "Ошибка", "Комментарий слишком длинный ($commentLength символов вместо 36 символов)");
}
if($club->getOwner()->getId() == $user->getId()) {
$club->setOwner_Comment($comment);
$club->save();
} else {
$manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId()); $manager = (new Managers)->getByUserAndClub($user->getId(), $club->getId());
$manager->setComment($comment); $manager->setComment($comment);
$manager->save(); $manager->save();
$this->flashFail("succ", "Операция успешна", "."); }
$this->flashFail("succ", "Операция успешна", "Комментарий к администратору изменён");
}else{ }else{
if($club->canBeModifiedBy($user)) { if($club->canBeModifiedBy($user)) {
$club->removeManager($user); $club->removeManager($user);
@ -150,6 +203,13 @@ final class GroupPresenter extends OpenVKPresenter
$club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about")); $club->setAbout(empty($this->postParam("about")) ? NULL : $this->postParam("about"));
$club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode")); $club->setShortcode(empty($this->postParam("shortcode")) ? NULL : $this->postParam("shortcode"));
$club->setWall(empty($this->postParam("wall")) ? 0 : 1); $club->setWall(empty($this->postParam("wall")) ? 0 : 1);
$club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display"));
$website = $this->postParam("website") ?? "";
if(empty($website))
$club->setWebsite(NULL);
else
$club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website);
if($_FILES["ava"]["error"] === UPLOAD_ERR_OK) { if($_FILES["ava"]["error"] === UPLOAD_ERR_OK) {
$photo = new Photo; $photo = new Photo;

View file

@ -58,8 +58,8 @@ final class InternalAPIPresenter extends OpenVKPresenter
try { try {
$params = array_merge($input->params ?? [], [function($data) { $params = array_merge($input->params ?? [], [function($data) {
$this->succ($data); $this->succ($data);
}, function($data) { }, function(int $errno, string $errstr) {
$this->fail($data); $this->fail($errno, $errstr);
}]); }]);
$handler->{$method}(...$params); $handler->{$method}(...$params);
} catch(\TypeError $te) { } catch(\TypeError $te) {

View file

@ -31,10 +31,10 @@ final class NotesPresenter extends OpenVKPresenter
]; ];
} }
function renderView(int $owner, int $id): void function renderView(int $owner, int $note_id): void
{ {
$note = $this->notes->get($id); $note = $this->notes->getNoteById($owner, $note_id);
if(!$note || $note->getOwner()->getId() !== $owner) if(!$note || $note->getOwner()->getId() !== $owner || $note->isDeleted())
$this->notFound(); $this->notFound();
$this->template->cCount = $note->getCommentsCount(); $this->template->cCount = $note->getCommentsCount();
@ -65,7 +65,7 @@ final class NotesPresenter extends OpenVKPresenter
$note->setSource($this->postParam("html")); $note->setSource($this->postParam("html"));
$note->save(); $note->save();
$this->redirect("/note" . $this->user->id . "_" . $note->getId()); $this->redirect("/note" . $this->user->id . "_" . $note->getVirtualId());
} }
} }

22
Web/Presenters/OpenVKPresenter.php Normal file → Executable file
View file

@ -6,7 +6,8 @@ use Chandler\Session\Session;
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
use Latte\Engine as TemplatingEngine; use Latte\Engine as TemplatingEngine;
use openvk\Web\Models\Entities\IP; use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Repositories\{IPs, Users, APITokens}; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets};
abstract class OpenVKPresenter extends SimplePresenter abstract class OpenVKPresenter extends SimplePresenter
{ {
@ -33,6 +34,11 @@ abstract class OpenVKPresenter extends SimplePresenter
])); ]));
} }
protected function setTempTheme(string $theme): void
{
Session::i()->set("_tempTheme", $theme);
}
protected function flashFail(string $type, string $title, ?string $message = NULL, ?int $code = NULL): void protected function flashFail(string $type, string $title, ?string $message = NULL, ?int $code = NULL): void
{ {
$this->flash($type, $title, $message, $code); $this->flash($type, $title, $message, $code);
@ -178,7 +184,7 @@ abstract class OpenVKPresenter extends SimplePresenter
{ {
$user = Authenticator::i()->getUser(); $user = Authenticator::i()->getUser();
$this->template->isXmas = intval(date('d')) >= 15 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
if(!is_null($user)) { if(!is_null($user)) {
$this->user = (object) []; $this->user = (object) [];
@ -201,6 +207,9 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->user->identity->save(); $this->user->identity->save();
} }
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByuId($this->user->id, 1);
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0))
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
} }
setlocale(LC_TIME, ...(explode(";", tr("__locale")))); setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
@ -223,5 +232,14 @@ abstract class OpenVKPresenter extends SimplePresenter
$this->template->flashMessage = json_decode(Session::i()->get("_error")); $this->template->flashMessage = json_decode(Session::i()->get("_error"));
Session::i()->set("_error", NULL); Session::i()->set("_error", NULL);
} }
if(Session::i()->get("_tempTheme"))
$this->template->theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")];
else if($this->requestParam("themePreview"))
$this->template->theme = Themepacks::i()[$this->requestParam("themePreview")];
else if($this->user->identity !== null && $this->user->identity->getTheme())
$this->template->theme = $this->user->identity->getTheme();
// Знаю, каша ебаная, целестора рефактор всё равно сделает :)))
} }
} }

View file

@ -159,6 +159,17 @@ final class PhotosPresenter extends OpenVKPresenter
$this->template->comments = iterator_to_array($photo->getComments($this->template->cPage)); $this->template->comments = iterator_to_array($photo->getComments($this->template->cPage));
} }
function renderAbsolutePhoto($id): void
{
$id = (int) base_convert((string) $id, 32, 10);
$photo = $this->photos->get($id);
if(!$photo || $photo->isDeleted())
$this->notFound();
$this->template->_template = "Photos/Photo.xml";
$this->renderPhoto($photo->getOwner(true)->getId(), $photo->getVirtualId());
}
function renderEditPhoto(int $ownerId, int $photoId): void function renderEditPhoto(int $ownerId, int $photoId): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -181,7 +181,7 @@ final class SupportPresenter extends OpenVKPresenter
$comment = new TicketComment; $comment = new TicketComment;
$comment->setUser_id($this->user->id); $comment->setUser_id($this->user->id);
$comment->setUser_type(1); $comment->setUser_type(1);
$comment->setText('Здравствуйте, '.$ticket->getUser()->getFirstName().'!<br></br>'.$this->postParam("text").'<br></br>С уважением,<br/> Команда поддержки OpenVK.'); $comment->setText($this->postParam("text"));
$comment->setTicket_id($id); $comment->setTicket_id($id);
$comment->setCreated(time()); $comment->setCreated(time());
$comment->save(); $comment->save();

View file

@ -4,9 +4,14 @@ use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\Photo; use openvk\Web\Models\Entities\Photo;
use openvk\Web\Models\Repositories\Users; use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\Repositories\Albums; use openvk\Web\Models\Repositories\Albums;
use openvk\Web\Models\Repositories\Videos; use openvk\Web\Models\Repositories\Videos;
use openvk\Web\Models\Repositories\Notes; use openvk\Web\Models\Repositories\Notes;
use openvk\Web\Models\Repositories\Vouchers;
use Chandler\Security\Authenticator;
use lfkeitel\phptotp\{Base32, Totp};
use chillerlan\QRCode\{QRCode, QROptions};
final class UserPresenter extends OpenVKPresenter final class UserPresenter extends OpenVKPresenter
{ {
@ -29,18 +34,14 @@ final class UserPresenter extends OpenVKPresenter
if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode()) if(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH) !== "/" . $user->getShortCode())
$this->redirect("/" . $user->getShortCode(), static::REDIRECT_TEMPORARY_PRESISTENT); $this->redirect("/" . $user->getShortCode(), static::REDIRECT_TEMPORARY_PRESISTENT);
$then = date_create("@" . $user->getOnline()->timestamp());
$now = date_create();
$diff = date_diff($now, $then);
$this->template->albums = (new Albums)->getUserAlbums($user); $this->template->albums = (new Albums)->getUserAlbums($user);
$this->template->albumsCount = (new Albums)->getUserAlbumsCount($user); $this->template->albumsCount = (new Albums)->getUserAlbumsCount($user);
$this->template->videos = (new Videos)->getByUser($user, 1, 2); $this->template->videos = (new Videos)->getByUser($user, 1, 2);
$this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->videosCount = (new Videos)->getUserVideosCount($user);
$this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4);
$this->template->notesCount = (new Notes)->getUserNotesCount($user); $this->template->notesCount = (new Notes)->getUserNotesCount($user);
$this->template->user = $user; $this->template->user = $user;
$this->template->diff = $diff;
} }
} }
@ -81,9 +82,42 @@ final class UserPresenter extends OpenVKPresenter
} else { } else {
$this->template->user = $user; $this->template->user = $user;
$this->template->page = $this->queryParam("p") ?? 1; $this->template->page = $this->queryParam("p") ?? 1;
$this->template->admin = $this->queryParam("act") == "managed";
} }
} }
function renderPinClub(): void
{
$this->assertUserLoggedIn();
$club = (new Clubs)->get((int) $this->queryParam("club"));
if(!$club)
$this->notFound();
if(!$club->canBeModifiedBy($this->user->identity ?? NULL))
$this->flashFail("err", "Ошибка доступа", "У вас недостаточно прав, чтобы изменять этот ресурс.");
$isClubPinned = $this->user->identity->isClubPinned($club);
if(!$isClubPinned && $this->user->identity->getPinnedClubCount() > 10)
$this->flashFail("err", "Ошибка", "Находится в левом меню могут максимум 10 групп");
if($club->getOwner()->getId() === $this->user->identity->getId()) {
$club->setOwner_Club_Pinned(!$isClubPinned);
$club->save();
} else {
$manager = $club->getManager($this->user->identity);
if(!is_null($manager)) {
$manager->setClub_Pinned(!$isClubPinned);
$manager->save();
}
}
if($isClubPinned)
$this->flashFail("succ", "Операция успешна", "Группа " . $club->getName() . " была успешно удалена из левого меню");
else
$this->flashFail("succ", "Операция успешна", "Группа " . $club->getName() . " была успешно добавлена в левое меню");
}
function renderEdit(): void function renderEdit(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
@ -108,7 +142,7 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0) if ($this->postParam("marialstatus") <= 8 && $this->postParam("marialstatus") >= 0)
$user->setMarital_Status($this->postParam("marialstatus")); $user->setMarital_Status($this->postParam("marialstatus"));
if ($this->postParam("politViews") <= 8 && $this->postParam("politViews") >= 0) if ($this->postParam("politViews") <= 9 && $this->postParam("politViews") >= 0)
$user->setPolit_Views($this->postParam("politViews")); $user->setPolit_Views($this->postParam("politViews"));
if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0)
@ -125,9 +159,15 @@ final class UserPresenter extends OpenVKPresenter
} }
} elseif($_GET['act'] === "contacts") { } elseif($_GET['act'] === "contacts") {
$user->setEmail_Contact(empty($this->postParam("email_contact")) ? NULL : $this->postParam("email_contact")); $user->setEmail_Contact(empty($this->postParam("email_contact")) ? NULL : $this->postParam("email_contact"));
$user->setTelegram(empty($this->postParam("telegram")) ? NULL : $this->postParam("telegram")); $user->setTelegram(empty($this->postParam("telegram")) ? NULL : ltrim($this->postParam("telegram"), "@"));
$user->setCity(empty($this->postParam("city")) ? NULL : $this->postParam("city")); $user->setCity(empty($this->postParam("city")) ? NULL : $this->postParam("city"));
$user->setAddress(empty($this->postParam("address")) ? NULL : $this->postParam("address")); $user->setAddress(empty($this->postParam("address")) ? NULL : $this->postParam("address"));
$website = $this->postParam("website") ?? "";
if(empty($website))
$user->setWebsite(NULL);
else
$user->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website);
} elseif($_GET['act'] === "interests") { } elseif($_GET['act'] === "interests") {
$user->setInterests(empty($this->postParam("interests")) ? NULL : ovk_proc_strtr($this->postParam("interests"), 300)); $user->setInterests(empty($this->postParam("interests")) ? NULL : ovk_proc_strtr($this->postParam("interests"), 300));
$user->setFav_Music(empty($this->postParam("fav_music")) ? NULL : ovk_proc_strtr($this->postParam("fav_music"), 300)); $user->setFav_Music(empty($this->postParam("fav_music")) ? NULL : ovk_proc_strtr($this->postParam("fav_music"), 300));
@ -136,6 +176,18 @@ final class UserPresenter extends OpenVKPresenter
$user->setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300)); $user->setFav_Books(empty($this->postParam("fav_books")) ? NULL : ovk_proc_strtr($this->postParam("fav_books"), 300));
$user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300)); $user->setFav_Quote(empty($this->postParam("fav_quote")) ? NULL : ovk_proc_strtr($this->postParam("fav_quote"), 300));
$user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300)); $user->setAbout(empty($this->postParam("about")) ? NULL : ovk_proc_strtr($this->postParam("about"), 300));
} elseif($_GET['act'] === "status") {
if(mb_strlen($this->postParam("status")) > 255) {
$statusLength = (string) mb_strlen($this->postParam("status"));
$this->flashFail("err", "Ошибка", "Статус слишком длинный ($statusLength символов вместо 255 символов)");
}
$user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status"));
$user->save();
header("HTTP/1.1 302 Found");
header("Location: /id" . $user->getId());
exit;
} }
try { try {
@ -224,6 +276,9 @@ final class UserPresenter extends OpenVKPresenter
if(!$id) if(!$id)
$this->notFound(); $this->notFound();
if(in_array($this->queryParam("act"), ["finance", "finance.top-up"]) && !OPENVK_ROOT_CONF["openvk"]["preferences"]["commerce"])
$this->flashFail("err", tr("error"), tr("feature_disabled"));
$user = $this->users->get($id); $user = $this->users->get($id);
if($_SERVER["REQUEST_METHOD"] === "POST") { if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->willExecuteWriteAction(); $this->willExecuteWriteAction();
@ -231,6 +286,12 @@ final class UserPresenter extends OpenVKPresenter
if($_GET['act'] === "main" || $_GET['act'] == NULL) { if($_GET['act'] === "main" || $_GET['act'] == NULL) {
if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) { if($this->postParam("old_pass") && $this->postParam("new_pass") && $this->postParam("repeat_pass")) {
if($this->postParam("new_pass") === $this->postParam("repeat_pass")) { if($this->postParam("new_pass") === $this->postParam("repeat_pass")) {
if($this->user->identity->is2faEnabled()) {
$code = $this->postParam("code");
if(!($code === (new Totp)->GenerateToken(Base32::decode($this->user->identity->get2faSecret())) || $this->user->identity->use2faBackupCode((int) $code)))
$this->flashFail("err", tr("error"), tr("incorrect_2fa_code"));
}
if(!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass"))) if(!$this->user->identity->getChandlerUser()->updatePassword($this->postParam("new_pass"), $this->postParam("old_pass")))
$this->flashFail("err", tr("error"), tr("error_old_password")); $this->flashFail("err", tr("error"), tr("error_old_password"));
} else { } else {
@ -240,7 +301,7 @@ final class UserPresenter extends OpenVKPresenter
if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc"))) if(!$user->setShortCode(empty($this->postParam("sc")) ? NULL : $this->postParam("sc")))
$this->flashFail("err", tr("error"), tr("error_shorturl_incorrect")); $this->flashFail("err", tr("error"), tr("error_shorturl_incorrect"));
}elseif($_GET['act'] === "privacy") { } else if($_GET['act'] === "privacy") {
$settings = [ $settings = [
"page.read", "page.read",
"page.info.read", "page.info.read",
@ -256,9 +317,27 @@ final class UserPresenter extends OpenVKPresenter
$input = $this->postParam(str_replace(".", "_", $setting)); $input = $this->postParam(str_replace(".", "_", $setting));
$user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); $user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting))));
} }
}elseif($_GET['act'] === "interface") { } else if($_GET['act'] === "finance.top-up") {
$token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3");
$voucher = (new Vouchers)->getByToken($token);
if(!$voucher)
$this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad"));
$perm = $voucher->willUse($user);
if(!$perm)
$this->flashFail("err", tr("invalid_voucher"), tr("voucher_bad"));
$user->setCoins($user->getCoins() + $voucher->getCoins());
$user->setRating($user->getRating() + $voucher->getRating());
$user->save();
$this->flashFail("succ", tr("voucher_good"), tr("voucher_redeemed"));
} else if($_GET['act'] === "interface") {
if (isset(Themepacks::i()[$this->postParam("style")]) || $this->postParam("style") === Themepacks::DEFAULT_THEME_ID) if (isset(Themepacks::i()[$this->postParam("style")]) || $this->postParam("style") === Themepacks::DEFAULT_THEME_ID)
{
$user->setStyle($this->postParam("style")); $user->setStyle($this->postParam("style"));
$this->setTempTheme($this->postParam("style"));
}
if ($this->postParam("style_avatar") <= 2 && $this->postParam("style_avatar") >= 0) if ($this->postParam("style_avatar") <= 2 && $this->postParam("style_avatar") >= 0)
$user->setStyle_Avatar((int)$this->postParam("style_avatar")); $user->setStyle_Avatar((int)$this->postParam("style_avatar"));
@ -271,7 +350,7 @@ final class UserPresenter extends OpenVKPresenter
if(in_array($this->postParam("nsfw"), [0, 1, 2])) if(in_array($this->postParam("nsfw"), [0, 1, 2]))
$user->setNsfwTolerance((int) $this->postParam("nsfw")); $user->setNsfwTolerance((int) $this->postParam("nsfw"));
}elseif($_GET['act'] === "lMenu") { } else if($_GET['act'] === "lMenu") {
$settings = [ $settings = [
"menu_bildoj" => "photos", "menu_bildoj" => "photos",
"menu_filmetoj" => "videos", "menu_filmetoj" => "videos",
@ -296,14 +375,76 @@ final class UserPresenter extends OpenVKPresenter
$this->flash( $this->flash(
"succ", "succ",
"Изменения сохранены", "Изменения сохранены",
"Новые данные появятся на вашей странице.<br/>Если вы изменили стиль, перезагрузите страницу." "Новые данные появятся на вашей странице."
); );
} }
$this->template->mode = in_array($this->queryParam("act"), [ $this->template->mode = in_array($this->queryParam("act"), [
"main", "privacy", "finance", "interface" "main", "privacy", "finance", "finance.top-up", "interface"
]) ? $this->queryParam("act") ]) ? $this->queryParam("act")
: "main"; : "main";
$this->template->user = $user; $this->template->user = $user;
$this->template->themes = Themepacks::i()->getThemeList(); $this->template->themes = Themepacks::i()->getThemeList();
} }
function renderTwoFactorAuthSettings(): void
{
$this->assertUserLoggedIn();
if($this->user->identity->is2faEnabled()) {
if($_SERVER["REQUEST_METHOD"] === "POST") {
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
$this->flashFail("err", tr("error"), tr("incorrect_password"));
$this->user->identity->generate2faBackupCodes();
$this->template->_template = "User/TwoFactorAuthCodes.xml";
$this->template->codes = $this->user->identity->get2faBackupCodes();
return;
}
$this->redirect("/settings");
}
$secret = Base32::encode(Totp::GenerateSecret(16));
if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->willExecuteWriteAction();
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
$this->flashFail("err", tr("error"), tr("incorrect_password"));
$secret = $this->postParam("secret");
$code = $this->postParam("code");
if($code === (new Totp)->GenerateToken(Base32::decode($secret))) {
$this->user->identity->set2fa_secret($secret);
$this->user->identity->save();
$this->flash("succ", tr("two_factor_authentication_enabled_message"), tr("two_factor_authentication_enabled_message_description"));
$this->redirect("/settings");
}
$this->template->secret = $secret;
$this->flash("err", tr("error"), tr("incorrect_code"));
} else {
$this->template->secret = $secret;
}
$issuer = OPENVK_ROOT_CONF["openvk"]["appearance"]["name"];
$email = $this->user->identity->getEmail();
$this->template->qrCode = substr((new QRCode(new QROptions([
"imageTransparent" => false
])))->render("otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer"), 22);
}
function renderDisableTwoFactorAuth(): void
{
$this->assertUserLoggedIn();
$this->willExecuteWriteAction();
if(!Authenticator::verifyHash($this->postParam("password"), $this->user->identity->getChandlerUser()->getRaw()->passwordHash))
$this->flashFail("err", tr("error"), tr("incorrect_password"));
$this->user->identity->set2fa_secret(NULL);
$this->user->identity->save();
$this->flashFail("succ", tr("information_-1"), tr("two_factor_authentication_disabled_message"));
}
} }

View file

@ -5,6 +5,7 @@ use Chandler\Database\DatabaseConnection as DB;
use openvk\VKAPI\Exceptions\APIErrorException; use openvk\VKAPI\Exceptions\APIErrorException;
use openvk\Web\Models\Entities\{User, APIToken}; use openvk\Web\Models\Entities\{User, APIToken};
use openvk\Web\Models\Repositories\{Users, APITokens}; use openvk\Web\Models\Repositories\{Users, APITokens};
use lfkeitel\phptotp\{Base32, Totp};
final class VKAPIPresenter extends OpenVKPresenter final class VKAPIPresenter extends OpenVKPresenter
{ {
@ -161,6 +162,10 @@ final class VKAPIPresenter extends OpenVKPresenter
$uId = $chUser->related("profiles.user")->fetch()->id; $uId = $chUser->related("profiles.user")->fetch()->id;
$user = (new Users)->get($uId); $user = (new Users)->get($uId);
$code = $this->requestParam("code");
if($user->is2faEnabled() && !($code === (new Totp)->GenerateToken(Base32::decode($user->get2faSecret())) || $user->use2faBackupCode((int) $code)))
$this->fail(28, "Invalid 2FA code", "internal", "acquireToken");
$token = new APIToken; $token = new APIToken;
$token->setUser($user); $token->setUser($user);
$token->save(); $token->save();

View file

@ -68,6 +68,8 @@ final class VideosPresenter extends OpenVKPresenter
$video->setLink($this->postParam("link")); $video->setLink($this->postParam("link"));
else else
$this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку."); $this->flashFail("err", "Нету видеозаписи", "Выберите файл или укажите ссылку.");
} catch(\DomainException $ex) {
$this->flashFail("err", "Произошла ошибка", "Файл повреждён или не содержит видео." );
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна."); $this->flashFail("err", "Произошла ошибка", "Возможно, ссылка некорректна.");
} }

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\{Post, Photo, Club, User}; use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{LikeNotification, RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Nette\InvalidStateException as ISE; use Nette\InvalidStateException as ISE;
@ -45,17 +45,21 @@ final class WallPresenter extends OpenVKPresenter
exit("Ошибка доступа: " . (string) random_int(0, 255)); exit("Ошибка доступа: " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) if(is_null($this->user)) {
$canPost = false; $canPost = false;
else if($user > 0) } else if($user > 0) {
if(!$owner->isBanned())
$canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity); $canPost = $owner->getPrivacyPermission("wall.write", $this->user->identity);
else if($user < 0) else
$this->flashFail("err", tr("error"), "Ошибка доступа");
} else if($user < 0) {
if($owner->canBeModifiedBy($this->user->identity)) if($owner->canBeModifiedBy($this->user->identity))
$canPost = true; $canPost = true;
else else
$canPost = $owner->canPost(); $canPost = $owner->canPost();
else } else {
$canPost = false; $canPost = false;
}
if ($embedded == true) $this->template->_template = "components/wall.xml"; if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner; $this->template->oObj = $owner;
@ -164,21 +168,33 @@ final class WallPresenter extends OpenVKPresenter
$wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1)) $wallOwner = ($wall > 0 ? (new Users)->get($wall) : (new Clubs)->get($wall * -1))
?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует."); ?? $this->flashFail("err", "Не удалось опубликовать пост", "Такого пользователя не существует.");
if($wall > 0) if($wall > 0) {
if(!$wallOwner->isBanned())
$canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity); $canPost = $wallOwner->getPrivacyPermission("wall.write", $this->user->identity);
else if($wall < 0) else
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену.");
} else if($wall < 0) {
if($wallOwner->canBeModifiedBy($this->user->identity)) if($wallOwner->canBeModifiedBy($this->user->identity))
$canPost = true; $canPost = true;
else else
$canPost = $wallOwner->canPost(); $canPost = $wallOwner->canPost();
else } else {
$canPost = false; $canPost = false;
}
if(!$canPost) if(!$canPost)
$this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену."); $this->flashFail("err", "Ошибка доступа", "Вам нельзя писать на эту стену.");
if(false) $anon = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["enable"];
$this->flashFail("err", "Не удалось опубликовать пост", "Пост слишком большой."); if($wallOwner instanceof Club && $this->postParam("as_group") === "on" && $this->postParam("force_sign") !== "on" && $anon) {
$manager = $wallOwner->getManager($this->user->identity);
if($manager)
$anon = $manager->isHidden();
elseif($this->user->identity->getId() === $wallOwner->getOwner()->getId())
$anon = $wallOwner->isOwnerHidden();
} else {
$anon = $anon && $this->postParam("anon") === "on";
}
$flags = 0; $flags = 0;
if($this->postParam("as_group") === "on") if($this->postParam("as_group") === "on")
@ -186,49 +202,49 @@ final class WallPresenter extends OpenVKPresenter
if($this->postParam("force_sign") === "on") if($this->postParam("force_sign") === "on")
$flags |= 0b01000000; $flags |= 0b01000000;
try {
$photo = NULL;
$video = NULL;
if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_pic_attachment"]["error"] === UPLOAD_ERR_OK) {
try { $album = NULL;
$photo = new Photo; if(!$anon && $wall > 0 && $wall === $this->user->id)
$photo->setOwner($this->user->id); $album = (new Albums)->getUserWallAlbum($wallOwner);
$photo->setDescription(iconv_substr($this->postParam("text"), 0, 36) . "...");
$photo->setCreated(time());
$photo->setFile($_FILES["_pic_attachment"]);
$photo->save();
if($wall > 0 && $wall === $this->user->id) { $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
(new Albums)->getUserWallAlbum($wallOwner)->addPhoto($photo);
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён.");
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Файл изображения повреждён, слишком велик или одна сторона изображения в разы больше другой."); $this->flashFail("err", "Не удалось опубликовать пост", "Файл медиаконтента повреждён или слишком велик.");
} }
$post = new Post; if(empty($this->postParam("text")) && !$photo && !$video)
$post->setOwner($this->user->id); $this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой.");
$post->setWall($wall);
$post->setCreated(time());
$post->setContent($this->postParam("text"));
$post->setFlags($flags);
$post->setNsfw($this->postParam("nsfw") === "on");
$post->save();
$post->attach($photo);
} elseif($this->postParam("text")) {
try { try {
$post = new Post; $post = new Post;
$post->setOwner($this->user->id); $post->setOwner($this->user->id);
$post->setWall($wall); $post->setWall($wall);
$post->setCreated(time()); $post->setCreated(time());
$post->setContent($this->postParam("text")); $post->setContent($this->postParam("text"));
$post->setAnonymous($anon);
$post->setFlags($flags); $post->setFlags($flags);
$post->setNsfw($this->postParam("nsfw") === "on"); $post->setNsfw($this->postParam("nsfw") === "on");
$post->save(); $post->save();
} catch(\LogicException $ex) { } catch (\LengthException $ex) {
$this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой."); $this->flashFail("err", "Не удалось опубликовать пост", "Пост слишком большой.");
}
} else {
$this->flashFail("err", "Не удалось опубликовать пост", "Пост пустой или слишком большой.");
} }
if(!is_null($photo))
$post->attach($photo);
if(!is_null($video))
$post->attach($video);
if($wall > 0 && $wall !== $this->user->identity->getId()) if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
@ -250,10 +266,11 @@ final class WallPresenter extends OpenVKPresenter
$this->logPostView($post, $wall); $this->logPostView($post, $wall);
$this->template->post = $post; $this->template->post = $post;
if ($post->getTargetWall() > 0) if ($post->getTargetWall() > 0) {
{
$this->template->wallOwner = (new Users)->get($post->getTargetWall()); $this->template->wallOwner = (new Users)->get($post->getTargetWall());
$this->template->isWallOfGroup = false; $this->template->isWallOfGroup = false;
if($this->template->wallOwner->isBanned())
$this->flashFail("err", tr("error"), "Ошибка доступа");
} else { } else {
$this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall())); $this->template->wallOwner = (new Clubs)->get(abs($post->getTargetWall()));
$this->template->isWallOfGroup = true; $this->template->isWallOfGroup = true;
@ -274,9 +291,6 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($this->user)) { if(!is_null($this->user)) {
$post->toggleLike($this->user->identity); $post->toggleLike($this->user->identity);
if($post->getOwner(false)->getId() !== $this->user->identity->getId() && !($post->getOwner() instanceof Club))
(new LikeNotification($post->getOwner(false), $post, $this->user->identity))->emit();
} }
$this->redirect( $this->redirect(
@ -298,7 +312,7 @@ final class WallPresenter extends OpenVKPresenter
$nPost = new Post; $nPost = new Post;
$nPost->setOwner($this->user->id); $nPost->setOwner($this->user->id);
$nPost->setWall($this->user->id); $nPost->setWall($this->user->id);
$nPost->setContent(""); $nPost->setContent($this->postParam("text"));
$nPost->save(); $nPost->save();
$nPost->attach($post); $nPost->attach($post);
@ -306,8 +320,7 @@ final class WallPresenter extends OpenVKPresenter
(new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit(); (new RepostNotification($post->getOwner(false), $post, $this->user->identity))->emit();
}; };
$this->flash("succ", "Успешно", "Запись появится на вашей стене. <a href='/wall" . $wall . "_" . $post_id . "'>Вернуться к записи.</a>"); exit(json_encode(["wall_owner" => $this->user->identity->getId()]));
$this->redirect($this->user->identity->getURL());
} }
function renderDelete(int $wall, int $post_id): void function renderDelete(int $wall, int $post_id): void

View file

@ -1,4 +1,7 @@
<div class="ovk-lw-container"> {extends "@layout.xml"}
{block wrap}
<div class="ovk-lw-container">
<div class="ovk-lw--list"> <div class="ovk-lw--list">
{var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} {var data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
@ -54,4 +57,5 @@
<a href="?C=M;O=A" class="profile_link">{_"sort_down"}</a> <a href="?C=M;O=A" class="profile_link">{_"sort_down"}</a>
</div> </div>
</div> </div>
</div> </div>
{/block}

View file

@ -1,14 +1,18 @@
{var instance_name = OPENVK_ROOT_CONF['openvk']['appearance']['name']}
<html n:if="!isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'"> <html n:if="!isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'">
<head> <head>
<title> <title>
{ifset title}{include title} - {/ifset}OpenVK {ifset title}{include title} - {/ifset}{$instance_name}
</title> </title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" /> <link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta name="application-name" content="OpenVK" /> <meta name="application-name" content="{$instance_name}" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" /> <meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{php echo getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{ifset $thisUser} {ifset $thisUser}
@ -16,11 +20,11 @@
{css "css/nsfw-posts.css"} {css "css/nsfw-posts.css"}
{/if} {/if}
{if !is_null($thisUser->getTheme())} {if $theme !== null}
{var theme = $thisUser->getTheme()}
{if $theme->inheritDefault()} {if $theme->inheritDefault()}
{css "css/style.css"} {css "css/style.css"}
{css "css/dialog.css"} {css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas} {if $isXmas}
{css "css/xmas.css"} {css "css/xmas.css"}
{/if} {/if}
@ -28,11 +32,12 @@
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" /> <link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas} {if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/xmas.css" /> <link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if} {/if}
{else} {else}
{css "css/style.css"} {css "css/style.css"}
{css "css/dialog.css"} {css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas} {if $isXmas}
{css "css/xmas.css"} {css "css/xmas.css"}
{/if} {/if}
@ -53,6 +58,7 @@
{css "css/style.css"} {css "css/style.css"}
{css "css/dialog.css"} {css "css/dialog.css"}
{css "css/nsfw-posts.css"} {css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas} {if $isXmas}
{css "css/xmas.css"} {css "css/xmas.css"}
@ -71,6 +77,7 @@
</div> </div>
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['bellsAndWhistles']['testLabel']" id="test-label">FOR TESTING PURPOSES ONLY</div> <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['bellsAndWhistles']['testLabel']" id="test-label">FOR TESTING PURPOSES ONLY</div>
<div class="notifications_global_wrap"></div>
<div class="dimmer"></div> <div class="dimmer"></div>
<div class="toTop"> <div class="toTop">
⬆ Вверх ⬆ Вверх
@ -78,8 +85,8 @@
<div class="layout"> <div class="layout">
<div id="xhead" class="dm"></div> <div id="xhead" class="dm"></div>
<div class="page_header"> <div class="page_header {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}page_custom_header{/if}">
<a href="/" class="home_button" title="OpenVK">openvk</a> <a href="/" class="home_button {if $instance_name != OPENVK_DEFAULT_INSTANCE_NAME}home_button_custom{/if}" title="{$instance_name}">{$instance_name}</a>
<div n:if="isset($thisUser) ? !$thisUser->isBanned() : true" class="header_navigation"> <div n:if="isset($thisUser) ? !$thisUser->isBanned() : true" class="header_navigation">
{ifset $thisUser} {ifset $thisUser}
<div class="link"> <div class="link">
@ -95,14 +102,17 @@
<a href="/search">{_"header_search"}</a> <a href="/search">{_"header_search"}</a>
</div> </div>
<div class="link"> <div class="link">
<a href="/support">{_"header_help"}</a> <a href="/support">
{_"header_help"}
<b n:if="$ticketAnsweredCount > 0">({$ticketAnsweredCount})</b>
</a>
</div> </div>
<div class="link"> <div class="link">
<a href="/logout">{_"header_log_out"}</a> <a href="/logout?hash={urlencode($csrfToken)}">{_"header_log_out"}</a>
</div> </div>
<div class="link"> <div class="link">
<form action="/search" method="get"> <form action="/search" method="get">
<input type="search" name="query" placeholder="{_"header_search"}" style="background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" /> <input type="search" name="query" placeholder="{_"header_search"}" style="height: 20px;background: url('/assets/packages/static/openvk/img/search_icon.png') no-repeat 3px 4px; background-color: #fff; padding-left: 18px;width: 120px;" />
</form> </form>
</div> </div>
@ -154,12 +164,19 @@
{/if} {/if}
</a> </a>
<a href="/settings" class="link">{_"my_settings"}</a> <a href="/settings" class="link">{_"my_settings"}</a>
<div style="height: 1px;background: #CCC;margin: 4px 0 2px;"></div> {var canAccessAdminPanel = $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
{if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {var canAccessHelpdesk = $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
{var menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0}
<div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div>
{if $canAccessAdminPanel}
<a href="/admin" class="link">Админ-панель</a> <a href="/admin" class="link">Админ-панель</a>
{/if} {/if}
{if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {if $canAccessHelpdesk}
<a href="/support/tickets" class="link">Helpdesk</a> <a href="/support/tickets" class="link">Helpdesk
{if $helpdeskTicketAnsweredCount > 0}
(<b>{$helpdeskTicketNotAnsweredCount}</b>)
{/if}
</a>
<a href="/admin/reports" class="link">Reports</a> <a href="/admin/reports" class="link">Reports</a>
{/if} {/if}
<a <a
@ -167,7 +184,14 @@
href="{$menuItem['url']}" href="{$menuItem['url']}"
target="_blank" target="_blank"
class="link">{$menuItem["name"]}</a> class="link">{$menuItem["name"]}</a>
<div id="_groupListPinnedGroups">
<div n:if="$thisUser->getPinnedClubCount() > 0" class="menu_divider"></div>
<a
n:foreach="$thisUser->getPinnedClubs() as $club"
href="{$club->getURL()}"
class="link group_link">{$club->getName()}</a>
</div>
<a <a
n:if="OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['enable']" n:if="OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['enable']"
href="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['link']}" > href="{php echo OPENVK_ROOT_CONF['openvk']['preferences']['adPoster']['link']}" >
@ -177,7 +201,6 @@
class="psa-poster" class="psa-poster"
style="max-width: 100%; margin-top: 50px;" /> style="max-width: 100%; margin-top: 50px;" />
</a> </a>
{else} {else}
<a href="/support" class="link">Поддержка</a> <a href="/support" class="link">Поддержка</a>
<a href="/logout" class="link">Выйти</a> <a href="/logout" class="link">Выйти</a>
@ -248,19 +271,25 @@
<a href="/language" class="link">{_footer_choose_language}</a> <a href="/language" class="link">{_footer_choose_language}</a>
<a href="/privacy" class="link">{_footer_privacy}</a> <a href="/privacy" class="link">{_footer_privacy}</a>
</div> </div>
<p>OpenVK <a href="/about:openvk2">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p> <p>OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {$dbVersion}</p>
<p n:ifcontent> <p n:ifcontent="ifcontent">
{php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]} {php echo OPENVK_ROOT_CONF["openvk"]["appearance"]["motd"]}
</p> </p>
</div> </div>
<script src="https://rawgit.com/kawanet/msgpack-lite/master/dist/msgpack.min.js"></script> {script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/node_modules/soundjs/lib/soundjs.min.js"}
{script "js/node_modules/ky/umd.js"} {script "js/node_modules/ky/umd.js"}
{script "js/messagebox.js"} {script "js/messagebox.js"}
{script "js/notifications.js"}
{script "js/scroll.js"} {script "js/scroll.js"}
{script "js/al_wall.js"} {script "js/al_wall.js"}
{script "js/al_api.js"} {script "js/al_api.js"}
{ifset $thisUser}
{script "js/al_notifs.js"}
{/ifset}
<script src="https://unpkg.com/fartscroll@1.0.0/fartscroll.js"></script> <script src="https://unpkg.com/fartscroll@1.0.0/fartscroll.js"></script>
<script n:if="OPENVK_ROOT_CONF['openvk']['preferences']['bellsAndWhistles']['fartscroll']"> <script n:if="OPENVK_ROOT_CONF['openvk']['preferences']['bellsAndWhistles']['fartscroll']">
fartscroll(400); fartscroll(400);
@ -269,6 +298,10 @@
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" <script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']"
async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}"
src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script> src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script>
{ifset bodyScripts}
{include bodyScripts}
{/ifset}
</body> </body>
</html> </html>

View file

@ -40,6 +40,7 @@
"count" => $count, "count" => $count,
"amount" => sizeof($data), "amount" => sizeof($data),
"perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE,
"atBottom" => true,
]} ]}
</div> </div>
{else} {else}

View file

@ -6,21 +6,10 @@
{/block} {/block}
{block content} {block content}
<b>OpenVK - универсальное средство поиска коллег основанное на структуре ВКонтакте.</b><br> {presenter "openvk!Support->knowledgeBaseArticle", "about"}
<p>Мы хотим, чтобы друзья, однокурсники, одноклассники, соседи и коллеги всегда могли быть в контакте.</p>
<p>
Нас уже <b>{$stats->all}</b> пользователя и <b>{$stats->online}</b> из
<b>{$stats->active}</b> активных сейчас в сети.
</p>
<p>С помощью этого сайта Вы можете:</p>
<ul>
<li><span>Найти людей, с которыми Вы когда-либо учились, работали или отдыхали.</span></li>
<li><span>Узнать больше о людях, которые Вас окружают, и найти новых друзей.</span></li>
<li><span>Всегда оставаться в контакте с теми, кто Вам дорог.</span></li>
<li><span>Продвигать своё творчество и/или мнение.</span></li>
</ul>
<center> <center>
<a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_"log_in"}</a> <a class="button" style="margin-right: 5px;cursor: pointer;" href="/login">{_"log_in"}</a>
<a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" class="button" style="cursor: pointer;" href="/reg">{_"registration"}</a></center> <a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['registration']['enable']" class="button" style="cursor: pointer;" href="/reg">{_"registration"}</a>
</div> </center>
{* TO-DO: Add statistics about this instance as on mastodon.social *}
{/block} {/block}

View file

@ -8,7 +8,7 @@
{block content} {block content}
<div class="navigation"> <div class="navigation">
{foreach $languages as $language} {foreach $languages as $language}
<a href="language?lg={$language['code']}" class="link"><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif"> {$language['native_name']}</a> <a href="language?lg={$language['code']}&hash={urlencode($csrfToken)}" class="link"><img src="/assets/packages/static/openvk/img/flags/{$language['flag']}.gif"> {$language['native_name']}</a>
{/foreach} {/foreach}
</div> </div>
{/block} {/block}

View file

@ -79,7 +79,7 @@
#ovkLogo { #ovkLogo {
float: right; float: right;
border: 0; border: 0;
width: 30px; height: 30px;
padding-top: 6px; padding-top: 6px;
position: relative; position: relative;
} }
@ -97,7 +97,7 @@
<tr class="h"> <tr class="h">
<td> <td>
<h1 class="p" style="float: left;">OpenVK {=OPENVK_VERSION}</h1> <h1 class="p" style="float: left;">OpenVK {=OPENVK_VERSION}</h1>
<img id="ovkLogo" src="/assets/packages/static/openvk/img/logo.svg" alt="OpenVK Logo" /> <img id="ovkLogo" src="/assets/packages/static/openvk/img/logo_full.svg" alt="OpenVK Logo" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -397,8 +397,8 @@
<tr> <tr>
<td class="e"> <td class="e">
Vladimir Barinov (veselcraft), Alexandra Katunina (rem-pai), Konstantin Kichulkin (kosfurler), Vladimir Barinov (veselcraft), Alexandra Katunina (rem-pai), Konstantin Kichulkin (kosfurler),
Nikita Volkov (sup_ban), Daniil Myslivets (myslivets), Alexander Kotov (l-lacker), Nikita Volkov (sup_ban), Daniel Myslivets (myslivets), Alexander Kotov (l-lacker),
Alexey Assemblerov (BiosNod), Ilya Prokopenko (dsrev) and Vladimir Lapskiy (0x7d5) Alexey Assemblerov (BiosNod), Ilya Prokopenko (dsrev) and Maxim Leshchenko (maksales / maksalees)
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -412,7 +412,7 @@
<tr> <tr>
<td class="e"> <td class="e">
Vladimir Barinov (veselcraft) and Konstantin Kichulkin (kosfurler)<br/> Vladimir Barinov (veselcraft) and Konstantin Kichulkin (kosfurler)<br/>
OpenVK is a free open-source software that "cosplays" (or imitates) older versions of russian website VKontakte. VKontakte belongs to Pavel Durov and mail.ru. OpenVK is a free open-source software that "cosplays" (or imitates) older versions of russian website VKontakte. VKontakte belongs to Pavel Durov and VK Group.
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -69,9 +69,19 @@
Группы Группы
</a> </a>
</li> </li>
</ul>
<div class="aui-nav-heading">
<strong>Платные услуги</strong>
</div>
<ul class="aui-nav">
<li> <li>
<a href="/admin/files"> <a href="/admin/vouchers">
Загруженные файлы {_vouchers}
</a>
</li>
<li>
<a href="/admin/gifts">
Подарки
</a> </a>
</li> </li>
</ul> </ul>
@ -120,10 +130,28 @@
</nav> </nav>
</div> </div>
<section class="aui-page-panel-content"> <section class="aui-page-panel-content">
{ifset $flashMessage}
{var type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]}
<div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;">
<p class="title">
<strong>{$flashMessage->title}</strong>
</p>
<p>{$flashMessage->msg|noescape}</p>
</div>
{/ifset}
{ifset preHeader}
{include preHeader}
{/ifset}
<header class="aui-page-header"> <header class="aui-page-header">
<div class="aui-page-header-inner"> <div class="aui-page-header-inner">
<div class="aui-page-header-main"> <div class="aui-page-header-main">
{ifset headingWrap}
{include headingWrap}
{else}
<h1>{include heading}</h1> <h1>{include heading}</h1>
{/ifset}
</div> </div>
</div> </div>
</header> </header>
@ -141,5 +169,13 @@
</section> </section>
</footer> </footer>
</div> </div>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"}
<script>AJS.tabs.setup();</script>
{ifset scripts}
{include scripts}
{/ifset}
</body> </body>
</html> </html>

View file

@ -0,0 +1,106 @@
{extends "@layout.xml"}
{block title}
{if $form->id === 0}
Новый подарок
{else}
Подарок "{$form->name}"
{/if}
{/block}
{block heading}
{include title}
{/block}
{block content}
<form class="aui" method="POST" enctype="multipart/form-data">
<div class="field-group">
<label for="avatar">
Изображение
<span n:if="$form->id === 0" class="aui-icon icon-required"></span>
</label>
{if $form->id === 0}
<input type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" required="required" />
{else}
<span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge">
<span class="aui-avatar-inner">
<img id="pic" src="{$form->pic}" style="object-fit: cover;"></img>
</span>
</span>
<input style="display: none;" id="picInput" type="file" name="pic" accept="image/jpeg,image/png,image/gif,image/webp" />
<div class="description">
<a id="picChange" href="javascript:false">Заменить изображение?</a>
</div>
{/if}
</div>
<div class="field-group">
<label for="id">
ID
</label>
<input class="text long-field" type="number" id="id" disabled="disabled" value="{$form->id}" />
</div>
<div class="field-group">
<label for="putin">
Использований
</label>
<input class="text long-field" type="number" id="putin" disabled="disabled" value="{$form->usages}" />
<div n:if="$form->usages > 0" class="description">
<a href="javascript:$('#putin').value(0);">Обнулить?</a>
</div>
</div>
<div class="field-group">
<label for="name">
Внутренее имя
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name" name="name" value="{$form->name}" />
</div>
<div class="field-group">
<label for="price">
Цена
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" id="price" name="price" min="0" value="{$form->price}" />
</div>
<div class="field-group">
<label for="limit">
Ограничение
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="number" min="-1" id="limit" name="limit" value="{$form->limit}" />
</div>
<fieldset class="group">
<legend></legend>
<div class="checkbox" resolved="">
<input n:attr="disabled => $form->id === 0, checked => $form->id === 0" class="checkbox" type="checkbox" name="reset_limit" id="reset_limit" />
<span class="aui-form-glyph"></span>
<label for="reset_limit">Сбросить счётчик ограничений</label>
</div>
</fieldset>
<input n:if="$form->id === 0" type="hidden" name="_cat" value="{$_GET['cat'] ?? 1}" />
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
</div>
</div>
</form>
{/block}
{block scripts}
<script>
const TRANS_GIF = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
$("#picChange").click(_ => $("#picInput").click());
$("#picInput").bind("change", e => {
if(typeof e.target.files[0] === "undefined")
$("#pic").prop("src", URL.createObjectURL(TRANS_GIF));
$("#pic").prop("src", URL.createObjectURL(e.target.files[0]));
});
</script>
{/block}

View file

@ -0,0 +1,56 @@
{extends "@layout.xml"}
{block title}
Наборы подарков
{/block}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/gifts/new.0.meta">
{_create}
</a>
<h1>Наборы подарков</h1>
{/block}
{block content}
<div id="spacer" style="height: 8px;"></div>
{if sizeof($categories) > 0}
<table class="aui aui-table-list">
<tbody>
<tr n:foreach="$categories as $cat">
<td style="vertical-align: middle;">
<span class="aui-icon aui-icon-small aui-iconfont-folder-filled">{$cat->getName()}</span>
{$cat->getName()}
</td>
<td style="vertical-align: middle;">
{ovk_proc_strtr($cat->getDescription(), 128)}
</td>
<td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}.meta">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
<a class="aui-button" href="/admin/gifts/{$cat->getSlug()}.{$cat->getId()}/">
<span class="aui-icon aui-icon-small aui-iconfont-gallery">Открыть</span>
Открыть
</a>
</td>
</tr>
</tbody>
</table>
{else}
<center>
<p>Наборов подарков нету. Чтобы создать подарок, создайте набор.</p>
</center>
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($categories)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?act={$act}&p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div>
{/block}

View file

@ -0,0 +1,72 @@
{extends "@layout.xml"}
{block title}
{if $form->id === 0}
Создать набор подарков
{else}
{$form->languages["master"]->name}
{/if}
{/block}
{block heading}
{include title}
{/block}
{block content}
<form class="aui" method="POST">
<h3>Общие настройки</h3>
<fieldset>
<div class="field-group">
<label for="id">
ID
</label>
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div>
<div class="field-group">
<label for="name_master">
Наименование
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="name_master" name="name_master" value="{$form->languages['master']->name}" />
<div class="description">Внутреннее название набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
</div>
<div class="field-group">
<label for="description_master">
Описание
<span class="aui-icon icon-required"></span>
</label>
<input class="text long-field" type="text" id="description_master" name="description_master" value="{$form->languages['master']->description}" />
<div class="description">Внутреннее описание набора, которое будет использоваться, если не удаётся найти название на языке пользователя.</div>
</div>
</fieldset>
<h3>Языко-зависимые настройки</h3>
<fieldset>
{foreach $form->languages as $locale => $data}
{continueIf $locale === "master"}
<div class="field-group">
<label for="name_{$locale}">
Наименование
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="name_{$locale}" name="name_{$locale}" value="{$data->name}" />
</div>
<div class="field-group">
<label for="description_{$locale}">
Описание
<img src="/assets/packages/static/openvk/img/flags/{$locale}.gif" alt="{$locale}" />
</label>
<input class="text long-field" type="text" id="description_{$locale}" name="description_{$locale}" value="{$data->description}" />
</div>
{/foreach}
</fieldset>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
</div>
</div>
</form>
{/block}

View file

@ -0,0 +1,82 @@
{extends "@layout.xml"}
{block title}
{$cat->getName()}
{/block}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/gifts/id0?act=edit&cat={$cat->getId()}">
{_create}
</a>
<h1>Набор "{$cat->getName()}"</h1>
{/block}
{block content}
{if sizeof($gifts) > 0}
<table class="aui aui-table-list">
<thead>
<th>Подарок</th>
<th>Имя</th>
<th>Цена</th>
<th>Подарен</th>
<th>Ограничение</th>
<th>Сброс счётчика ограничений</th>
<th>Действия</th>
</thead>
<tbody>
<tr n:foreach="$gifts as $gift">
<td style="vertical-align: middle; width: 0px;">
<img style="max-width: 32px;" src="{$gift->getImage(2)}" alt="{$gift->getName()}" />
</td>
<td style="vertical-align: middle;">
{$gift->getName()}
<span n:if="$gift->isFree()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">
бесплатный
</span>
</td>
<td style="vertical-align: middle;">
{$gift->getPrice()} голосов
</td>
<td style="vertical-align: middle;">
{$gift->getUsages()} раз
</td>
<td style="vertical-align: middle;">
{if $gift->getLimit() === INF}
Отсутствует
{else}
Не более {$gift->getLimit()} дарений
{/if}
</td>
<td style="vertical-align: middle;">
{if !$gift->getLimitResetTime()}
Никогда
{else}
Последний раз в
{$gift->getLimitResetTime()->format("%a, %d %B %G")}
{/if}
</td>
<td style="vertical-align: middle; text-align: right;">
<a class="aui-button aui-button-primary" href="/admin/gifts/id{$gift->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
</td>
</tr>
</tbody>
</table>
{else}
<center>
<p>Подарков нету. Нажмите на красивую кнопку вверху, чтобы создать первый.</p>
</center>
{/if}
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($gifts)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div>
{/block}

View file

@ -0,0 +1,92 @@
{extends "@layout.xml"}
{block title}
{_edit}
{/block}
{block heading}
{_edit} №{$form->token ?? "undefined"}
{/block}
{block content}
<div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs">
<ul class="tabs-menu">
<li class="menu-item active-tab">
<a href="#info">Информация</a>
</li>
<li class="menu-item">
<a href="#activators">{_voucher_activators}</a>
</li>
</ul>
<div class="tabs-pane active-pane" id="info">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">
ID
</label>
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div>
<div class="field-group">
<label for="token">
Серийный номер
</label>
<input class="text long-field" type="text" id="token" name="token" value="{$form->token}" />
<div class="description">Номер состоит из 24 символов, если формат неправильный или поле не заполнено, будет назначен автоматически.</div>
</div>
<div class="field-group">
<label for="coins">
Количество голосов
</label>
<input class="text long-field" type="number" min="0" id="coins" name="coins" value="{$form->coins}" />
</div>
<div class="field-group">
<label for="rating">
Количество рейтинга
</label>
<input class="text long-field" type="number" min="0" id="rating" name="rating" value="{$form->rating}" />
</div>
<div class="field-group">
<label for="usages">
{if $form->id === 0}
{_usages_total}
{else}
{_usages_left}
{/if}
</label>
<input class="text long-field" type="number" min="-1" id="usages" name="usages" value="{$form->usages}" />
<div class="description">Количество аккаунтов, которые могут использовать ваучер. Если написать -1, будет Infinity.</div>
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
</div>
</div>
</form>
</div>
<div class="tabs-pane" id="activators">
<table rules="none" class="aui aui-table-list">
<tbody>
<tr n:foreach="$form->users as $user">
<td>
<span class="aui-avatar aui-avatar-xsmall">
<span class="aui-avatar-inner">
<img src="{$user->getAvatarUrl()}" alt="{$user->getCanonicalName()}" role="presentation" />
</span>
</span>
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a>
<span n:if="$user->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">
заблокирован
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/block}

View file

@ -0,0 +1,61 @@
{extends "@layout.xml"}
{block title}
{_vouchers}
{/block}
{block headingWrap}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/vouchers/id0">
{_create}
</a>
<h1>{_vouchers}</h1>
{/block}
{block content}
<table class="aui aui-table-list">
<thead>
<tr>
<th>#</th>
<th>Серийный номер</th>
<th>Голоса</th>
<th>Рейгтинг</th>
<th>Осталось использований</th>
<th>Состояние</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$vouchers as $voucher">
<td>{$voucher->getId()}</td>
<td>{$voucher->getFormattedToken()}</td>
<td>{$voucher->getCoins()}&#xa2;</td>
<td>{$voucher->getRating()}</td>
<td>{$voucher->getRemainingUsages() === INF ? "∞" : $voucher->getRemainingUsages()}</td>
<td>
{if $voucher->isExpired()}
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">закончился</span>
{else}
<span class="aui-lozenge aui-lozenge-subtle aui-lozenge-success">активен</span>
{/if}
</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/vouchers/id{$voucher->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span>
</a>
</td>
</tr>
</tbody>
</table>
<br/>
<div align="right">
{var isLast = ((20 * (($_GET['p'] ?? 1) - 1)) + sizeof($vouchers)) < $count}
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">
⭁ туда
</a>
<a n:if="$isLast" class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">
⭇ сюда
</a>
</div>
{/block}

View file

@ -17,6 +17,11 @@
<label for="password">Новый пароль: </label> <label for="password">Новый пароль: </label>
<input id="password" type="password" name="password" required /> <input id="password" type="password" name="password" required />
<br/><br/> <br/><br/>
{if $is2faEnabled}
<label for="code">Код двухфакторной аутентификации: </label>
<input id="code" type="text" name="code" required />
<br/><br/>
{/if}
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="Сбросить пароль" class="button" style="float: right;" /> <input type="submit" value="Сбросить пароль" class="button" style="float: right;" />

View file

@ -0,0 +1,38 @@
{extends "../@layout.xml"}
{block title}{_"log_in"}{/block}
{block header}
{_"log_in"}
{/block}
{block content}
<p>
{_"two_factor_authentication_login"}
</p>
<form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
<tbody>
<tr>
<td>
<span>{_code}: </span>
</td>
<td>
<input type="text" name="code" required />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="login" value="{$login}" />
<input type="hidden" name="password" value="{$password}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_'log_in'}" class="button" />
</td>
</tr>
</tbody>
</table>
</form>
{/block}

View file

@ -24,7 +24,7 @@
</p> </p>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center"> <table cellspacing="7" cellpadding="0" width="52%" border="0" align="center">
<tbody> <tbody>
<tr> <tr>
<td> <td>
@ -54,6 +54,14 @@
</select> </select>
</td> </td>
</tr> </tr>
<tr>
<td>
<span>{_"birth_date"}: </span>
</td>
<td>
<input max={date('Y-m-d')} name="birthday" type="date"/>
</td>
</tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td> <td>

View file

@ -0,0 +1,30 @@
{extends "../@layout.xml"}
{block title}
{_send_gift}
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">{_gift_select}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">{_collections}</a> »
<a href="/gifts?act=menu&user={$user->getId()}&pack={$cat->getId()}">{$cat->getName(tr("__lang"))}</a> »
{_confirm}
{/block}
{block content}
<center>
<img class="gift_confirm_pic" style="max-width: 256px;" src="{$gift->getImage(2)}" alt="Подарок" />
<form style="width: 65%;" method="POST">
<textarea name="comment" style="resize: vertical; height: 65px;" placeholder="{_gift_your_message}"></textarea>
<br/><br/>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_send}" class="button" />
<label>
<input type="checkbox" name="anonymous"> {_as_anonymous}
</label>
</form>
</center>
{/block}

View file

@ -0,0 +1,29 @@
{extends "../@listView.xml"}
{block title}
{_gift_select}
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">{_gift_select}</a> »
{_collections}
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
/gifts?act=menu&user={$user->getId()}&pack={$x->getId()}
{/block}
{block preview}
<img src="{$x->getThumbnailURL()}" width="75" alt="{$x->getName(tr('__lang'))}" />
{/block}
{block name}
{$x->getName(tr("__lang"))}
{/block}
{block description}
{$x->getDescription(tr("__lang"))}
{/block}

View file

@ -0,0 +1,58 @@
{extends "../@layout.xml"}
{block title}
{_gift_select}
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">{_gift_select}</a> »
<a href="/gifts?act=pick&user={$user->getId()}">{_collections}</a> »
{$cat->getName(tr("__lang"))}
{/block}
{block content}
<div class="gift_grid">
<div n:foreach="$gifts as $gift" n:class="gift_sel, !$gift->canUse($thisUser) ? disabled" data-gift="{$gift->getId()}">
<img class="gift_pic" src="{$gift->getImage(2)}" alt="{_gift}" />
<strong class="gift_price">
{if $gift->isFree()}
{_free_gift}
{else}
{tr('coins', $gift->getPrice())}
{/if}
</strong>
<strong class="gift_limit">
{if $gift->getUsagesLeft($thisUser) !== INF}
{tr("gifts_left", $gift->getUsagesLeft($thisUser))}
{/if}&nbsp;
</strong>
</div>
</div>
<div style="padding: 8px;">
{include "../components/paginator.xml", conf => (object) [
"page" => $page,
"count" => $count,
"amount" => sizeof($gifts),
"perPage" => OPENVK_DEFAULT_PER_PAGE,
]}
</div>
{/block}
{block bodyScripts}
<script>
$(".gift_sel").click(function() {
let el = $(this);
if(el.hasClass("disabled"))
return false;
let link = "/gifts?act=confirm&user={$user->getId()}&pack={$cat->getId()}&elid=";
let gift = el.data("gift");
window.location.assign(link + gift);
});
</script>
{/block}

View file

@ -0,0 +1,43 @@
{extends "../@listView.xml"}
{block title}
{tr("users_gifts", $user->getFirstName())}
{/block}
{block header}
<a href="{$user->getURL()}">{$user->getCanonicalName()}</a> »
{_gifts}
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
javascript:false
{/block}
{block preview}
<img src="{$x->gift->getImage(2)}" width="75" alt="{_gift}" />
{/block}
{block name}
{_gift}
{/block}
{block description}
<table class="ugc-table" n:if="$hideInfo ? !$x->anon : true">
<tbody>
<tr>
<td><span class="nobold">{_sender}: </span></td>
<td>
<a href="{$x->sender->getURL()}">
{$x->sender->getFullName()}
</a>
</td>
</tr>
<tr n:if="!empty($x->caption)">
<td><span class="nobold">{_comment}: </span></td>
<td>{$x->caption}</td>
</tr>
</tbody>
</table>
{/block}

View file

@ -27,7 +27,7 @@
<div class="container_gray"> <div class="container_gray">
<h4>{_main_information}</h4> <h4>{_main_information}</h4>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center"> <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody> <tbody>
<tr> <tr>
<td width="120" valign="top"> <td width="120" valign="top">
@ -53,6 +53,14 @@
<input type="text" name="shortcode" value="{$club->getShortcode()}" /> <input type="text" name="shortcode" value="{$club->getShortcode()}" />
</td> </td>
</tr> </tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_website}: </span>
</td>
<td>
<input type="text" name="website" value="{$club->getWebsite()}" />
</td>
</tr>
<tr> <tr>
<td width="120" valign="top"> <td width="120" valign="top">
<span class="nobold">{_avatar}: </span> <span class="nobold">{_avatar}: </span>
@ -69,6 +77,17 @@
<input type="checkbox" name="wall" value="1" {if $club->canPost()}checked{/if}/> {_group_allow_post_for_everyone} <input type="checkbox" name="wall" value="1" {if $club->canPost()}checked{/if}/> {_group_allow_post_for_everyone}
</td> </td>
</tr> </tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_group_administrators_list}: </span>
</td>
<td>
{var areAllAdminsHidden = $club->getManagersCount(true) == 0}
<input type="radio" name="administrators_list_display" value="0" n:attr="checked => $club->getAdministratorsListDisplay() == 0, disabled => $areAllAdminsHidden" /> {_group_display_only_creator}<br>
<input type="radio" name="administrators_list_display" value="1" n:attr="checked => $club->getAdministratorsListDisplay() == 1, disabled => $areAllAdminsHidden" /> {_group_display_all_administrators}<br>
<input type="radio" name="administrators_list_display" value="2" n:attr="checked => $club->getAdministratorsListDisplay() == 2" /> {_group_dont_display_administrators_list}<br>
</td>
</tr>
<tr> <tr>
<td> <td>

View file

@ -1,5 +1,6 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = $followers} {var $Manager = openvk\Web\Models\Entities\Manager::class}
{var iterator = $onlyShowManagers ? $managers : $followers}
{var count = $paginatorConf->count} {var count = $paginatorConf->count}
{var page = $paginatorConf->page} {var page = $paginatorConf->page}
{var perPage = 6} {var perPage = 6}
@ -9,6 +10,8 @@
{block header} {block header}
<a href="{$club->getURL()}">{$club->getCanonicalName()}</a> <a href="{$club->getURL()}">{$club->getCanonicalName()}</a>
» {_followers} » {_followers}
<a n:if="!$onlyShowManagers" href="/club{$club->getId()}/followers?onlyAdmins=1" style="float: right;">{_all_followers}</a>
<a n:if="$onlyShowManagers" href="/club{$club->getId()}/followers" style="float: right;">{_only_administrators}</a>
{/block} {/block}
{block actions} {block actions}
@ -17,45 +20,98 @@
{* BEGIN ELEMENTS DESCRIPTION *} {* BEGIN ELEMENTS DESCRIPTION *}
{block tabs}
{if $club->canBeModifiedBy($thisUser)}
<div class="tab">
<a href="/club{$club->getId()}/edit">
{_main}
</a>
</div>
<div id="activetabs" class="tab">
<a id="act_tab_a" href="/club{$club->getId()}/followers">
{_followers}
</a>
</div>
<div class="tab">
<a href="/club{$club->getId()}/stats">
{_statistics}
</a>
</div>
{/if}
{/block}
{block link|strip|stripHtml} {block link|strip|stripHtml}
/id{$x->getId()} /id{$x instanceof $Manager ? $x->getUserId() : $x->getId()}
{/block} {/block}
{block preview} {block preview}
<img src="{$x->getAvatarURL()}" alt="{$x->getCanonicalName()}" width=75 /> <img src="{$x instanceof $Manager ? $x->getUser()->getAvatarURL() : $x->getAvatarURL()}" alt="{$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()}" width=75 />
{/block} {/block}
{block name} {block name}
{$x->getCanonicalName()} {$x instanceof $Manager ? $x->getUser()->getCanonicalName() : $x->getCanonicalName()}
{/block} {/block}
{block description} {block description}
{var user = $x instanceof $Manager ? $x->getUser() : $x}
{var manager = $x instanceof $Manager ? $x : $club->getManager($user, !$club->canBeModifiedBy($thisUser))}
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">{_"gender"}: </span></td> <td width="120" valign="top"><span class="nobold">{_"gender"}: </span></td>
<td>{$x->isFemale() ? "женский" : "мужской"}</td> <td>{$user->isFemale() ? "женский" : "мужской"}</td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">{_"registration_date"}: </span></td> <td width="120" valign="top"><span class="nobold">{_"registration_date"}: </span></td>
<td>{$x->getRegistrationTime()}</td> <td>{$user->getRegistrationTime()}</td>
</tr> </tr>
<tr> <tr>
<td width="120" valign="top"><span class="nobold">{_role}: </span></td> <td width="120" valign="top"><span class="nobold">{_role}: </span></td>
<td> <td>
{$club->canBeModifiedBy($x) ? tr("administrator") : tr("follower")} {$club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager) ? tr("administrator") : tr("follower")}
</td> </td>
</tr> </tr>
<tr n:if="$club->canBeModifiedBy($thisUser ?? NULL) && $club->getOwner()->getId() !== $x->getId()"> <tr n:if="$manager && !empty($manager->getComment()) || $club->getOwner()->getId() === $user->getId() && !empty($club->getOwnerComment()) && (!$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser))">
<td width="120" valign="top"><span class="nobold">{_comment}: </span></td>
<td>
{if $club->getOwner()->getId() === $user->getId()}
{$club->getOwnerComment()}
{else}
{$manager->getComment()}
{/if}
</td>
</tr>
<tr n:if="$club->canBeModifiedBy($thisUser ?? NULL)">
<td width="120" valign="top"><span class="nobold">{_actions}: </span></td> <td width="120" valign="top"><span class="nobold">{_actions}: </span></td>
<td> <td>
<a href="/club{$club->getId()}/setAdmin.jsp?user={$x->getId()}&hash={rawurlencode($csrfToken)}"> <a href="/club{$club->getId()}/setAdmin.jsp?user={$user->getId()}&hash={rawurlencode($csrfToken)}" n:if="$club->getOwner()->getId() !== $user->getId()">
{if $club->canBeModifiedBy($x)} {if $manager}
{_devote} {_devote}
{else} {else}
{_promote_to_admin} {_promote_to_admin}
{/if} {/if}
</a> </a>
{if $manager}
|
<a href="javascript:setClubAdminComment('{$club->getId()}', '{$manager->getUserId()}', '{rawurlencode($csrfToken)}')">
{_set_comment}
</a>
{/if}
<a n:if="$club->getOwner()->getId() === $user->getId()" href="javascript:setClubAdminComment('{$club->getId()}', '{$club->getOwner()->getId()}', '{rawurlencode($csrfToken)}')">
{_set_comment}
</a>
{if $manager}
|
<a href="/club{$club->getId()}/setAdmin.jsp?user={$user->getId()}&hidden={(int) !$manager->isHidden()}&hash={rawurlencode($csrfToken)}">
{if $manager->isHidden()}{_hidden_yes}{else}{_hidden_no}{/if}
</a>
{/if}
{if $club->getOwner()->getId() == $user->getId()}
|
<a href="/club{$club->getId()}/setAdmin.jsp?user={$user->getId()}&hidden={(int) !$club->isOwnerHidden()}&hash={rawurlencode($csrfToken)}">
{if $club->isOwnerHidden()}{_hidden_yes}{else}{_hidden_no}{/if}
</a>
{/if}
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -1,15 +1,15 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Статистика группы{/block} {block title}{$club->getName()} » {_statistics}{/block}
{block header} {block header}
<a href="{$club->getURL()}">{$club->getName()}</a> » Статистика <a href="{$club->getURL()}">{$club->getName()}</a> » {_statistics}
{/block} {/block}
{block content} {block content}
<div class="tabs"> <div class="tabs">
<div class="tab"> <div class="tab">
<a href="/club{$club->getId()}/edit"> <a href="/club{$club->getId()}/edit">
Настройки {_main}
</a> </a>
</div> </div>
<div class="tab"> <div class="tab">
@ -19,7 +19,7 @@
</div> </div>
<div id="activetabs" class="tab"> <div id="activetabs" class="tab">
<a id="act_tab_a" href="javascript:void(0)"> <a id="act_tab_a" href="javascript:void(0)">
Статистика {_statistics}
</a> </a>
</div> </div>
</div> </div>

View file

@ -29,6 +29,14 @@
<td><span class="nobold">{_"description"}:</span></td> <td><span class="nobold">{_"description"}:</span></td>
<td>{$club->getDescription()}</td> <td>{$club->getDescription()}</td>
</tr> </tr>
<tr n:if="!is_null($club->getWebsite())">
<td><span class="nobold">{_"website"}: </span></td>
<td>
<a href="{$club->getWebsite()}" rel="ugc" target="_blank">
{$club->getWebsite()}
</a>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -105,17 +113,66 @@
{_"group_type_open"} {_"group_type_open"}
</div> </div>
</div> </div>
<div> <div n:if="$club->getAdministratorsListDisplay() == 0">
<div class="content_title_expanded" onclick="hidePanel(this);"> <div class="content_title_expanded" onclick="hidePanel(this);">
{_"creator"} {_"creator"}
</div> </div>
<div style="padding:4px"> <div class="avatar-list-item" style="padding: 8px;">
{var author = $club->getOwner()} {var author = $club->getOwner()}
<ul> <div class="avatar">
<li> <a href="{$author->getURL()}">
<a href="{$author->getURL()}"><b>{$author->getCanonicalName()}</b></a> <img class="ava" src="{$author->getAvatarUrl()}" />
</li> </a>
</ul> </div>
{* Это наверное костыль, ну да ладно *}
<div n:class="info, mb_strlen($author->getCanonicalName()) < 22 ? info-centered" n:if="empty($club->getOwnerComment())">
<a href="{$author->getURL()}" class="title">{$author->getCanonicalName()}</a>
</div>
<div class="info" n:if="!empty($club->getOwnerComment())">
<a href="{$author->getURL()}" class="title">{$author->getCanonicalName()}</a>
<div class="subtitle">{$club->getOwnerComment()}</div>
</div>
</div>
</div>
<div n:if="$club->getAdministratorsListDisplay() == 1">
{var managersCount = $club->getManagersCount(true)}
<div class="content_title_expanded" onclick="hidePanel(this, {$managersCount});">
{_"administrators"}
</div>
<div>
<div class="content_subtitle">
{tr("administrators", $managersCount)}
<div style="float: right;">
<a href="/club{$club->getId()}/followers?onlyAdmins=1">{_"all_title"}</a>
</div>
</div>
<div class="avatar-list">
<div class="avatar-list-item" n:if="!$club->isOwnerHidden()">
{var author = $club->getOwner()}
<div class="avatar">
<a href="{$author->getURL()}">
<img class="ava" src="{$author->getAvatarUrl()}" />
</a>
</div>
<div class="info">
<a href="{$author->getURL()}" class="title">{$author->getCanonicalName()}</a>
<div class="subtitle" n:if="!empty($club->getOwnerComment())">{$club->getOwnerComment()}</div>
</div>
</div>
<div class="avatar-list-item" n:foreach="$club->getManagers(1, true) as $manager">
{var user = $manager->getUser()}
<div class="avatar">
<a href="{$user->getURL()}">
<img height="32" class="ava" src="{$user->getAvatarUrl()}" />
</a>
</div>
<div class="info">
<a href="{$user->getURL()}" class="title">{$user->getCanonicalName()}</a>
<div class="subtitle" n:if="!empty($manager->getComment())">{$manager->getComment()}</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div n:if="$albumsCount > 0"> <div n:if="$albumsCount > 0">

View file

@ -21,7 +21,7 @@
{* BEGIN ELEMENTS DESCRIPTION *} {* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml} {block link|strip|stripHtml}
/note{$x->getOwner()->getId()}_{$x->getId()} /note{$x->getPrettyId()}
{/block} {/block}
{block preview} {block preview}

View file

@ -1,5 +1,5 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Помощь{/block} {block title}{_menu_help}{/block}
{block header} {block header}
{$ticket->getName()} {$ticket->getName()}
@ -12,15 +12,15 @@
{$ticket->getName()} {$ticket->getName()}
</b> </b>
</a> </a>
<br></b>Автор: <a href="/id{$ticket->getUser()->getId()}">{$ticket->getUser()->getFullName()}</a> | {$ticket->getUser()->getRegistrationIP()} | Статус: {$ticket->getStatus()} <br></b>{_author}: <a href="/id{$ticket->getUser()->getId()}">{$ticket->getUser()->getFullName()}</a> | {$ticket->getUser()->getRegistrationIP()} | {_status}: {$ticket->getStatus()}.
</div> </div>
<div class="text" style="padding-top: 10px;border-bottom: #ECECEC solid 1px;"> <div class="text" style="padding-top: 10px;border-bottom: #ECECEC solid 1px;">
{$ticket->getContext()} {$ticket->getText()|noescape}
<br></br> <br></br>
</div> </div>
<div style="padding-top: 5px;"> <div style="padding-top: 5px;">
{$ticket->getTime()}&nbsp;|&nbsp; {$ticket->getTime()}&nbsp;|&nbsp;
<a href="/support/delete/{$id}?hash={$csrfToken}">Удалить</a> <a href="/support/delete/{$id}?hash={$csrfToken}">{_delete}</a>
</div><br/> </div><br/>
<div> <div>
<form action="/al_comments.pl/create/support/reply/{$id}" method="post" style="margin:0;"> <form action="/al_comments.pl/create/support/reply/{$id}" method="post" style="margin:0;">
@ -30,16 +30,17 @@
</div> </div>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<br> <br>
<input type="submit" value="Ответить" class="button"> <input type="submit" value="{_write}" class="button">
<select name="status" style="width: unset;"> <select name="status" style="width: unset;">
<option value="1">Есть ответ</option> <option value="1">{_support_status_1}</option>
<option value="2">Закрыто</option> <option value="2">{_support_status_2}</option>
<option value="0">Вопрос на рассмотрении</option> <option value="0">{_support_status_0}</option>
</select> </select>
</form> </form>
</div> </div>
<br/> <br/>
<p n:if="!$comments">Комментарии отсутствуют</p> <p n:if="!$comments">{_no_comments}</p>
{var $printedSupportGreeting = false}
<table n:foreach="$comments as $comment" border="0" style="font-size: 11px;" class="post"> <table n:foreach="$comments as $comment" border="0" style="font-size: 11px;" class="post">
<tbody> <tbody>
<tr> <tr>
@ -50,7 +51,7 @@
{else} {else}
<td width="54" valign="top"> <td width="54" valign="top">
<img <img
src="https://avatars.mds.yandex.net/get-zen_doc/164147/pub_5b60816e2dc67000a862253e_5b6081d4e9151400a9f48625/scale_1200" src="{$comment->getAvatar()}"
style="max-width: 50px; filter: hue-rotate({$comment->getColorRotation()}deg);" /> style="max-width: 50px; filter: hue-rotate({$comment->getColorRotation()}deg);" />
</td> </td>
{/if} {/if}
@ -59,14 +60,14 @@
<div class="post-author"> <div class="post-author">
<a href="{$comment->getUser()->getURL()}"><b> <a href="{$comment->getUser()->getURL()}"><b>
{$comment->getUser()->getFullName()} {$comment->getUser()->getFullName()}
</b></a> написал<br> </b></a> {($comment->getUser()->isFemale() ? tr("post_writes_f") : tr("post_writes_m"))}<br>
<a href="#" class="date">{$comment->getTime()}</a> <a href="#" class="date">{$comment->getTime()}</a>
</div> </div>
{elseif ($comment->getUType() === 1)} {elseif ($comment->getUType() === 1)}
<div class="post-author"> <div class="post-author">
<a href="javascript:false"> <a href="javascript:false">
<b> <b>
{=OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"]} №{$comment->getAgentNumber()} {$comment->getAuthorName()}
</b> </b>
</a> </a>
{if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)} {if $thisUser->getChandlerUser()->can("write")->model('openvk\Web\Models\Entities\TicketReply')->whichBelongsTo(0)}
@ -76,17 +77,31 @@
</span> </span>
</a> </a>
{/if} {/if}
написал<br> {_post_writes_m}<br>
<a href="#" class="date">{$comment->getTime()}</a> <a href="#" class="date">{$comment->getTime()}</a>
</div> </div>
{/if} {/if}
<div class="post-content" id="{$comment->getId()}"> <div class="post-content" id="{$comment->getId()}">
<div class="text" id="text{$comment->getId()}"> <div class="text" id="text{$comment->getId()}">
{$comment->getContext()|noescape} {if $comment->getUType() === 1 && !$printedSupportGreeting}
{var $printedSupportGreeting = true}
{tr("support_greeting_hi", $ticket->getUser()->getFullName())}
<br/>
<br/>
{$comment->getText()|noescape}
<br/>
<br/>
{tr("support_greeting_regards", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"])|noescape}
{else}
{$comment->getText()|noescape}
{/if}
</div> </div>
{if $comment->getUType() === 0} {if $comment->getUType() === 0}
<div class="post-menu"> <div class="post-menu">
<a href="/support/comment/{$comment->getId()}/delete">Удалить</a> <a href="/support/comment/{$comment->getId()}/delete">{_delete}</a>
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -1,8 +1,8 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Помощь{/block} {block title}{_menu_help}{/block}
{block header} {block header}
Помощь {_menu_help}
{/block} {/block}
{block content} {block content}
@ -14,13 +14,13 @@
{if $thisUser} {if $thisUser}
<div class="tabs"> <div class="tabs">
<div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isMain ? 'act_tab_a' : 'ki')" href="/support">Часто задаваемые вопросы</a> <a n:attr="id => ($isMain ? 'act_tab_a' : 'ki')" href="/support">{_support_faq}</a>
</div> </div>
<div n:attr="id => ($isList ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isList ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isList ? 'act_tab_a' : 'ki')" href="/support?act=list">Список обращений</a> <a n:attr="id => ($isList ? 'act_tab_a' : 'ki')" href="/support?act=list">{_support_list}</a>
</div> </div>
<div n:attr="id => ($isNew ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isNew ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isNew ? 'act_tab_a' : 'ki')" href="/support?act=new">Новое обращение</a> <a n:attr="id => ($isNew ? 'act_tab_a' : 'ki')" href="/support?act=new">{_support_new}</a>
</div> </div>
</div> </div>
<br> <br>
@ -28,19 +28,19 @@
{if $isNew} {if $isNew}
<div class="new"> <div class="new">
<form action="/support" method="post" style="margin:0;"> <form action="/support" method="post" style="margin:0;">
<center><input name="name" style="width: 80%;resize: vertical;" placeholder="Введите тему вашего обращения"></center><br> <center><input name="name" style="width: 80%;resize: vertical;" placeholder="{_support_new_title}"></center><br>
<center><textarea name="text" style="width: 80%;resize: vertical;" placeholder="Опишите проблему или предложение"></textarea></center><br> <center><textarea name="text" style="width: 80%;resize: vertical;" placeholder="{_support_new_content}"></textarea></center><br>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<center><input type="submit" value="Написать" class="button" style="margin-left:70%;"></center><br> <center><input type="submit" value="{_write}" class="button" style="margin-left:70%;"></center><br>
</form> </form>
</div> </div>
{/if}{/if} {/if}{/if}
{if $isMain} {if $isMain}
<h4>Часто задаваемые вопросы</h4><br> <h4>{_support_faq}</h4><br>
<div class="faq"> <div class="faq">
<div id="faqhead">Для кого этот сайт?</div> <div id="faqhead">{_support_faq_title}</div>
<div id="faqcontent">Сайт предназначен для поиска друзей и знакомых, а также просмотр данных пользователя. Это как справочник города, с помощью которого люди могут быстро найти актуальную информацию о человеке. Также этот сайт подойдёт для ностальгираторов и тех, кто решил слезть с трубы "ВКонтакте", которого клон и является.<br></div> <div id="faqcontent">{_support_faq_content}</div>
</div> </div>
{/if} {/if}
@ -49,7 +49,7 @@
<tbody> <tbody>
<tr> <tr>
<td width="54" valign="top"> <td width="54" valign="top">
<center><img src="/assets/packages/static/openvk/img/note_icon.png" alt="Заметка" style="margin-top: 17px;"></center> <center><img src="/assets/packages/static/openvk/img/note_icon.png" alt="{_support_ticket}" style="margin-top: 17px;"></center>
</td> </td>
<td width="345" valign="top"> <td width="345" valign="top">
<div class="post-author"> <div class="post-author">
@ -58,7 +58,7 @@
</a> </a>
</div> </div>
<div class="post-content" style="padding: 4px;font-size: 11px;"> <div class="post-content" style="padding: 4px;font-size: 11px;">
Статус: {$ticket->getStatus()} {_status}: {$ticket->getStatus()}
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -5,18 +5,18 @@
{/block} {/block}
{block header} {block header}
Helpdesk » Тикеты Helpdesk » {_support_tickets}
{/block} {/block}
{block tabs} {block tabs}
<div n:attr="id => ($act === 'open' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'open' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'open' ? 'act_tab_a' : 'ki')" href="?">Открытые</a> <a n:attr="id => ($act === 'open' ? 'act_tab_a' : 'ki')" href="?">{_support_opened}</a>
</div> </div>
<div n:attr="id => ($act === 'answered' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'answered' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'answered' ? 'act_tab_a' : 'ki')" href="?act=answered">С ответом</a> <a n:attr="id => ($act === 'answered' ? 'act_tab_a' : 'ki')" href="?act=answered">{_support_answered}</a>
</div> </div>
<div n:attr="id => ($act === 'closed' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'closed' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'closed' ? 'act_tab_a' : 'ki')" href="?act=closed">Закрытые</a> <a n:attr="id => ($act === 'closed' ? 'act_tab_a' : 'ki')" href="?act=closed">{_support_closed}</a>
</div> </div>
{/block} {/block}
@ -28,7 +28,7 @@
{block preview} {block preview}
<center> <center>
<img src="/assets/packages/static/openvk/img/note_icon.png" alt="Тикет" style="margin-top: 8px;"> <img src="/assets/packages/static/openvk/img/note_icon.png" alt="{_support_ticket}" style="margin-top: 8px;">
</center> </center>
{/block} {/block}
@ -40,5 +40,5 @@
{var author = $x->getUser()} {var author = $x->getUser()}
{ovk_proc_strtr($x->getContext(), 50)}<br/> {ovk_proc_strtr($x->getContext(), 50)}<br/>
<span class="nobold">Автор: </span> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a> <span class="nobold">{_author}: </span> <a href="{$author->getURL()}">{$author->getCanonicalName()}</a>
{/block} {/block}

View file

@ -1,5 +1,5 @@
{extends "../@layout.xml"} {extends "../@layout.xml"}
{block title}Помощь{/block} {block title}{_menu_help}{/block}
{block header} {block header}
{$ticket->getName()} {$ticket->getName()}
@ -13,15 +13,15 @@
{$ticket->getName()} {$ticket->getName()}
</b> </b>
</a> </a>
<br></b>Статус: {$ticket->getStatus()} <br></b>{_status}: {$ticket->getStatus()}
</div> </div>
<div class="text" style="padding-top: 10px;border-bottom: #ECECEC solid 1px;"> <div class="text" style="padding-top: 10px;border-bottom: #ECECEC solid 1px;">
{$ticket->getContext()} {$ticket->getText()|noescape}
<br></br> <br></br>
</div> </div>
<div style="padding-top: 5px;"> <div style="padding-top: 5px;">
{$ticket->getTime()}&nbsp;|&nbsp; {$ticket->getTime()}&nbsp;|&nbsp;
<a href="/support/delete/{$id}?hash={$csrfToken}">Удалить</a> <a href="/support/delete/{$id}?hash={$csrfToken}">{_delete}</a>
</div> </div>
{if $ticket->getType() !== 2} {if $ticket->getType() !== 2}
<br> <br>
@ -33,12 +33,13 @@
</div> </div>
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<br> <br>
<input type="submit" value="Написать" class="button"> <input type="submit" value="{_write}" class="button">
</form> </form>
</div> </div>
{/if} {/if}
</br> </br>
<p n:if="!$comments">Комментарии отсутствуют</p> <p n:if="!$comments">{_no_comments}</p>
{var $printedSupportGreeting = false}
<table n:foreach="$comments as $comment" border="0" style="font-size: 11px;" class="post"> <table n:foreach="$comments as $comment" border="0" style="font-size: 11px;" class="post">
<tbody> <tbody>
<tr> <tr>
@ -49,7 +50,7 @@
{else} {else}
<td width="54" valign="top"> <td width="54" valign="top">
<img <img
src="https://avatars.mds.yandex.net/get-zen_doc/164147/pub_5b60816e2dc67000a862253e_5b6081d4e9151400a9f48625/scale_1200" src="{$comment->getAvatar()}"
style="max-width: 50px; filter: hue-rotate({$comment->getColorRotation()}deg);" /> style="max-width: 50px; filter: hue-rotate({$comment->getColorRotation()}deg);" />
</td> </td>
{/if} {/if}
@ -58,24 +59,40 @@
<div class="post-author"> <div class="post-author">
<a href="{$comment->getUser()->getURL()}"><b> <a href="{$comment->getUser()->getURL()}"><b>
{$comment->getUser()->getFullName()} {$comment->getUser()->getFullName()}
</b></a> написал<br> </b></a> {($comment->getUser()->isFemale() ? tr("post_writes_f") : tr("post_writes_m"))}<br>
<a href="#" class="date">{$comment->getTime()}</a> <a href="#" class="date">{$comment->getTime()}</a>
</div> </div>
{elseif ($comment->getUType() === 1)} {elseif ($comment->getUType() === 1)}
<div class="post-author"> <div class="post-author">
<a href="#"><b> <a href="javascript:false">
{=OPENVK_ROOT_CONF["openvk"]["preferences"]["support"]["supportName"]} №{$comment->getAgentNumber()} <b>
</b></a> написал<br> {$comment->getAuthorName()}
</b>
</a>
{_post_writes_m}<br>
<a href="#" class="date">{$comment->getTime()}</a> <a href="#" class="date">{$comment->getTime()}</a>
</div> </div>
{/if} {/if}
<div class="post-content" id="{$comment->getId()}"> <div class="post-content" id="{$comment->getId()}">
<div class="text" id="text{$comment->getId()}"> <div class="text" id="text{$comment->getId()}">
{$comment->getContext()|noescape} {if $comment->getUType() === 1 && !$printedSupportGreeting}
{var $printedSupportGreeting = true}
{tr("support_greeting_hi", $ticket->getUser()->getFullName())}
<br/>
<br/>
{$comment->getText()|noescape}
<br/>
<br/>
{tr("support_greeting_regards", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"])|noescape}
{else}
{$comment->getText()|noescape}
{/if}
</div> </div>
{if $comment->getUType() === 0} {if $comment->getUType() === 0}
<div class="post-menu"> <div class="post-menu">
<a href="/support/comment/{$comment->getId()}/delete">Удалить</a> <a href="/support/comment/{$comment->getId()}/delete">{_delete}</a>
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -173,6 +173,14 @@
<input type="text" name="telegram" value="{$user->getTelegram()}" /> <input type="text" name="telegram" value="{$user->getTelegram()}" />
</td> </td>
</tr> </tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_"personal_website"}: </span>
</td>
<td>
<input type="text" name="website" value="{$user->getWebsite()}" />
</td>
</tr>
<tr> <tr>
<td width="120" valign="top"> <td width="120" valign="top">
<span class="nobold">{_"city"}: </span> <span class="nobold">{_"city"}: </span>

View file

@ -1,6 +1,6 @@
{extends "../@listView.xml"} {extends "../@listView.xml"}
{var iterator = $user->getClubs($page)} {var iterator = $user->getClubs($page, $admin)}
{var count = $user->getClubCount()} {var count = $user->getClubCount($admin)}
{block title}{_"groups"}{/block} {block title}{_"groups"}{/block}
@ -18,14 +18,23 @@
</div> </div>
{/block} {/block}
{block actions}
<div class="tile">
<a href="javascript:alert('Не запилил')" class="profile_link">Поиск групп</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *} {* BEGIN ELEMENTS DESCRIPTION *}
{block tabs}
{if !is_null($thisUser) && $user->getId() === $thisUser->getId()}
<div {if !$admin}id="activetabs"{/if} class="tab">
<a {if !$admin}id="act_tab_a"{/if} href="/groups{$user->getId()}">
{_groups}
</a>
</div>
<div {if $admin}id="activetabs"{/if} class="tab">
<a {if $admin}id="act_tab_a"{/if} href="/groups{$user->getId()}?act=managed">
{_managed}
</a>
</div>
{/if}
{/block}
{block link|strip|stripHtml} {block link|strip|stripHtml}
{$x->getURL()} {$x->getURL()}
{/block} {/block}
@ -40,4 +49,23 @@
{block description} {block description}
{$x->getDescription()} {$x->getDescription()}
{if $x->canBeModifiedBy($thisUser ?? NULL)}
{var clubPinned = $thisUser->isClubPinned($x)}
<table n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10">
<tbody>
<tr>
<td width="120" valign="top"><span class="nobold">{_actions}: </span></td>
<td>
<a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
{if $clubPinned}
{_remove_from_left_menu}
{else}
{_add_to_left_menu}
{/if}
</a>
</td>
</tr>
</tbody>
</table>
{/if}
{/block} {/block}

View file

@ -10,6 +10,7 @@
{var isMain = $mode === 'main'} {var isMain = $mode === 'main'}
{var isPrivacy = $mode === 'privacy'} {var isPrivacy = $mode === 'privacy'}
{var isFinance = $mode === 'finance'} {var isFinance = $mode === 'finance'}
{var isFinanceTU = $mode === 'finance.top-up'}
{var isInterface = $mode === 'interface'} {var isInterface = $mode === 'interface'}
<div class="tabs"> <div class="tabs">
@ -19,8 +20,8 @@
<div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isPrivacy ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_"privacy"}</a> <a n:attr="id => ($isPrivacy ? 'act_tab_a' : 'ki')" href="/settings?act=privacy">{_"privacy"}</a>
</div> </div>
<div n:attr="id => ($isFinance ? 'activetabs' : 'ki')" class="tab"> <div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce']" n:attr="id => (($isFinance || $isFinanceTU) ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isFinance ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a> <a n:attr="id => (($isFinance || $isFinanceTU) ? 'act_tab_a' : 'ki')" href="/settings?act=finance">{_points}</a>
</div> </div>
<div n:attr="id => ($isInterface ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($isInterface ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isInterface ? 'act_tab_a' : 'ki')" href="/settings?act=interface">{_"interface"}</a> <a n:attr="id => ($isInterface ? 'act_tab_a' : 'ki')" href="/settings?act=interface">{_"interface"}</a>
@ -58,6 +59,14 @@
<input type="password" name="repeat_pass" style="width: 100%;" /> <input type="password" name="repeat_pass" style="width: 100%;" />
</td> </td>
</tr> </tr>
<tr n:if="$user->is2faEnabled()">
<td width="120" valign="top">
<span class="nobold">{_"2fa_code"}</span>
</td>
<td>
<input type="text" name="code" style="width: 100%;" />
</td>
</tr>
<tr> <tr>
<td> <td>
@ -70,6 +79,70 @@
</tbody> </tbody>
</table> </table>
<br/> <br/>
<h4>{_two_factor_authentication}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
{if $user->is2faEnabled()}
<tr>
<td>
<div class="accent-box">
{_two_factor_authentication_enabled}
</div>
</td>
</tr>
<tr>
<td style="text-align: center;">
<a class="button" href="javascript:viewBackupCodes()">{_view_backup_codes}</a>
<a class="button" href="javascript:disableTwoFactorAuth()">{_disable}</a>
</td>
</tr>
<script>
function viewBackupCodes() {
MessageBox("Просмотр резервных кодов", `
<form id="back-codes-view-form" method="post" action="/settings/2fa">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" required />
<input type="hidden" name="hash" value={$csrfToken} />
</form>
`, ["Просмотреть", "Отменить"], [
() => {
document.querySelector("#back-codes-view-form").submit();
}, Function.noop
]);
}
function disableTwoFactorAuth() {
MessageBox("Отключить 2FA", `
<form id="two-factor-auth-disable-form" method="post" action="/settings/2fa/disable">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" required />
<input type="hidden" name="hash" value={$csrfToken} />
</form>
`, ["Отключить", "Отменить"], [
() => {
document.querySelector("#two-factor-auth-disable-form").submit();
}, Function.noop
]);
}
</script>
{else}
<tr>
<td>
<div class="accent-box">
{_two_factor_authentication_disabled}
</div>
</td>
</tr>
<tr>
<td style="text-align: center;">
<a class="button" href="/settings/2fa">{_connect}</a>
</td>
</tr>
{/if}
</tbody>
</table>
<br/>
<h4>{_your_email_address}</h4> <h4>{_your_email_address}</h4>
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center"> <table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody> <tbody>
@ -265,11 +338,26 @@
<b> <b>
{_on_your_account}<br/> {_on_your_account}<br/>
<span style="font-size: 50px;">{$thisUser->getCoins()}</span><br/> <span style="font-size: 50px;">{$thisUser->getCoins()}</span><br/>
{_points_count} {_points_count}<br/><br/>
<small><a href="?act=finance.top-up">[{_have_voucher}?]</a></small>
</b> </b>
</p> </p>
</div> </div>
{elseif $isFinanceTU}
<p>{_voucher_explanation} {_voucher_explanation_ex}</p>
<form action="/settings?act=finance.top-up" method="POST" enctype="multipart/form-data">
<input type="text" name="key0" size="6" placeholder="123456" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
<input type="text" name="key1" size="6" placeholder="789012" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
<input type="text" name="key2" size="6" placeholder="345678" required="required" style="display: inline-block; width: 50px; text-align: center;" /> -
<input type="text" name="key3" size="6" placeholder="90ABCD" required="required" style="display: inline-block; width: 50px; text-align: center;" />
<br/><br/>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_redeem}" class="button" />
</form>
{elseif $isInterface} {elseif $isInterface}
<h4>{_ui_settings_interface}</h4> <h4>{_ui_settings_interface}</h4>

View file

@ -0,0 +1,18 @@
{extends "../@layout.xml"}
{block title}{_"my_settings"} - {_"two_factor_authentication"}{/block}
{block header}
<a href="/settings">{_"my_settings"}</a> » {_"two_factor_authentication"}
{/block}
{block content}
<h4>{_"backup_codes"}</h4>
<p>{_"two_factor_authentication_backup_codes_1"}</p>
<p>{_"two_factor_authentication_backup_codes_2"|noescape}</p>
<ol style="columns: 2; text-align: center;">
<li n:foreach="$codes as $code">{$code}</li>
</ol>
<p>{_"two_factor_authentication_backup_codes_3"}</p>
{/block}

View file

@ -0,0 +1,48 @@
{extends "../@layout.xml"}
{block title}{_"my_settings"} - {_"two_factor_authentication"}{/block}
{block header}
<a href="/settings">{_"my_settings"}</a> » {_"two_factor_authentication"}
{/block}
{block content}
{_"two_factor_authentication_settings_1"|noescape}
<p>{_"two_factor_authentication_settings_2"}</p>
<div style="text-align: center;">
<img src="data:image/png;base64,{$qrCode}">
</div>
<p>{tr("two_factor_authentication_settings_3", $secret)|noescape}</p>
<p>{_"two_factor_authentication_settings_4"}</p>
<form method="POST">
<table cellspacing="7" cellpadding="0" width="40%" border="0" align="center">
<tbody>
<tr>
<td>
<span>{_code}: </span>
</td>
<td>
<input type="text" name="code" required />
</td>
</tr>
<tr>
<td>
<span>{_password}: </span>
</td>
<td>
<input type="password" name="password" required />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="secret" value="{$secret}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_enable}" class="button" />
</td>
</tr>
</tbody>
</table>
</form>
{/block}

View file

@ -34,7 +34,7 @@
<!-- DEBUG: ONLINE REPORT: static {$user->getOnline()->timestamp()}s adjusted {$user->getOnline()->timestamp() + 2505600}s real {time()}s --> <!-- DEBUG: ONLINE REPORT: static {$user->getOnline()->timestamp()}s adjusted {$user->getOnline()->timestamp() + 2505600}s real {time()}s -->
<div n:if="$user->getOnline()->timestamp() + 2505600 > time()" style="float:right;"> <div n:if="$user->getOnline()->timestamp() + 2505600 > time()" style="float:right;">
{if $diff->i <= 5} {if $user->isOnline()}
<span><b>{_online}</b></span> <span><b>{_online}</b></span>
{else} {else}
<span>{_was_online} {$user->getOnline()}</span> <span>{_was_online} {$user->getOnline()}</span>
@ -76,6 +76,9 @@
{/if} {/if}
{if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)} {if $thisUser->getChandlerUser()->can("access")->model("admin")->whichBelongsTo(NULL)}
<a href="/admin/users/id{$user->getId()}" class="profile_link">
{_manage_user_action}
</a>
<a href="javascript:banUser()" class="profile_link"> <a href="javascript:banUser()" class="profile_link">
{_ban_user_action} {_ban_user_action}
</a> </a>
@ -84,6 +87,8 @@
</a> </a>
{/if} {/if}
<a n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce']" href="/gifts?act=pick&user={$user->getId()}" class="profile_link">{_send_gift}</a>
{var subStatus = $user->getSubscriptionStatus($thisUser)} {var subStatus = $user->getSubscriptionStatus($thisUser)}
{if $subStatus === 0} {if $subStatus === 0}
<form action="/setSub/user" method="post"> <form action="/setSub/user" method="post">
@ -120,8 +125,8 @@
<div n:if="isset($thisUser) && !$thisUser->prefersNotToSeeRating()" class="profile-hints"> <div n:if="isset($thisUser) && !$thisUser->prefersNotToSeeRating()" class="profile-hints">
{var completeness = $user->getProfileCompletenessReport()} {var completeness = $user->getProfileCompletenessReport()}
<div class="completeness-gauge"> <div n:class="completeness-gauge, $completeness->total >= 100 ? completeness-gauge-gold">
<div style="width: {$completeness->total}%"></div> <div style="width: {$completeness->percent}%"></div>
<span>{$completeness->total}%</span> <span>{$completeness->total}%</span>
</div> </div>
@ -150,6 +155,30 @@
{/if} {/if}
</div> </div>
<br /> <br />
<div n:if="OPENVK_ROOT_CONF['openvk']['preferences']['commerce'] && ($giftCount = $user->getGiftCount()) > 0">
<div class="content_title_expanded" onclick="hidePanel(this, {$giftCount});">
{_gifts}
</div>
<div>
<div class="content_subtitle">
{tr("gifts", $giftCount)}
<div style="float:right;">
<a href="/gifts{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="ovk-avView">
<div class="ovk-avView--el" n:foreach="$user->getGifts(1, 3) as $giftDescriptor">
{var hideInfo = !is_null($thisUser) ? ($giftDescriptor->anon ? $thisUser->getId() !== $user->getId() : false) : false}
<a href="{$hideInfo ? 'javascript:false' : $giftDescriptor->sender->getURL()}">
<img class="ava"
src="{$giftDescriptor->gift->getImage(2)}"
alt="{$hideInfo ? tr('gift') : ($giftDescriptor->caption ?? tr('gift'))}" />
</a>
</div>
</div>
</div>
</div>
<div n:if="$user->getFriendsCount() > 0 && $user->getPrivacyPermission('friends.read', $thisUser ?? NULL)"> <div n:if="$user->getFriendsCount() > 0 && $user->getPrivacyPermission('friends.read', $thisUser ?? NULL)">
{var friendCount = $user->getFriendsCount()} {var friendCount = $user->getFriendsCount()}
@ -227,7 +256,7 @@
</div> </div>
</div> </div>
<div n:if="$videosCount > 0 && $user->getPrivacyPermission('videos.read', $thisUser ?? NULL)"> <div n:if="$videosCount > 0 && $user->getPrivacyPermission('videos.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$videosCount});">
{_videos} {_videos}
</div> </div>
<div> <div>
@ -238,21 +267,22 @@
</div> </div>
</div> </div>
<div style="padding: 5px;"> <div style="padding: 5px;">
<div class="ovk-video" style="margin-bottom: 1rem; padding: 0 11px;" n:foreach="$videos as $video"> <div class="ovk-video" n:foreach="$videos as $video">
<div style="width: 170px;" align="center"> <a href="/video{$video->getPrettyId()}" class="preview" align="center">
<img <img
src="{$video->getThumbnailURL()}" src="{$video->getThumbnailURL()}"
style="max-width: 170px; margin: auto;" /> style="max-width: 170px; max-height: 127px; margin: auto;" />
</div> </a>
<div> <div>
<b><a href="/video{$video->getPrettyId()}">{ovk_proc_strtr($video->getName(), 30)}</a></b> <b><a href="/video{$video->getPrettyId()}">{ovk_proc_strtr($video->getName(), 30)}</a></b><br>
<span style="font-size: 10px;">{$video->getPublicationTime()} | {_comments} ({$video->getCommentsCount()})</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div n:if="$notesCount > 0 && $user->getPrivacyPermission('notes.read', $thisUser ?? NULL)"> <div n:if="$notesCount > 0 && $user->getPrivacyPermission('notes.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$notesCount});">
{_notes} {_notes}
</div> </div>
<div> <div>
@ -266,13 +296,13 @@
<div style="padding: 5px 8px 15px 8px;"> <div style="padding: 5px 8px 15px 8px;">
<ul class="notes_titles" n:foreach="$notes as $note"> <ul class="notes_titles" n:foreach="$notes as $note">
<li class="written"> <li class="written">
<a href="/note{$user->getId()}_{$note->getId()}"> <a href="/note{$note->getPrettyId()}">
{$note->getName()} {$note->getName()}
</a> </a>
<small> <small>
{$note->getPublicationTime()} {$note->getPublicationTime()}
<span class="divide">|</span> <span class="divide">|</span>
<a href="/note{$user->getId()}_{$note->getId()}">{_comments}</a> <a href="/note{$note->getPrettyId()}">{_comments}</a>
</small> </small>
</li> </li>
</ul> </ul>
@ -322,19 +352,30 @@
<div class="right_big_block"> <div class="right_big_block">
<div class="page_info"> <div class="page_info">
<div n:if="!is_null($alert = $user->getAlert())" class="user-alert">{$alert}</div>
{var thatIsThisUser = isset($thisUser) && $user->getId() == $thisUser->getId()}
<div n:if="$thatIsThisUser" class="page_status_popup" id="status_editor" style="display: none;">
<form method="post" action="/edit?act=status">
<div style="margin-bottom: 10px;">
<input type="text" name="status" size="50" value="{$user->getStatus()}" />
</div>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" class="button" value="{_'save'}" />
</form>
</div>
<div class="accountInfo clearFix"> <div class="accountInfo clearFix">
<div class="profileName"> <div class="profileName">
<h2>{$user->getFullName()}</h2> <h2>{$user->getFullName()}</h2>
{if !is_null($user->getStatus())} {if !is_null($user->getStatus())}
<div class="page_status">{$user->getStatus()}</div> <div n:class="page_status, $thatIsThisUser ? page_status_edit_button" n:attr="id => $thatIsThisUser ? page_status_text : NULL">{$user->getStatus()}</div>
{elseif isset($thisUser) && $user->getId() == $thisUser->getId()} {elseif $thatIsThisUser}
<div class="page_status"> <div class="page_status">
<a href="/edit" class="edit_link">[ {_"change_status"} ]</a> <div n:class="edit_link, $thatIsThisUser ? page_status_edit_button" id="page_status_text">[ {_"change_status"} ]</div>
</div> </div>
{/if} {/if}
</div> </div>
</div><div> </div>
<div>
<table id="basicInfo" class="ugc-table" border="0" cellspacing="0" cellpadding="0" border="0" cellspacing="0" cellpadding="0" n:if=" $user->getPrivacyPermission('page.info.read', $thisUser ?? NULL)"> <table id="basicInfo" class="ugc-table" border="0" cellspacing="0" cellpadding="0" border="0" cellspacing="0" cellpadding="0" n:if=" $user->getPrivacyPermission('page.info.read', $thisUser ?? NULL)">
<tbody> <tbody>
<tr> <tr>
@ -357,17 +398,18 @@
<td class="label"><span class="nobold">{_"politViews"}:</span></td> <td class="label"><span class="nobold">{_"politViews"}:</span></td>
<td class="data">{var $pviews = $user->getPoliticalViews()}{_"politViews_$pviews"}</td> <td class="data">{var $pviews = $user->getPoliticalViews()}{_"politViews_$pviews"}</td>
</tr> </tr>
{if $user->getBirthday() != 0} {if $user->getBirthday() > 0}
<tr> <tr>
<td class="label"><span class="nobold">{_"birth_date"}:</span></td> <td class="label"><span class="nobold">{_"birth_date"}:</span></td>
<td class="data">{date('d F Y',$user->getBirthday())}, {date('Y') - date('Y', $user->getBirthday())} {_"years"}</td> <td class="data">{date('d F Y',$user->getBirthday())},
{tr("years", $user->getAge())}</td>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div n:if=" $user->getPrivacyPermission('page.info.read', $thisUser ?? NULL)"> <div n:if="$user->getPrivacyPermission('page.info.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this);"> <div class="content_title_expanded" onclick="hidePanel(this);">
{_"information"} {_"information"}
</div> </div>
@ -375,7 +417,6 @@
{capture $contactInfo_Tmp} {capture $contactInfo_Tmp}
<table class="ugc-table" border="0" cellspacing="0" cellpadding="0" border="0" cellspacing="0" cellpadding="0" n:ifcontent> <table class="ugc-table" border="0" cellspacing="0" cellpadding="0" border="0" cellspacing="0" cellpadding="0" n:ifcontent>
<tbody n:ifcontent> <tbody n:ifcontent>
<!--sse-->
<tr n:if="!is_null($user->getContactEmail())"> <tr n:if="!is_null($user->getContactEmail())">
<td class="label"><span class="nobold">{_"email"}: </span></td> <td class="label"><span class="nobold">{_"email"}: </span></td>
<td> <td>
@ -392,7 +433,14 @@
</a> </a>
</td> </td>
</tr> </tr>
<!--/sse--> <tr n:if="!is_null($user->getWebsite())">
<td class="label"><span class="nobold">{_"personal_website"}: </span></td>
<td>
<a href="{$user->getWebsite()}" rel="ugc" target="_blank">
{$user->getWebsite()}
</a>
</td>
</tr>
<tr n:if="!is_null($user->getCity())"> <tr n:if="!is_null($user->getCity())">
<td class="label"><span class="nobold">{_"city"}:</span></td> <td class="label"><span class="nobold">{_"city"}:</span></td>
<td class="data">{$user->getCity()}</td> <td class="data">{$user->getCity()}</td>
@ -440,7 +488,6 @@
{/capture} {/capture}
<div> <div>
<div style="padding: 10px 8px 15px 8px;" n:ifcontent> <div style="padding: 10px 8px 15px 8px;" n:ifcontent>
{if !empty($contactInfo_Tmp)}
<h4 style="border-bottom: none; font-size: 11px; padding: 0; display: inline-block;">{_"contact_information"} {ifset $thisUser}{if $thisUser->getId() == $user->getId()}<a href="/edit?act=contacts" class="edit_link">[ {_"edit"} ]</a>{/if}{/ifset}</h4> <h4 style="border-bottom: none; font-size: 11px; padding: 0; display: inline-block;">{_"contact_information"} {ifset $thisUser}{if $thisUser->getId() == $user->getId()}<a href="/edit?act=contacts" class="edit_link">[ {_"edit"} ]</a>{/if}{/ifset}</h4>
{if !empty($contactInfo_Tmp)} {if !empty($contactInfo_Tmp)}
{$contactInfo_Tmp|noescape} {$contactInfo_Tmp|noescape}
@ -448,7 +495,6 @@
<div style="padding: 15px;color:gray;text-align: center;">{_no_information_provided}</div> <div style="padding: 15px;color:gray;text-align: center;">{_no_information_provided}</div>
{/if} {/if}
<br> <br>
{/if}
<h4 style="border-bottom: none; font-size: 11px; padding: 0; display: inline-block;">{_"personal_information"} {ifset $thisUser}{if $thisUser->getId() == $user->getId()}<a href="/edit?act=interests" class="edit_link">[ {_"edit"} ]</a>{/if}{/ifset}</h4> <h4 style="border-bottom: none; font-size: 11px; padding: 0; display: inline-block;">{_"personal_information"} {ifset $thisUser}{if $thisUser->getId() == $user->getId()}<a href="/edit?act=interests" class="edit_link">[ {_"edit"} ]</a>{/if}{/ifset}</h4>
{if !empty($uInfo_Tmp)} {if !empty($uInfo_Tmp)}
{$uInfo_Tmp|noescape} {$uInfo_Tmp|noescape}
@ -457,9 +503,6 @@
{/if} {/if}
</div> </div>
</div> </div>
<p n:if="empty($contactInfo_Tmp) && empty($uInfo_Tmp)">
Пользователь предпочёл оставить о себе только воздух тайны.
</p>
</div> </div>
{presenter "openvk!Wall->wallEmbedded", $user->getId()} {presenter "openvk!Wall->wallEmbedded", $user->getId()}
@ -509,6 +552,19 @@
]); ]);
} }
</script> </script>
<script n:if="isset($thisUser) && $user->getId() == $thisUser->getId()">
function setStatusEditorShown(shown) {
document.getElementById("status_editor").style.display = shown ? "block" : "none";
}
document.addEventListener("click", event => {
if(!event.target.closest("#status_editor") && !event.target.closest("#page_status_text"))
setStatusEditorShown(false);
});
document.getElementById("page_status_text").onclick = setStatusEditorShown.bind(this, true);
</script>
</div> </div>
{/if} {/if}

View file

@ -14,26 +14,8 @@
{/block} {/block}
{block content} {block content}
<div> <div class="postFeedWrapper">
<div class="content_title_expanded" onclick="hidePanel(this);"> {include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost"}
{_"publish_post"}
</div>
<div style="margin: 0 5px;"><br>
<form action="/wall{$thisUser->getId()}/makePost" method="POST" enctype="multipart/form-data" style="margin:0;">
<textarea id="wall-post-input" name="text" style="width: 100%;resize: none;"></textarea>
<input type="file" name="_pic_attachment" accept="image/*" style="display:none;" />
<input type="hidden" name="type" value="1" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<div></div>
<br/>
<input type="submit" value="{_'write'}" class="button" />
<div style="float: right;">
<a href="javascript:void(document.querySelector(`input[name=_pic_attachment]`).click());">
{_"attach_photo"}
</a>
</div>
</form>
</div>
</div> </div>
<br/> <br/>
@ -41,7 +23,7 @@
{foreach $posts as $post} {foreach $posts as $post}
<a name="postGarter={$post->getId()}"></a> <a name="postGarter={$post->getId()}"></a>
{include "../components/post.xml", post => $post, onWallOf => true} {include "../components/post.xml", post => $post, onWallOf => true, commentSection => true}
{/foreach} {/foreach}
{include "../components/paginator.xml", conf => $paginatorConf} {include "../components/paginator.xml", conf => $paginatorConf}
<br/> <br/>

View file

@ -15,52 +15,8 @@
{block content} {block content}
<div class="content_divider"> <div class="content_divider">
<div> <div>
<!-- TODO: Move the creating post form to dedicated file -->
<div n:if="$canPost" class="content_subtitle"> <div n:if="$canPost" class="content_subtitle">
<div id="write" style="padding: 5px 0;" > {include "../components/textArea.xml", route => "/wall$owner/makePost"}
<form action="/wall{$owner}/makePost" method="post" enctype="multipart/form-data" style="margin:0;">
<textarea id="wall-post-input" placeholder="{_write}" name="text" style="width: 100%;resize: none;" class="expanded-textarea"></textarea>
<div>
<!-- padding to fix <br/> bug -->
</div>
<div id="post-buttons">
<div class="post-upload">
Вложение: <span>(unknown)</span>
</div>
<div class="post-opts">
{if !is_null($thisUser) && $owner < 0 && $club->canBeModifiedBy($thisUser)}
<script>
function onWallAsGroupClick(el) {
_display = el.checked ? "block" : "none";
document.querySelector("#forceSignOpt").style.display = _display;
}
</script>
<label>
<input type="checkbox" name="as_group" onchange="onWallAsGroupClick(this)" /> От имени сообщества
</label>
<label id="forceSignOpt" style="display: none;">
<input type="checkbox" name="force_sign" /> Подпись автора
</label>
{/if}
<label>
<input type="checkbox" name="nsfw" /> Содержит NSFW-контент
</label>
</div>
<input type="file" name="_pic_attachment" accept="image/*" style="display:none;" />
<input type="hidden" name="type" value="1" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<br/>
<input type="submit" value="{_'write'}" class="button" />
<div style="float: right;">
<a href="javascript:void(document.querySelector(`input[name=_pic_attachment]`).click());">
{_attach_photo}
</a>
</div>
</div>
</form>
</div>
</div> </div>
<div class="content"> <div class="content">
@ -68,7 +24,7 @@
{foreach $posts as $post} {foreach $posts as $post}
<a name="postGarter={$post->getId()}"></a> <a name="postGarter={$post->getId()}"></a>
{include "../components/post.xml", post => $post} {include "../components/post.xml", post => $post, commentSection => true}
{/foreach} {/foreach}
{include "../components/paginator.xml", conf => $paginatorConf} {include "../components/paginator.xml", conf => $paginatorConf}
{else} {else}

View file

@ -1,6 +1,7 @@
{if $attachment instanceof \openvk\Web\Models\Entities\Photo} {if $attachment instanceof \openvk\Web\Models\Entities\Photo}
{if !$attachment->isDeleted()} {if !$attachment->isDeleted()}
<a href="/photo{$attachment->getPrettyId()}"> {var link = "/photo" . ($attachment->isAnonymous() ? ("s/" . base_convert((string) $attachment->getId(), 10, 32)) : $attachment->getPrettyId())}
<a href="{$link}">
<img class="media" src="{$attachment->getURL()}" alt="{$attachment->getDescription()}" /> <img class="media" src="{$attachment->getURL()}" alt="{$attachment->getDescription()}" />
</a> </a>
{else} {else}
@ -8,6 +9,8 @@
<img class="media" src="/assets/packages/static/openvk/img/camera_200.png" alt="{_"attach_no_longer_available"}" /> <img class="media" src="/assets/packages/static/openvk/img/camera_200.png" alt="{_"attach_no_longer_available"}" />
</a> </a>
{/if} {/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
<video class="media" src="{$attachment->getURL()}" controls="controls"></video>
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post} {elseif $attachment instanceof \openvk\Web\Models\Entities\Post}
{php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1} {php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1}
{if $GLOBALS["_nesAttGloCou"] > 2} {if $GLOBALS["_nesAttGloCou"] > 2}

View file

@ -1,36 +1,44 @@
{var author = $comment->getOwner()} {var author = $comment->getOwner()}
{var $Club = openvk\Web\Models\Entities\Club::class}
<a name="cid={$comment->getId()}"></a> <a name="cid={$comment->getId()}"></a>
<table border="0" style="font-size: 11px;" class="post"> <table border="0" style="font-size: 11px;" class="post comment" id="_comment{$comment->getId()}" data-comment-id="{$comment->getId()}" data-owner-id="{$author->getId()}" data-from-group="{$comment->getOwner() instanceof $Club}">
<tbody> <tbody>
<tr> <tr>
<td width="54" valign="top"> <td width="30" valign="top">
<img <img
src="{$author->getAvatarURL()}" src="{$author->getAvatarURL()}"
width="50" /> width="30"
class="cCompactAvatars" />
</td> </td>
<td width="345" valign="top"> <td width="100%" valign="top">
<div class="post-author"> <div class="post-author">
<a href="{$author->getURL()}"><b> <a href="{$author->getURL()}"><b>
{$author->getCanonicalName()} {$author->getCanonicalName()}
</b></a> {$author->isFemale() ? tr("post_writes_f") : tr("post_writes_m")}<br/> </b></a><br/>
<a href="/comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}</a>
</div> </div>
<div class="post-content" id="{$comment->getId()}"> <div class="post-content" id="{$comment->getId()}">
<div class="text" id="text{$comment->getId()}"> <div class="text" id="text{$comment->getId()}">
{$comment->getText()|noescape} {$comment->getText()|noescape}
<div n:ifcontent class="attachments_b">
<div class="attachment" n:foreach="$comment->getChildren() as $attachment" data-localized-nsfw-text="{_nsfw_warning}">
{include "attachment.xml", attachment => $attachment}
</div>
</div>
</div> </div>
<div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu"> <div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu">
<a href="#_comment{$comment->getId()}" class="date">{$comment->getPublicationTime()}</a>&nbsp;|
{var canDelete = $comment->getOwner()->getId() == $thisUser->getId()} {var canDelete = $comment->getOwner()->getId() == $thisUser->getId()}
{var canDelete = $canDelete || $comment->getTarget()->getOwner()->getId() == $thisUser->getId()} {var canDelete = $canDelete || $comment->getTarget()->getOwner()->getId() == $thisUser->getId()}
{if $canDelete} {if $canDelete}
<a href="/comment{$comment->getId()}/delete">{_"delete"}</a> <a href="/comment{$comment->getId()}/delete">{_"delete"}</a>&nbsp;|
{/if} {/if}
<a class="comment-reply">Ответить</a>
<div style="float: right; font-size: .7rem;"> <div style="float: right; font-size: .7rem;">
<a href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}"> <a class="post-like-button" href="/comment{$comment->getId()}/like?hash={rawurlencode($csrfToken)}">
<div class="heart" style="{if $comment->hasLikeFrom($thisUser)}opacity: 1;{else}opacity: 0.4;{/if}"></div> <div class="heart" style="{if $comment->hasLikeFrom($thisUser)}opacity: 1;{else}opacity: 0.4;{/if}"></div>
<span class="likeCnt">{$comment->getLikesCount()}</span> <span class="likeCnt">{if $comment->getLikesCount() > 0}{$comment->getLikesCount()}{/if}</span>
</a> </a>
</div> </div>
</div> </div>

View file

@ -1,12 +1,9 @@
<h4>{_"comments"} ({$count})</h4> <h4>{_"comments"} ({$count})</h4>
<div n:ifset="$thisUser"> <div n:ifset="$thisUser">
<form action="/al_comments.pl/create/{$model}/{$parent->getId()}" method="POST" style="margin-top: 2pt;"> {var commentsURL = "/al_comments.pl/create/$model/" . $parent->getId()}
<textarea name="text" style="width: 411px; margin: 0px; height: 53px; resize: none;"></textarea> {var club = $parent instanceof \openvk\Web\Models\Entities\Post && $parent->getTargetWall() < 0 ? (new openvk\Web\Models\Repositories\Clubs)->get(abs($parent->getTargetWall())) : NULL}
<br/> {include "textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), club => $club}
<input type="hidden" value="{$csrfToken}" name="hash" />
<input type="submit" value="{_'write'}" class="button" style="bottom-right: 50px;" />
</form>
</div> </div>
{if sizeof($comments) > 0} {if sizeof($comments) > 0}
@ -28,3 +25,5 @@
{/if} --> {/if} -->
{_"comments_tip"} {_"comments_tip"}
{/if} {/if}
{script "js/al_comments.js"}

View file

@ -4,8 +4,3 @@
<a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a> <a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a>
{$notification->getDateTime()} {_nt_liked_yours} {$notification->getDateTime()} {_nt_liked_yours}
<a href="/wall{$post->getPrettyId()}"><b>{_nt_post_nominative}</b></a> {_nt_from} {$post->getPublicationTime()}. <a href="/wall{$post->getPrettyId()}"><b>{_nt_post_nominative}</b></a> {_nt_from} {$post->getPublicationTime()}.
<?php
// костыльно скрыл лол, сами исправите проблему - гфх
//{tr('notifications_like', '<a href="'.$user->getURL().'"><b>'.$user->getCanonicalName().'</b></a>', '<a href="/wall'.$post->getPrettyId().'"><b>', '</b></a>', $post->getPublicationTime())}
?>

View file

@ -0,0 +1,4 @@
{var gift = $notification->getModel(0)}
{var sender = $notification->getModel(1)}
<a href="{$sender->getURL()}"><b>{$sender->getCanonicalName()}</b></a> отправил вам {$notification->getDateTime()} подарок.

View file

@ -1,14 +1,16 @@
<div n:if="!($conf->page === 1 && $conf->count <= $conf->perPage)" class="paginator"> {var $space = 2}
<br/> {var $pageCount = ceil($conf->count / $conf->perPage)}
<center>
<a n:if="$conf->page != 1"
href="?{http_build_query(array_merge($_GET, ['p' => ($conf->page - 1)]), 'k', '&', PHP_QUERY_RFC3986)}"
style="float: left;">&lt;&lt; {_paginator_back}</a>
{tr("paginator_page", $conf->page)} <div n:if="!($conf->page === 1 && $conf->count <= $conf->perPage)" n:class="paginator, $conf->atBottom ? paginator-at-bottom">
{if $conf->page > $space}
<a n:if="$conf->count > (($conf->page - 1) * $conf->perPage + $conf->amount) && $conf->amount > 0" <a n:attr="class => ($conf->page === 1 ? 'active')" href="?{http_build_query(array_merge($_GET, ['p' => 1]), 'k', '&', PHP_QUERY_RFC3986)}">«</a>
href="?{http_build_query(array_merge($_GET, ['p' => ($conf->page + 1)]), 'k', '&', PHP_QUERY_RFC3986)}" {/if}
style="float: right;">{_paginator_next} &gt;&gt;</a> {for $j = $conf->page - ($space-1); $j <= $conf->page + ($space-1); $j++}
</center> {if $j > 0 && $j <= $pageCount}
<a n:attr="class => ($conf->page === $j ? 'active')" href="?{http_build_query(array_merge($_GET, ['p' => $j]), 'k', '&', PHP_QUERY_RFC3986)}">{$j}</a>
{/if}
{/for}
{if $conf->page <= $pageCount-$space}
<a n:attr="class => ($conf->page === $pageCount ? 'active')" href="?{http_build_query(array_merge($_GET, ['p' => $pageCount]), 'k', '&', PHP_QUERY_RFC3986)}">»</a>
{/if}
</div> </div>

View file

@ -1,7 +1,12 @@
{var microblogEnabled = isset($thisUser) ? $thisUser->hasMicroblogEnabled() : false} {var microblogEnabled = isset($thisUser) ? $thisUser->hasMicroblogEnabled() : false}
{if !$post->isPostedOnBehalfOfGroup()}
{var then = date_create("@" . $post->getOwner()->getOnline()->timestamp())}
{var now = date_create()}
{var diff = date_diff($now, $then)}
{/if}
{if $microblogEnabled} {if $microblogEnabled}
{include "post/microblogpost.xml", post => $post} {include "post/microblogpost.xml", post => $post, diff => $diff, commentSection => $commentSection}
{else} {else}
{include "post/oldpost.xml", post => $post} {include "post/oldpost.xml", post => $post, diff => $diff}
{/if} {/if}

View file

@ -1,4 +1,8 @@
{var author = $post->getOwner()} {var author = $post->getOwner()}
{var comments = $post->getLastComments(3)}
{var commentsCount = $post->getCommentsCount()}
{var commentTextAreaId = $post === null ? rand(1,300) : $post->getId()}
<table border="0" style="font-size: 11px;" n:class="post, !$compact ? post-divider, $post->isExplicit() ? post-nsfw"> <table border="0" style="font-size: 11px;" n:class="post, !$compact ? post-divider, $post->isExplicit() ? post-nsfw">
<tbody> <tbody>
@ -7,6 +11,11 @@
<img <img
src="{$author->getAvatarURL()}" src="{$author->getAvatarURL()}"
width="{ifset $compact}25{else}50{/ifset}" /> width="{ifset $compact}25{else}50{/ifset}" />
{if !$post->isPostedOnBehalfOfGroup() && !$compact}
<span n:if="$author->isOnline()" class="post-online">
{_online}
</span>
{/if}
</td> </td>
<td width="100%" valign="top"> <td width="100%" valign="top">
<div class="post-author"> <div class="post-author">
@ -18,24 +27,22 @@
{if $author->isVerified()}<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">{/if} {if $author->isVerified()}<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">{/if}
{ifset $compact}<br> {ifset $compact}<br>
<a href="/wall{$post->getPrettyId()}" class="date"> <a href="/wall{$post->getPrettyId()}" class="date">
{if $post->isPinned()}
{$post->getPublicationTime()},
{_pinned}
{else}
{$post->getPublicationTime()} {$post->getPublicationTime()}
{/if}
</a> </a>
{/ifset} {/ifset}
{if $post->isPinned()}
<span class="nobold">{_pinned}</span>
{/if}
{if $post->canBeDeletedBy($thisUser) && !($forceNoDeleteLink ?? false) && !isset($compact)} {if $post->canBeDeletedBy($thisUser) && !($forceNoDeleteLink ?? false) && !isset($compact)}
<a class="delete" href="/wall{$post->getPrettyId()}/delete"></a> <a class="delete" href="/wall{$post->getPrettyId()}/delete"></a>
{/if} {/if}
{if $post->canBePinnedBy($thisUser) && !($forceNoPinLink ?? false) && !isset($compact)} {if $post->canBePinnedBy($thisUser) && !($forceNoPinLink ?? false) && !isset($compact)}
{if $post->isPinned()} {if $post->isPinned()}
<a class="delete" href="/wall{$post->getPrettyId()}/pin?act=unpin&hash={rawurlencode($csrfToken)}"></a> <a class="pin" href="/wall{$post->getPrettyId()}/pin?act=unpin&hash={rawurlencode($csrfToken)}"></a>
{else} {else}
<a class="delete" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a> <a class="pin" href="/wall{$post->getPrettyId()}/pin?act=pin&hash={rawurlencode($csrfToken)}"></a>
{/if} {/if}
{/if} {/if}
</div> </div>
@ -69,23 +76,15 @@
&nbsp; &nbsp;
{if !($forceNoCommentsLink ?? false)} {if !($forceNoCommentsLink ?? false)}
<a href="/wall{$post->getPrettyId()}#comments"> <a n:if="$commentsCount == 0" href="javascript:expand_comment_textarea({$commentTextAreaId})">
{if $post->getCommentsCount() > 0} {_"comment"}
{_"comments"} (<b>{$post->getCommentsCount()}</b>)
{else}
{_"comments"}
{/if}
</a> </a>
{/if} {/if}
<div class="like_wrap"> <div class="like_wrap">
<a class="post-share-button" href="/wall{$post->getPrettyId()}/repost?hash={rawurlencode($csrfToken)}" <a class="post-share-button" href="javascript:repostPost('{$post->getPrettyId()}', '{rawurlencode($csrfToken)}')">
class="post-like-button">
<div class="repost-icon" style="opacity: 0.4;"></div> <div class="repost-icon" style="opacity: 0.4;"></div>
<span class="likeCnt">{$post->getRepostCount()}</span> <span class="likeCnt">{if $post->getRepostCount() > 0}{$post->getRepostCount()}{/if}</span>
</a> </a>
{var liked = $post->hasLikeFrom($thisUser)} {var liked = $post->hasLikeFrom($thisUser)}
@ -93,14 +92,23 @@
class="post-like-button" class="post-like-button"
data-liked="{(int) $liked}" data-liked="{(int) $liked}"
data-likes="{$post->getLikesCount()}"> data-likes="{$post->getLikesCount()}">
<div class="heart" style="{if $liked}opacity: 1;{else}opacity: 0.4;{/if}"></div> <div class="heart" id="{if $liked}liked{/if}"></div>
<span class="likeCnt">{$post->getLikesCount()}</span> <span class="likeCnt">{if $post->getLikesCount() > 0}{$post->getLikesCount()}{/if}</span>
</a> </a>
</div> </div>
{/if} {/if}
</div> </div>
<div n:if="isset($thisUser) &&! ($compact ?? false)" class="post-menu-s"> <div n:if="$commentSection == true && $compact == false" class="post-menu-s">
<!-- kosfurler --> {if $commentsCount > 3}
<a href="/wall{$post->getPrettyId()}" class="expand_button">{_view_other_comments}</a>
{/if}
{foreach $comments as $comment}
{include "../comment.xml", comment => $comment, $compact => true}
{/foreach}
<div n:ifset="$thisUser" id="commentTextArea{$commentTextAreaId}" n:attr="style => ($commentsCount == 0 ? 'display: none;')" class="commentsTextFieldWrap">
{var commentsURL = "/al_comments.pl/create/posts/" . $post->getId()}
{include "../textArea.xml", route => $commentsURL, postOpts => false, graffiti => (bool) ovkGetQuirk("comments.allow-graffiti"), post => $post}
</div>
</div> </div>
</td> </td>
</tr> </tr>

Some files were not shown because too many files have changed in this diff Show more