Merge branch 'OpenVK:master' into master

This commit is contained in:
ayato 2025-07-01 02:49:05 +05:00 committed by GitHub
commit c61f1a05bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 863 additions and 422 deletions

View file

@ -98,6 +98,10 @@ final class Friends extends VKAPIRequestHandler
switch ($user->getSubscriptionStatus($this->getUser())) { switch ($user->getSubscriptionStatus($this->getUser())) {
case 0: case 0:
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "friends.outgoing_sub")) {
$this->failTooOften();
}
$user->toggleSubscription($this->getUser()); $user->toggleSubscription($this->getUser());
return 1; return 1;

View file

@ -61,6 +61,10 @@ final class Gifts extends VKAPIRequestHandler
$this->fail(-105, "Commerce is disabled on this instance"); $this->fail(-105, "Commerce is disabled on this instance");
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "gifts.send", false)) {
$this->failTooOften();
}
$user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids) $user = (new UsersRepo())->get((int) $user_ids); # FAKE прогноз погоды (в данном случае user_ids)
if (!$user || $user->isDeleted()) { if (!$user || $user->isDeleted()) {

View file

@ -312,6 +312,10 @@ final class Groups extends VKAPIRequestHandler
$isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0; $isMember = !is_null($this->getUser()) ? (int) $club->getSubscriptionStatus($this->getUser()) : 0;
if ($isMember == 0) { if ($isMember == 0) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "groups.sub")) {
$this->failTooOften();
}
$club->toggleSubscription($this->getUser()); $club->toggleSubscription($this->getUser());
} }

View file

@ -317,6 +317,32 @@ final class Users extends VKAPIRequestHandler
$response[$i]->custom_fields = $append_array; $response[$i]->custom_fields = $append_array;
break; break;
case "bdate":
if (!$canView) {
$response[$i]->bdate = "01.01.1970";
break;
}
$visibility = $usr->getBirthdayPrivacy();
$response[$i]->bdate_visibility = $visibility;
$birthday = $usr->getBirthday();
if ($birthday) {
switch ($visibility) {
case 1:
$response[$i]->bdate = $birthday->format('%d.%m');
break;
case 2:
$response[$i]->bdate = $birthday->format('%d.%m.%Y');
break;
case 0:
default:
$response[$i]->bdate = null;
break;
}
} else {
$response[$i]->bdate = null;
}
break;
} }
} }

View file

@ -25,6 +25,11 @@ abstract class VKAPIRequestHandler
throw new APIErrorException($message, $code); throw new APIErrorException($message, $code);
} }
protected function failTooOften(): never
{
$this->fail(9, "Rate limited");
}
protected function getUser(): ?User protected function getUser(): ?User
{ {
return $this->user; return $this->user;

View file

@ -713,6 +713,10 @@ final class Wall extends VKAPIRequestHandler
$post->setSuggested(1); $post->setSuggested(1);
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->getUser(), "wall.post")) {
$this->failTooOften();
}
$post->save(); $post->save();
} catch (\LogicException $ex) { } catch (\LogicException $ex) {
$this->fail(100, "One of the parameters specified was missing or invalid"); $this->fail(100, "One of the parameters specified was missing or invalid");
@ -723,7 +727,7 @@ final class Wall extends VKAPIRequestHandler
} }
if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) { if ($owner_id > 0 && $owner_id !== $this->getUser()->getId()) {
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->getUser()))->emit();
} }
return (object) ["post_id" => $post->getVirtualId()]; return (object) ["post_id" => $post->getVirtualId()];
@ -873,6 +877,8 @@ final class Wall extends VKAPIRequestHandler
"id" => $comment->getId(), "id" => $comment->getId(),
"from_id" => $oid, "from_id" => $oid,
"date" => $comment->getPublicationTime()->timestamp(), "date" => $comment->getPublicationTime()->timestamp(),
"can_edit" => $post->canBeEditedBy($this->getUser()),
"can_delete" => $post->canBeDeletedBy($this->getUser()),
"text" => $comment->getText(false), "text" => $comment->getText(false),
"post_id" => $post->getVirtualId(), "post_id" => $post->getVirtualId(),
"owner_id" => method_exists($post, 'isPostedOnBehalfOfGroup') && $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(), "owner_id" => method_exists($post, 'isPostedOnBehalfOfGroup') && $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(),

View file

@ -176,11 +176,13 @@ class Post extends Postable
$platform = $this->getRecord()->api_source_name; $platform = $this->getRecord()->api_source_name;
if ($forAPI) { if ($forAPI) {
switch ($platform) { switch ($platform) {
case 'openvk_native':
case 'openvk_refresh_android': case 'openvk_refresh_android':
case 'openvk_legacy_android': case 'openvk_legacy_android':
return 'android'; return 'android';
break; break;
case 'openvk_native_ios':
case 'openvk_ios': case 'openvk_ios':
case 'openvk_legacy_ios': case 'openvk_legacy_ios':
return 'iphone'; return 'iphone';

View file

@ -34,9 +34,9 @@ trait TSubscribable
"target" => $this->getId(), "target" => $this->getId(),
]; ];
$sub = $ctx->table("subscriptions")->where($data); $sub = $ctx->table("subscriptions")->where($data);
if (!($sub->fetch())) { if (!($sub->fetch())) {
$ctx->table("subscriptions")->insert($data); $ctx->table("subscriptions")->insert($data);
return true; return true;
} }

View file

@ -971,11 +971,13 @@ class User extends RowModel
$platform = $this->getRecord()->client_name; $platform = $this->getRecord()->client_name;
if ($forAPI) { if ($forAPI) {
switch ($platform) { switch ($platform) {
case 'openvk_native':
case 'openvk_refresh_android': case 'openvk_refresh_android':
case 'openvk_legacy_android': case 'openvk_legacy_android':
return 'android'; return 'android';
break; break;
case 'openvk_native_ios':
case 'openvk_ios': case 'openvk_ios':
case 'openvk_legacy_ios': case 'openvk_legacy_ios':
return 'iphone'; return 'iphone';
@ -1738,4 +1740,52 @@ class User extends RowModel
{ {
return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count(); return DatabaseConnection::i()->getContext()->table("blacklist_relations")->where("author", $this->getId())->count();
} }
public function getEventCounters(array $list): array
{
$count_of_keys = sizeof(array_keys($list));
$ev_str = $this->getRecord()->events_counters;
$counters = [];
if (!$ev_str) {
for ($i = 0; $i < sizeof(array_keys($list)); $i++) {
$counters[] = 0;
}
} else {
$counters = unpack("S" . $count_of_keys, base64_decode($ev_str, true));
}
return [
'counters' => array_combine(array_keys($list), $counters),
'refresh_time' => $this->getRecord()->events_refresh_time,
];
}
public function stateEvents(array $state_list): void
{
$pack_str = "";
foreach ($state_list as $item => $id) {
$pack_str .= "S";
}
$this->stateChanges("events_counters", base64_encode(pack($pack_str, ...array_values($state_list))));
if (!$this->getRecord()->events_refresh_time) {
$this->stateChanges("events_refresh_time", time());
}
}
public function resetEvents(array $list): void
{
$values = [];
foreach ($list as $key => $val) {
$values[$key] = 0;
}
$this->stateEvents($values);
$this->stateChanges("events_refresh_time", time());
$this->save();
}
} }

View file

@ -147,6 +147,29 @@ final class AboutPresenter extends OpenVKPresenter
$this->redirect("https://github.com/openvk/openvk#readme"); $this->redirect("https://github.com/openvk/openvk#readme");
} }
public function renderAssetLinksJSON(): void
{
# Необходимо любому андроид приложению для автоматического разрешения принимать ссылки с этого сайта.
# Не шарю как писать норм на php поэтому тут чутка на вайбкодил - искренне ваш, ZAZiOs.
header("Content-Type: application/json");
$data = [
[
"relation" => ["delegate_permission/common.handle_all_urls"],
"target" => [
"namespace" => "android_app",
"package_name" => "oss.OpenVK.Native",
"sha256_cert_fingerprints" => [
"79:67:14:23:DC:6E:FA:49:64:1F:F1:81:0E:B0:A3:AE:6E:88:AB:0D:CF:BC:02:96:F3:6D:76:6B:82:94:D6:9C",
],
],
],
];
echo json_encode($data, JSON_UNESCAPED_SLASHES);
exit;
}
public function renderDev(): void public function renderDev(): void
{ {
$this->redirect("https://docs.ovk.to/"); $this->redirect("https://docs.ovk.to/");

View file

@ -106,6 +106,10 @@ final class GiftsPresenter extends OpenVKPresenter
return; return;
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "gifts.send")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$comment = empty($c = $this->postParam("comment")) ? null : $c; $comment = empty($c = $this->postParam("comment")) ? null : $c;
$notification = new GiftNotification($user, $this->user->identity, $gift, $comment); $notification = new GiftNotification($user, $this->user->identity, $gift, $comment);
$notification->emit(); $notification->emit();

View file

@ -68,6 +68,10 @@ 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->setOwner($this->user->id); $club->setOwner($this->user->id);
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.create")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
try { try {
$club->save(); $club->save();
} catch (\PDOException $ex) { } catch (\PDOException $ex) {
@ -79,6 +83,7 @@ final class GroupPresenter extends OpenVKPresenter
} }
$club->toggleSubscription($this->user->identity); $club->toggleSubscription($this->user->identity);
$this->redirect("/club" . $club->getId()); $this->redirect("/club" . $club->getId());
} else { } else {
$this->flashFail("err", tr("error"), tr("error_no_group_name")); $this->flashFail("err", tr("error"), tr("error_no_group_name"));
@ -103,6 +108,12 @@ final class GroupPresenter extends OpenVKPresenter
$this->flashFail("err", tr("error"), tr("forbidden")); $this->flashFail("err", tr("error"), tr("forbidden"));
} }
if (!$club->getSubscriptionStatus($this->user->identity)) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "groups.sub")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
}
$club->toggleSubscription($this->user->identity); $club->toggleSubscription($this->user->identity);
$this->redirect($club->getURL()); $this->redirect($club->getURL());

View file

@ -103,6 +103,10 @@ final class ReportPresenter extends OpenVKPresenter
exit(json_encode([ "error" => "You can't report yourself" ])); exit(json_encode([ "error" => "You can't report yourself" ]));
} }
if ($this->user->identity->isBannedInSupport()) {
exit(json_encode([ "reason" => $this->queryParam("reason") ]));
}
if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) { if (in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio", "doc"])) {
if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, null, $this->user->id))) <= 0) {
$report = new Report(); $report = new Report();

View file

@ -418,6 +418,12 @@ final class UserPresenter extends OpenVKPresenter
if ($this->postParam("act") == "rej") { if ($this->postParam("act") == "rej") {
$user->changeFlags($this->user->identity, 0b10000000, true); $user->changeFlags($this->user->identity, 0b10000000, true);
} else { } else {
if ($user->getSubscriptionStatus($this->user->identity) == \openvk\Web\Models\Entities\User::SUBSCRIPTION_ABSENT) {
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "friends.outgoing_sub")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
}
$user->toggleSubscription($this->user->identity); $user->toggleSubscription($this->user->identity);
} }

View file

@ -356,6 +356,10 @@ final class WallPresenter extends OpenVKPresenter
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
} }
if (\openvk\Web\Util\EventRateLimiter::i()->tryToLimit($this->user->identity, "wall.post")) {
$this->flashFail("err", tr("error"), tr("limit_exceed_exception"));
}
$should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2; $should_be_suggested = $wall < 0 && !$wallOwner->canBeModifiedBy($this->user->identity) && $wallOwner->getWallType() == 2;
try { try {
$post = new Post(); $post = new Post();

View file

@ -2,15 +2,69 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
<style> <style>
{var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")} {var $css = file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.css")}
{str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape} {str_replace("fonts/", "/assets/packages/static/openvk/js/node_modules/@atlassian/aui/dist/aui/fonts/", $css)|noescape}
{file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping-darkmode.css")|noescape} {file_get_contents(OPENVK_ROOT . "/Web/static/js/node_modules/@atlassian/aui/dist/aui/aui-prototyping-darkmode.css")|noescape}
.fake-icon {
float: left;
width: 20px;
margin-right: 10px;
}
.aui-sidebar[aria-expanded="false"] .aui-sidebar-group-tier-one .aui-nav > li > .aui-nav-item .fake-icon {
margin-right: 0;
float: none;
}
@media (max-width: 600px) {
.aui-sidebar {
min-width: 0px;
}
.aui-page-sidebar.aui-sidebar-collapsed {
--aui-sidebar-width: 0px;
}
.aui-sidebar[aria-expanded="false"] .aui-sidebar-footer {
position:fixed;
background-color:var(--aui-sidebar-bg-color);
bottom:0;
left:0;
width: 56px;
}
.aui-page-panel {
overflow-x:auto;
width:100vw;
}
table.aui {
white-space: nowrap;
}
form.aui:not(.aui-legacy-forms) .date-select, form.aui:not(.aui-legacy-forms) .field-group, form.aui:not(.aui-legacy-forms) .group {
padding-left: 0;
}
form.aui:not(.aui-legacy-forms) .field-group > aui-label, form.aui:not(.aui-legacy-forms) .field-group > label, form.aui:not(.aui-legacy-forms) legend {
float: none;
margin-left: 0;
padding: 5px 0 5px;
text-align: inherit;
width: 100%;
display: block;
}
form.aui:not(.aui-legacy-forms) > .field-group:has(input[type="checkbox"]) {
display: flex;
}
form.aui:not(.aui-legacy-forms) .select, form.aui:not(.aui-legacy-forms) .text, form.aui:not(.aui-legacy-forms) .textarea {
max-width: 100%;
}
form.aui .field-group::after, form.aui .field-group::before {
display: none;
}
}
</style> </style>
<title>{include title} - {_admin} {$instance_name}</title> <title>{include title} - {_admin} {$instance_name}</title>
</head> </head>
<body> <body class="aui-page aui-page-sidebar">
<div id="page"> <div id="page">
<header id="header" role="banner"> <header id="header" role="banner">
<nav class="aui-header aui-dropdown2-trigger-group" role="navigation"> <nav class="aui-header aui-dropdown2-trigger-group" role="navigation">
@ -69,97 +123,135 @@
</div> </div>
</div> </div>
</nav> </nav>
</header> </header>
<div class="aui-page-panel"> <div id="content">
<div class="aui-page-panel-inner"> <div class="aui-sidebar" id="admin-sidebar" aria-label="Admin sidebar">
<div class="aui-page-panel-nav"> <div class="aui-sidebar-wrapper" aria-expanded="true">
<nav class="aui-navgroup aui-navgroup-vertical"> <div class="aui-sidebar-body">
<div class="aui-navgroup-inner"> <nav class="aui-navgroup aui-navgroup-vertical">
<div class="aui-navgroup-primary"> <div class="aui-navgroup-inner">
<div class="aui-nav-heading"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<strong>{_admin_overview}</strong> <div class="aui-nav-heading">
<strong>{_admin_overview}</strong>
</div>
<ul class="aui-nav">
<li>
<a class="aui-nav-item" href="/admin">
<span class="aui-icon aui-icon-small aui-iconfont-dashboard"></span>
<span class="aui-nav-item-label">{_admin_overview_summary}</span>
</a>
</li>
</ul>
</div> </div>
<ul class="aui-nav"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<li> <div class="aui-nav-heading">
<a href="/admin">{_admin_overview_summary}</a> <strong>{_admin_content}</strong>
</li> </div>
</ul> <ul class="aui-nav">
<div class="aui-nav-heading"> <li>
<strong>{_admin_content}</strong> <a class="aui-nav-item" href="/admin/users">
<span class="aui-icon aui-icon-small aui-iconfont-people"></span>
<span class="aui-nav-item-label">{_users}</span>
</a>
</li>
<li>
<a class="aui-nav-item" href="/admin/clubs">
<span class="aui-icon aui-icon-small aui-iconfont-group"></span>
<span class="aui-nav-item-label">{_groups}</span>
</a>
</li>
<li>
<a class="aui-nav-item" href="/admin/bannedLinks">
<span class="aui-icon aui-icon-small aui-iconfont-cross-circle"></span>
<span class="aui-nav-item-label">{_admin_banned_links}</span>
</a>
</li>
<li>
<a class="aui-nav-item" href="/admin/music">
<span class="aui-icon aui-icon-small aui-iconfont-audio"></span>
<span class="aui-nav-item-label">{_admin_music}</span>
</a>
</li>
</ul>
</div> </div>
<ul class="aui-nav"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<li> <div class="aui-nav-heading">
<a href="/admin/users">{_users}</a> <strong>Chandler</strong>
</li> </div>
<li> <ul class="aui-nav">
<a href="/admin/clubs">{_groups}</a> <li>
</li> <a class="aui-nav-item" href="/admin/chandler/groups">
<li> <span class="aui-icon aui-icon-small aui-iconfont-group"></span>
<a href="/admin/bannedLinks">{_admin_banned_links}</a> <span class="aui-nav-item-label">{_c_groups}</span>
</li> </a>
<li> </li>
<a href="/admin/music">{_admin_music}</a> </ul>
</li>
</ul>
<div class="aui-nav-heading">
<strong>Chandler</strong>
</div> </div>
<ul class="aui-nav"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<li> <div class="aui-nav-heading">
<a href="/admin/chandler/groups">{_c_groups}</a> <strong>{_admin_services}</strong>
</li> </div>
</ul> <ul class="aui-nav">
<div class="aui-nav-heading"> <li>
<strong>{_admin_services}</strong> <a class="aui-nav-item" href="/admin/vouchers">
<span class="aui-icon aui-icon-small aui-iconfont-credit-card"></span>
<span class="aui-nav-item-label">{_vouchers}</span>
</a>
</li>
<li>
<a class="aui-nav-item" href="/admin/gifts">
<span class="fake-icon">🎁</span>
<span class="aui-nav-item-label">{_gifts}</span>
</a>
</li>
</ul>
</div> </div>
<ul class="aui-nav"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<li> <div class="aui-nav-heading">
<a href="/admin/vouchers">{_vouchers}</a> <strong>{_admin_settings}</strong>
</li> </div>
<li> <ul class="aui-nav">
<a href="/admin/gifts">{_gifts}</a> <li>
</li> <a class="aui-nav-item" href="/admin/logs">
</ul> <span class="aui-icon aui-icon-small aui-iconfont-list"></span>
<div class="aui-nav-heading"> <span class="aui-nav-item-label">{_logs}</span>
<strong>{_admin_settings}</strong> </a>
</li>
</ul>
</div> </div>
<ul class="aui-nav"> <div class="aui-sidebar-group aui-sidebar-group-tier-one">
<li> <div class="aui-nav-heading">
<a href="/admin/settings/tuning">{_admin_settings_tuning}</a> <strong>{_admin_about}</strong>
</li> </div>
<li> <ul class="aui-nav">
<a href="/admin/logs">Логи</a> <li>
</li> <a class="aui-nav-item" href="/about:openvk">
<li> <span class="aui-icon aui-icon-small aui-iconfont-info"></span>
<a href="/admin/settings/appearance">{_admin_settings_appearance}</a> <span class="aui-nav-item-label">{_admin_about_version}</span>
</li> </a>
<li> </li>
<a href="/admin/settings/security">{_admin_settings_security}</a> <li>
</li> <a class="aui-nav-item" href="/about">
<li> <span class="aui-icon aui-icon-small aui-iconfont-info-filled"></span>
<a href="/admin/settings/integrations">{_admin_settings_integrations}</a> <span class="aui-nav-item-label">{_admin_about_instance}</span>
</li> </a>
<li> </li>
<a href="/admin/settings/system">{_admin_settings_system}</a> </ul>
</li>
</ul>
<div class="aui-nav-heading">
<strong>{_admin_about}</strong>
</div> </div>
<ul class="aui-nav">
<li>
<a href="/about:openvk">{_admin_about_version}</a>
</li>
<li>
<a href="/about">{_admin_about_instance}</a>
</li>
</ul>
</div> </div>
</div> </nav>
</nav> </div>
<div class="aui-sidebar-footer" style="padding: 10px; text-align: center;">
<button type="button" id="sidebar-toggle" class="aui-button aui-button-subtle aui-sidebar-toggle aui-sidebar-footer-tipsy" aria-label="Toggle sidebar">
<span class="aui-icon aui-icon-small aui-iconfont-chevron-double-left"></span>
</button>
</div>
</div> </div>
<section class="aui-page-panel-content"> </div>
{ifset $flashMessage} <main class="aui-page-panel" id="main" role="main">
<div class="aui-page-panel-inner">
<div class="aui-page-panel-content">
{ifset $flashMessage}
{var $type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]} {var $type = ["err" => "error", "warn" => "warning", "info" => "basic", "succ" => "success"][$flashMessage->type]}
<div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;"> <div class="aui-message aui-message-{$type}" style="margin-bottom: 15px;">
<p class="title"> <p class="title">
@ -187,20 +279,43 @@
<main> <main>
{include content} {include content}
</main> </main>
</section> </div>
</div> </div>
</div> </main>
<footer id="footer" role="contentinfo">
<section class="footer-body">
OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {\Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)}
</section>
</footer>
</div> </div>
<footer id="footer" role="contentinfo">
<section class="footer-body">
OpenVK <a href="/about:openvk">{php echo OPENVK_VERSION}</a> | PHP: {phpversion()} | DB: {\Chandler\Database\DatabaseConnection::i()->getConnection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION)}
</section>
</footer>
{script "js/node_modules/jquery/dist/jquery.min.js"} {script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"} {script "js/node_modules/@atlassian/aui/dist/aui/aui-prototyping.js"}
<script>AJS.tabs.setup();</script> <script>AJS.tabs.setup();</script>
<script>
(function() {
function markActiveNavItems() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.aui-nav a');
navLinks.forEach(link => {
const href = link.getAttribute('href');
if (currentPath === href ||
(href !== '/admin' && currentPath.startsWith(href)) ||
(href === '/admin' && currentPath === '/admin')) {
link.parentElement.classList.add('aui-nav-selected');
let parentGroup = link.closest('.aui-sidebar-group');
while (parentGroup) {
parentGroup.classList.add('aui-nav-child-selected');
parentGroup = parentGroup.parentElement.closest('.aui-sidebar-group');
}
}
});
}
document.addEventListener('DOMContentLoaded', markActiveNavItems);
})();
</script>
{ifset scripts} {ifset scripts}
{include scripts} {include scripts}
{/ifset} {/ifset}

View file

@ -37,7 +37,7 @@
<span class="nobold">{_"2fa_code_2"}: </span> <span class="nobold">{_"2fa_code_2"}: </span>
</td> </td>
<td class="regform-right"> <td class="regform-right">
<input id="password" type="password" name="password" required /> <input id="password" type="number" autocomplete="off" name="password" required />
</td> </td>
</tr> </tr>
{/if} {/if}

View file

@ -25,7 +25,7 @@
<span class="nobold">{_code}: </span> <span class="nobold">{_code}: </span>
</td> </td>
<td> <td>
<input type="text" name="code" autocomplete="off" required autofocus /> <input type="number" name="code" autocomplete="off" required autofocus />
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -67,7 +67,7 @@
}, },
success: (response) => { success: (response) => {
if (response?.reports?.length != _content) { if (response?.reports?.length != _content) {
NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)"); // NewNotification("Обратите внимание", "В списке появились новые жалобы. Работа ждёт :)");
} }
if (response.reports.length > 0) { if (response.reports.length > 0) {

View file

@ -72,7 +72,7 @@
<span class="nobold">{_"2fa_code"}</span> <span class="nobold">{_"2fa_code"}</span>
</td> </td>
<td> <td>
<input type="text" name="email_change_code" style="width: 100%;" /> <input type="number" autocomplete="off" name="email_change_code" style="width: 100%;" />
</td> </td>
</tr> </tr>
<tr> <tr>
@ -161,7 +161,7 @@
<span class="nobold">{_"2fa_code"}</span> <span class="nobold">{_"2fa_code"}</span>
</td> </td>
<td> <td>
<input type="text" name="password_change_code" style="width: 100%;" /> <input type="number" autocomplete="off" name="password_change_code" style="width: 100%;" />
</td> </td>
</tr> </tr>
<tr> <tr>

View file

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Util;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\RowModel;
use Chandler\Patterns\TSimpleSingleton;
class EventRateLimiter
{
use TSimpleSingleton;
private $config;
public function __construct()
{
$this->config = OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"];
}
public function tryToLimit(?User $user, string $event_type, bool $is_update = true): bool
{
/*
Checks count of actions for last x seconds
Uses OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["eventsLimit"]
This check should be peformed only after checking other conditions cuz by default it increments counter
Returns:
true limit has exceed and the action must be restricted
false the action can be performed
Also returns "true" if this option is disabled
*/
$isEnabled = $this->config['enable'];
$isIgnoreForAdmins = $this->config['ignoreForAdmins'];
$restrictionTime = $this->config['restrictionTime'];
$eventsList = $this->config['list'];
if (!$isEnabled) {
return false;
}
if ($isIgnoreForAdmins && $user->isAdmin()) {
return false;
}
$eventsStats = $user->getEventCounters($eventsList);
$limitForThatEvent = $eventsList[$event_type];
$counters = $eventsStats["counters"];
$refresh_time = $eventsStats["refresh_time"];
$is_restrict_over = $refresh_time < (time() - $restrictionTime);
$event_counter = $counters[$event_type];
if ($refresh_time && $is_restrict_over) {
$user->resetEvents($eventsList);
return false;
}
$is_limit_exceed = $event_counter >= $limitForThatEvent;
if (!$is_limit_exceed && $is_update) {
$this->incrementEvent($counters, $event_type, $user);
}
return $is_limit_exceed;
}
public function incrementEvent(array $old_values, string $event_type, User $initiator): bool
{
/*
Updates counter for user
*/
$isEnabled = $this->config['enable'];
$eventsList = $this->config['list'];
if (!$isEnabled) {
return false;
}
$old_values[$event_type] += 1;
$initiator->stateEvents($old_values);
$initiator->save();
return true;
}
}

View file

@ -407,6 +407,8 @@ routes:
handler: "About->robotsTxt" handler: "About->robotsTxt"
- url: "/humans.txt" - url: "/humans.txt"
handler: "About->humansTxt" handler: "About->humansTxt"
- url: "/.well-known/assetlinks.json"
handler: "About->AssetLinksJSON"
- url: "/dev" - url: "/dev"
handler: "About->dev" handler: "About->dev"
- url: "/iapi/getPhotosFromPost/{num}_{num}" - url: "/iapi/getPhotosFromPost/{num}_{num}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -3,4 +3,6 @@
<Client tag="vk4me" name="VK4ME" url="http://vk4me.crx.moe/" img="/assets/packages/static/openvk/img/app_icons/vk4me.png" /> <Client tag="vk4me" name="VK4ME" url="http://vk4me.crx.moe/" img="/assets/packages/static/openvk/img/app_icons/vk4me.png" />
<Client tag="openvk_legacy_android" name="OpenVK Legacy" url="https://f-droid.org/packages/uk.openvk.android.legacy/" img="/assets/packages/static/openvk/img/app_icons/openvk_legacy.png" /> <Client tag="openvk_legacy_android" name="OpenVK Legacy" url="https://f-droid.org/packages/uk.openvk.android.legacy/" img="/assets/packages/static/openvk/img/app_icons/openvk_legacy.png" />
<Client tag="openvk_refresh_android" name="OpenVK Refresh" url="https://github.com/openvk/mobile-android-refresh" img="/assets/packages/static/openvk/img/app_icons/openvk_refresh.png" /> <Client tag="openvk_refresh_android" name="OpenVK Refresh" url="https://github.com/openvk/mobile-android-refresh" img="/assets/packages/static/openvk/img/app_icons/openvk_refresh.png" />
</Clients> <Client tag="openvk_native" name="OpenVK Native" url="https://ovk.to/club9628" img="/assets/packages/static/openvk/img/app_icons/openvk_native.png" />
<Client tag="openvk_native_ios" name="OpenVK Native" url="https://ovk.to/club9628" img="/assets/packages/static/openvk/img/app_icons/openvk_native.png" />
</Clients>

View file

@ -0,0 +1,3 @@
ALTER TABLE `profiles`
ADD `events_counters` VARCHAR(299) NULL DEFAULT NULL AFTER `audio_broadcast_enabled`,
ADD `events_refresh_time` BIGINT(20) UNSIGNED NULL DEFAULT NULL AFTER `events_counters`;

View file

@ -1653,6 +1653,8 @@
"error_geolocation" = "Error while trying to pin geolocation"; "error_geolocation" = "Error while trying to pin geolocation";
"error_no_geotag" = "There is no geo-tag pinned in this post"; "error_no_geotag" = "There is no geo-tag pinned in this post";
"limit_exceed_exception" = "You're doing this action too often. Try again later.";
/* Admin actions */ /* Admin actions */
"login_as" = "Login as $1"; "login_as" = "Login as $1";

View file

@ -1458,7 +1458,7 @@
"error_access_denied_short" = "Ошибка доступа"; "error_access_denied_short" = "Ошибка доступа";
"error_access_denied" = "У вас недостаточно прав, чтобы редактировать этот ресурс"; "error_access_denied" = "У вас недостаточно прав, чтобы редактировать этот ресурс";
"success" = "Успешно"; "success" = "Успешно";
"comment_will_not_appear" = "Этот комментарий больше не будет показыватся."; "comment_will_not_appear" = "Этот комментарий больше не будет показываться.";
"error_when_gifting" = "Не удалось подарить"; "error_when_gifting" = "Не удалось подарить";
"error_user_not_exists" = "Пользователь или набор не существуют."; "error_user_not_exists" = "Пользователь или набор не существуют.";
@ -1557,6 +1557,8 @@
"error_geolocation" = "Ошибка при прикреплении геометки"; "error_geolocation" = "Ошибка при прикреплении геометки";
"error_no_geotag" = "У поста не указана гео-метка"; "error_no_geotag" = "У поста не указана гео-метка";
"limit_exceed_exception" = "Вы совершаете это действие слишком часто. Повторите позже.";
/* Admin actions */ /* Admin actions */
"login_as" = "Войти как $1"; "login_as" = "Войти как $1";

View file

@ -41,6 +41,16 @@ openvk:
maxViolations: 50 maxViolations: 50
maxViolationsAge: 120 maxViolationsAge: 120
autoban: true autoban: true
eventsLimit:
enable: true
ignoreForAdmins: true
restrictionTime: 86400
list:
groups.create: 5
groups.sub: 50
friends.outgoing_sub: 25
wall.post: 5000
gifts.send: 30
blacklists: blacklists:
limit: 100 limit: 100
applyToAdmins: true applyToAdmins: true

View file

@ -1,321 +1,378 @@
body { body {
background: url("/themepack/openvk_modern/0.0.1.0/resource/1.png") repeat-x fixed; background: url("/themepack/openvk_modern/0.0.1.0/resource/1.png") repeat-x
} fixed;
}
.page_header {
position: fixed; .page_header {
height: 42px; position: fixed;
background: #3C3C3C; height: 42px;
z-index: 199; background: #3c3c3c;
} z-index: 199;
}
.home_button {
background: #3C3C3C url("/themepack/openvk_modern/0.0.1.0/resource/2.png") no-repeat; .home_button {
background-size: 80%; background: #3c3c3c url("/themepack/openvk_modern/0.0.1.0/resource/2.png")
background-position-y: 0px; no-repeat;
background-position-x: 1px; background-size: 80%;
} background-position-y: 0px;
background-position-x: 1px;
.home_button_custom { }
background: #3C3C3C url("/themepack/openvk_modern/0.0.1.0/resource/4.png") no-repeat;
background-size: 80%; .home_button_custom {
background-position-y: 0px; background: #3c3c3c url("/themepack/openvk_modern/0.0.1.0/resource/4.png")
background-position-x: 1px; no-repeat;
text-shadow: none; background-size: 80%;
} background-position-y: 0px;
background-position-x: 1px;
.header_navigation .link, .header_navigation .header_divider_stick { width: 145px !important;
background: unset !important; text-shadow: none;
} }
.header_navigation .link a:hover { .header_navigation .link,
text-decoration: none; .header_navigation .header_divider_stick {
} background: unset !important;
}
.sidebar {
margin-top: 47px; .header_navigation .link a:hover {
position: fixed; text-decoration: none;
} }
.page_body { .header_navigation #search_box .search_box_button {
margin-top: 42px; border: solid 1px #606060;
} box-shadow: unset;
}
.toTop {
margin-top: 42px; .header_navigation #search_box .search_box_button:active {
} background-color: #606060;
box-shadow: unset;
.content_title_expanded { }
cursor: pointer;
background-image: unset !important; .sidebar {
padding: 3px 10px; margin-top: 47px;
border-top: #e6e6e6 solid 1px; position: fixed;
} background-color: #fff;
z-index: 199;
.content_title_unexpanded { }
background-image: unset !important;
padding: 3px 10px; .page_body {
border-top: #eee solid 1px; margin-top: 42px;
} }
.content_subtitle { .toTop {
border-top: #F0F0F0 solid 1px; margin-top: 42px;
border-bottom: 1px solid #F0F0F0; }
}
.content_title_expanded {
.user-alert { cursor: pointer;
border: 1px solid #f3ddbd; background-image: unset !important;
} padding: 3px 10px;
border-top: #e6e6e6 solid 1px;
.msg { }
border: 1pt solid #e6f2f3;
} .content_title_unexpanded {
background-image: unset !important;
.msg.msg_succ { padding: 3px 10px;
border-color: #ddf3d7; border-top: #eee solid 1px;
} }
.msg.msg_err { .content_subtitle {
border-color: #f5e9ec; border-top: #f0f0f0 solid 1px;
} border-bottom: 1px solid #f0f0f0;
}
.navigation .link:hover {
border-top: 1px solid #E4E4E4; .user-alert {
} border: 1px solid #f3ddbd;
}
#profile_link, .profile_link {
border-bottom: 1px solid transparent; .msg {
} border: 1pt solid #e6f2f3;
}
.completeness-gauge-gold {
border: 1px solid #f6ebbb; .msg.msg_succ {
} border-color: #ddf3d7;
}
.post-author {
border-top: #fff solid 1px; .msg.msg_err {
border-bottom: #fff solid 1px; border-color: #f5e9ec;
background-color: #fff; }
padding: 0px 5px 3px;
} .navigation .link:hover {
border-top: 1px solid #e4e4e4;
.post-author .date { }
color: gray;
} #profile_link,
.profile_link {
.page_yellowheader { border-bottom: 1px solid transparent;
background: #E2E2E2; }
border-right: solid 1px #E2E2E2;
border-left: solid 1px #E2E2E2; .completeness-gauge {
border-bottom: solid 1px #E2E2E2; width: 100%;
} border: unset;
border-top: unset;
.page_yellowheader span { }
color: #BBBBBB;
} .post-author {
border-top: #fff solid 1px;
.page_yellowheader a { border-bottom: #fff solid 1px;
color: #5C5C5C; background-color: #fff;
} padding: 0px 5px 3px;
}
.page-wrap {
border-bottom: solid 1px #fff; .post-author .date {
border-left: solid 1px #fff; color: gray;
border-right: solid 1px #fff; }
}
.page_yellowheader {
.page_wrap { background: #e2e2e2;
border-bottom: solid 1px #fff; border-right: solid 1px #e2e2e2;
border-left: solid 1px #fff; border-left: solid 1px #e2e2e2;
border-right: solid 1px #fff; border-bottom: solid 1px #e2e2e2;
} }
#wrapHI { .page_yellowheader span {
border-right: solid 1px #E2E2E2; color: #bbbbbb;
border-left: solid 1px #E2E2E2; }
}
.page_yellowheader a {
.left_small_block { color: #5c5c5c;
border-right: 1px #fff solid; }
}
.page-wrap {
.menu_divider { border-bottom: solid 1px #fff;
background: #E5E5E5; border-left: solid 1px #fff;
} border-right: solid 1px #fff;
}
.postFeedWrapper {
border-bottom: 1px solid rgb(240,240,240); .page_wrap {
} border-bottom: solid 1px #fff;
border-left: solid 1px #fff;
.container_gray { border-right: solid 1px #fff;
border-top: #EBEBEB solid 1px; }
}
#wrapHI {
.container_gray .content { border-right: solid 1px #e2e2e2;
border: #E5E5E5 solid 1px; border-left: solid 1px #e2e2e2;
} }
.accent-box { .left_small_block {
border: 1px solid white; border-right: 1px #fff solid;
} }
input[type="text"], input[type="password"], input[type~="text"], .menu_divider {
input[type~="password"], input[type="email"], input[type="phone"], background: #e5e5e5;
input[type~="email"], input[type~="phone"], input[type="date"], }
input[type~="date"], input[type="search"], input[type~="search"],
textarea, select { .postFeedWrapper {
border: 1px solid #E5E5E5; border-bottom: 1px solid rgb(240, 240, 240);
} }
input[type=checkbox] { .container_gray {
background-image: url("/themepack/openvk_modern/0.0.1.0/resource/6.png") border-top: #ebebeb solid 1px;
} }
ul { .container_gray .content {
list-style: none; border: #e5e5e5 solid 1px;
list-style-type: disc; }
}
.accent-box {
.mb_tab#active div { border: 1px solid white;
border: 2px solid #898989; }
}
input[type="text"],
.summaryBar { input[type="password"],
border-bottom: #fff solid 1px; input[type~="text"],
} input[type~="password"],
input[type="email"],
.page_footer .link:hover { input[type="phone"],
border-top: 0px; input[type~="email"],
} input[type~="phone"],
input[type="date"],
.ovk-video > .preview { input[type~="date"],
border: #fff; input[type="search"],
} input[type~="search"],
textarea,
.crp-list { select {
border-top: 1px solid #fff; border: 1px solid #e5e5e5;
width: 629px; }
}
input[type="checkbox"] {
.crp-entry:first-of-type { background-image: url("/themepack/openvk_modern/0.0.1.0/resource/6.png");
border-color: #E5E5E5; }
}
ul {
.crp-entry { list-style: none;
width: 593px; list-style-type: disc;
border-color: #E5E5E5; }
}
.like_tooltip_wrapper .like_tooltip_head {
#faqhead { background: #515151;
border: 1px solid #FBF3C3; box-shadow: unset;
} border: solid 1px #515151;
}
#faqcontent {
border: 1px solid #FAFAFA; .like_tooltip_wrapper .like_tooltip_body {
} border: 1px solid #515151;
}
.ovk-diag {
border: none; .mb_tab#active div {
border-radius: 2px; border: 2px solid #898989;
} }
.ovk-diag-cont { .summaryBar {
border-radius: 2px; border-bottom: #fff solid 1px;
} }
.ovk-diag-head { .page_footer .link:hover {
border-bottom: 1px solid #757575; border-top: 0px;
border-top-left-radius: 2px; }
border-top-right-radius: 2px;
} .ovk-video > .preview {
border: #fff;
.ovk-diag-action { }
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; .crp-list {
} border-top: 1px solid #fff;
width: 629px;
#votesBalance { }
border-bottom: none;
} .crp-entry:first-of-type {
border-color: #e5e5e5;
.floating_sidebar,.floating_sidebar.show { }
display:none
} .crp-entry {
width: 593px;
#backdrop:before { border-color: #e5e5e5;
content:""; }
display:block;
position:fixed; #faqhead {
top:0; border: 1px solid #fbf3c3;
left:0; }
height:42px;
width:100%; #faqcontent {
background-color:#3c3c3c border: 1px solid #fafafa;
} }
.search_box_button { .ovk-diag {
box-shadow: none; border: none;
} border-radius: 2px;
}
.search_box_button:active {
box-shadow: none; .ovk-diag-cont {
} border-radius: 2px;
}
.verticalGrayTabs #used {
background: #3c3c3c !important; .ovk-diag-head {
border: 1px solid #3c3c3c; border-bottom: 1px solid #757575;
} border-top-left-radius: 2px;
border-top-right-radius: 2px;
.verticalGrayTabs #used a { }
color: white;
} .ovk-diag-action {
border-bottom-left-radius: 2px;
.search_option_name { border-bottom-right-radius: 2px;
background-color: #a4a4a4; }
border-bottom: unset;
} #votesBalance,
#news {
.verticalGrayTabsWrapper { border-bottom: unset;
border-top: unset; }
}
.floating_sidebar,
.sugglist { .floating_sidebar.show {
border-top: unset; display: none;
border-bottom: 1px solid gray; }
}
#backdrop:before {
.musicIcon { content: "";
filter: contrast(200%) !important; display: block;
} position: fixed;
top: 0;
.audioEntry .playerButton .playIcon { left: 0;
filter: contrast(2) !important; height: 42px;
} width: 100%;
background-color: #3c3c3c;
.audioEmbed .track > .selectableTrack, .bigPlayer .selectableTrack { }
border-top: #404040 1px solid !important;
} .search_box_button {
box-shadow: none;
.bigPlayer .paddingLayer .slider, .audioEmbed .track .slider { }
background: #3c3c3c !important;
} .search_box_button:active {
box-shadow: none;
.audioEntry.nowPlaying { }
background: #4b4b4b !important;
} .verticalGrayTabs #used {
background: #3c3c3c !important;
.audioEntry.nowPlaying:hover { border: 1px solid #3c3c3c;
background: #373737 !important; }
}
.verticalGrayTabs #used a {
.musicIcon.pressed { color: white;
filter: brightness(150%) !important; }
}
.search_option_name {
.musicIcon.lagged { background-color: #a4a4a4;
opacity: 50%; border-bottom: unset;
} }
.bigPlayer { .verticalGrayTabsWrapper {
position: sticky; border-top: unset;
top: 42px; border-left: unset;
} }
.sugglist {
border-top: unset;
border-bottom: 1px solid gray;
}
.musicIcon {
filter: contrast(200%) !important;
}
.audioEntry .playerButton .playIcon {
filter: contrast(2) !important;
}
.audioEmbed .track > .selectableTrack,
.bigPlayer .selectableTrack {
border-top: #404040 1px solid !important;
}
.bigPlayer .paddingLayer .slider,
.audioEmbed .track .slider {
background: #3c3c3c !important;
}
.audioEntry.nowPlaying {
background: #4b4b4b !important;
}
.audioEntry.nowPlaying:hover {
background: #373737 !important;
}
.musicIcon.pressed {
filter: brightness(150%) !important;
}
.musicIcon.lagged {
opacity: 50%;
}
.bigPlayer {
position: sticky;
top: 42px;
box-shadow: unset;
}
#audio_upload {
border: 1px solid #ccc;
}
#wallAttachmentMenu {
box-shadow: unset;
}
#backdropEditor {
border: unset;
}