diff --git a/Web/Models/Entities/Name.php b/Web/Models/Entities/Name.php new file mode 100644 index 000000000..d30c20f64 --- /dev/null +++ b/Web/Models/Entities/Name.php @@ -0,0 +1,43 @@ +getRecord()->id; + } + + function getCreationDate(): DateTime + { + return new DateTime($this->getRecord()->created); + } + + function getFirstName(): ?string + { + return $this->getRecord()->new_fn; + } + + function getLastName(): ?string + { + return $this->getRecord()->new_ln; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->author); + } + + function getStatus(): int + { + return $this->getRecord()->state; + } +} diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index f1045f624..91639518f 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -5,7 +5,7 @@ use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Gifts, Notifications, Names}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; @@ -1038,6 +1038,16 @@ function canUnbanThemself(): bool return true; } + + function getNamesRequests(int $status = 0, ?bool $actual = false): \Traversable + { + return (new Names)->getByUser($this->getId(), $status, $actual); + } + + function hasNamesRequests(int $status = 0, ?bool $actual = false): bool + { + return sizeof(iterator_to_array($this->getNamesRequests($status, $actual))) > 0; + } use Traits\TSubscribable; } diff --git a/Web/Models/Repositories/Names.php b/Web/Models/Repositories/Names.php new file mode 100644 index 000000000..570f5d720 --- /dev/null +++ b/Web/Models/Repositories/Names.php @@ -0,0 +1,50 @@ +context = DB::i()->getContext(); + $this->names = $this->context->table("names"); + $this->users = $this->context->table("profiles"); + } + + private function toName(?ActiveRow $ar): ?Name + { + return is_null($ar) ? NULL : new Name($ar); + } + + function get(int $id): ?Name + { + return $this->toName($this->names->get($id)); + } + + function getCount(int $status = 0): int + { + return sizeof(DB::i()->getContext()->table("names")->where("state", $status)); + } + + function getList(int $page = 1, int $status = 0): \Traversable + { + foreach($this->names->where("state", $status)->order("created ASC")->page($page, 5) as $name) + yield $this->toName($name); + } + + function getByUser(int $uid, int $status = 0, ?bool $actual = true): \Traversable + { + $filter = ["author" => $uid, "state" => $status]; + $actual && $filter[] = "created >= " . (time() - 259200); + + foreach($this->names->where($filter) as $name) + yield $this->toName($name); + } +} diff --git a/Web/Presenters/NamesPresenter.php b/Web/Presenters/NamesPresenter.php new file mode 100644 index 000000000..f5c39fddf --- /dev/null +++ b/Web/Presenters/NamesPresenter.php @@ -0,0 +1,73 @@ +names = $names; + } + + function renderList(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); + + $this->template->mode = $this->queryParam("act") ?? "new"; + $this->template->mode_status = 0; + + if ($this->template->mode == "accepted") + $this->template->mode_status = 1; + else if ($this->template->mode == "rejected") + $this->template->mode_status = 2; + + $this->template->page = (int) $this->queryParam("p") ?: 1; + + $this->template->iterator = $this->names->getList($this->template->page, $this->template->mode_status); + $this->template->names = iterator_to_array($this->template->iterator); + + $this->template->count = (clone $this->names)->getCount($this->template->mode_status); + } + + function renderAction(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertPermission("openvk\Web\Models\Entities\TicketReply", "write", 0); + + $act = $this->queryParam("act"); + + if(!$act) + $this->flashFail("err", tr("error"), tr("forbidden")); + + $name = $this->names->get($id); + + if(!$name) + $this->flashFail("err", tr("error"), "Заявка #$id не найдена."); + + $user = $name->getUser(); + + if($act == "accept") { + $user->setFirst_name($name->getFirstName()); + $user->setLast_name($name->getLastName()); + $user->save(); + + $name->setState(1); + $name->save(); + + $this->flashFail("success", "Успех", "Заявка #$id была принята."); + } elseif($act == "reject") { + $name->setState(2); + $name->save(); + + $this->flashFail("success", "Успех", "Заявка #$id была отклонена."); + } + + $this->flashFail("err", tr("error"), tr("forbidden")); + } +} diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 5b7908a8a..6567ad797 100755 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -7,7 +7,7 @@ use Latte\Engine as TemplatingEngine; use openvk\Web\Models\Entities\IP; use openvk\Web\Themes\Themepacks; -use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets}; +use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Names}; use WhichBrowser; abstract class OpenVKPresenter extends SimplePresenter @@ -258,8 +258,10 @@ function onStartup(): void } $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); - if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) + if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) { $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); + $this->template->namesNotModeratedCount = (new Names)->getCount(0); + } } header("X-OpenVK-User-Validated: $userValidated"); diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 48ebb660f..2f6cb09cd 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Util\Sms; use openvk\Web\Themes\Themepacks; -use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; +use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification, Name}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; use openvk\Web\Models\Exceptions\InvalidUserNameException; @@ -142,8 +142,19 @@ function renderEdit(): void if($_GET['act'] === "main" || $_GET['act'] == NULL) { try { - $user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); - $user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name")); + if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["namesModeration"]) { + $user->setFirst_Name(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); + $user->setLast_Name(empty($this->postParam("last_name")) ? "" : $this->postParam("last_name")); + } else { + if(!$user->hasNamesRequests(0, true) AND !$user->hasNamesRequests(2, true)) { + $name = new Name; + $name->setNew_fn(empty($this->postParam("first_name")) ? $user->getFirstName() : $this->postParam("first_name")); + $name->setNew_ln(empty($this->postParam("last_name")) ? $user->getFirstName() : $this->postParam("last_name")); + $name->setAuthor($user->getId()); + $name->setCreated(time()); + $name->save(); + } + } } catch(InvalidUserNameException $ex) { $this->flashFail("err", tr("error"), tr("invalid_real_name")); } diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index e717acfea..b4ecc1ca1 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -186,6 +186,11 @@ ({$helpdeskTicketNotAnsweredCount}) {/if} + Имена + {if $namesNotModeratedCount > 0} + ({$namesNotModeratedCount}) + {/if} + {strpos($menuItem["name"], "@") === 0 ? tr(substr($menuItem["name"], 1)) : $menuItem["name"]} diff --git a/Web/Presenters/templates/Names/List.xml b/Web/Presenters/templates/Names/List.xml new file mode 100644 index 000000000..2551b03a1 --- /dev/null +++ b/Web/Presenters/templates/Names/List.xml @@ -0,0 +1,121 @@ +{extends "../@layout.xml"} + +{block header} + Администрирование +{/block} + +{block content} + {var $isNew = $mode === 'new'} + {var $isAccepted = $mode === 'accepted'} + {var $isRejected = $mode === 'rejected'} + + {var $amount = sizeof($names)} + +
+
+ {_names_new} +
+
+ {_names_approved} +
+
+ {_names_rejected} +
+
+
+
+ {presenter "openvk!Support->knowledgeBaseArticle", "names"} +
+ + {if $count < 1} + {include "../components/nothing.xml"} + {else} +

{tr("names_showed", $amount, $count)}

+
+
+ + + + + + + +
+ + + +
{$name->getCreationDate()}
+
+ + + + + + + + + + + + +
+ {$name->getUser()->getFirstName()} {$name->getUser()->getLastName()} +
+ + + + + + + +
+ {$name->getFirstName()} {$name->getLastName()} +
+
+ {_city}: {$name->getUser()->getCity() ?? "Не указан"} +
+
+ +
+ {_approved} +
+
+ {_rejected} +
+
+
+
+
+
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $count, + "amount" => $amount, + "perPage" => 5, + "atBottom" => true, + ]} +
+ {/if} +{/block} diff --git a/Web/Presenters/templates/User/Edit.xml b/Web/Presenters/templates/User/Edit.xml index 96152132e..d711bbe5f 100644 --- a/Web/Presenters/templates/User/Edit.xml +++ b/Web/Presenters/templates/User/Edit.xml @@ -33,8 +33,17 @@
{if $isMain} - +

{_main_information}

+
+ {_name_change_request}
+ {_name_change_request_pending} +
+
+ {_name_change_request}
+ {_name_change_request_rejected} +
+
diff --git a/Web/di.yml b/Web/di.yml index ec8678093..21c3e7e1c 100644 --- a/Web/di.yml +++ b/Web/di.yml @@ -43,6 +43,9 @@ services: - openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\Applications - openvk\Web\Models\Repositories\ContentSearchRepository + - openvk\Web\Presenters\NamesPresenter + - openvk\Web\Models\Repositories\Names + - openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\Aliases - openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Presenters\MaintenancePresenter diff --git a/Web/routes.yml b/Web/routes.yml index d6f63a114..e0c3bf150 100644 --- a/Web/routes.yml +++ b/Web/routes.yml @@ -319,6 +319,10 @@ routes: handler: "VKAPI->tokenLogin" - url: "/admin/sandbox" handler: "About->sandbox" + - url: "/names_admin.php" + handler: "Names->list" + - url: "/name{num}" + handler: "Names->action" - url: "/internal/wall{num}" handler: "Wall->wallEmbedded" - url: "/robots.txt" diff --git a/data/knowledgebase/names.ru.md b/data/knowledgebase/names.ru.md new file mode 100644 index 000000000..4ec545e21 --- /dev/null +++ b/data/knowledgebase/names.ru.md @@ -0,0 +1,6 @@ +* Относитесь ответственно к каждой заявке. +* Не одобряйте изменения кириллицы на транслит для русскоязычных. +* Однако изменение ерунды на настоящее имя, записанное латиницей — одобряйте. +* Смену латиницы на кириллицу одобряйте. +* Смену нормального имени на уменьшительное не одобряйте. +* Если фамилия не была указана, а теперь указана, одобряйте всегда. diff --git a/install/sqls/00032-names-admin.sql b/install/sqls/00032-names-admin.sql new file mode 100644 index 000000000..567d60d20 --- /dev/null +++ b/install/sqls/00032-names-admin.sql @@ -0,0 +1,15 @@ +CREATE TABLE `names` ( + `id` bigint UNSIGNED NOT NULL, + `author` bigint UNSIGNED NOT NULL, + `new_fn` varchar(50) NOT NULL, + `new_ln` varchar(50) NOT NULL, + `created` bigint UNSIGNED NOT NULL, + `state` int NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +ALTER TABLE `names` + ADD PRIMARY KEY (`id`); + +ALTER TABLE `names` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT; +COMMIT; diff --git a/locales/en.strings b/locales/en.strings index 240dd5d06..0803a4ca9 100644 --- a/locales/en.strings +++ b/locales/en.strings @@ -1148,6 +1148,21 @@ "url_is_banned_title" = "Link to a suspicious site"; "url_is_banned_proceed" = "Follow the link"; +/* Names */ + +"names_new" = "New"; +"names_approved" = "Approved"; +"names_rejected" = "Rejected"; +"names_showed" = "$1 name change requests are shown (total $2)"; +"approve" = "Approve"; +"reject" = "Reject"; +"approved" = "Approved"; +"rejected" = "Rejected"; +"name_change_request" = "Name change"; +"name_change_request_pending" = "Your application for a name change is under consideration by the moderators."; +"name_change_request_rejected" = "Your request to change your name was rejected by the moderators. You can submit a new one when this message disappears."; + + /* Maintenance */ "global_maintenance" = "Undergoing maintenance"; diff --git a/locales/ru.strings b/locales/ru.strings index 6a7b8f46e..20bca834b 100644 --- a/locales/ru.strings +++ b/locales/ru.strings @@ -1209,6 +1209,20 @@ "url_is_banned_title" = "Ссылка на подозрительный сайт"; "url_is_banned_proceed" = "Перейти по ссылке"; +/* Names */ + +"names_new" = "Новые"; +"names_approved" = "Принятые"; +"names_rejected" = "Отклонённые"; +"names_showed" = "Показано $1 заявок на смену имени (всего $2)"; +"approve" = "Одобрить"; +"reject" = "Отклонить"; +"approved" = "Одобрена"; +"rejected" = "Отклонена"; +"name_change_request" = "Смена имени"; +"name_change_request_pending" = "Ваша заявка на смену имени находится на рассмотрении модераторами."; +"name_change_request_rejected" = "Ваша заявка на смену имени была отклонена модераторами. Вы можете подать новую, когда это сообщение исчезнет."; + /* Maintenance */ "global_maintenance" = "Технические работы"; diff --git a/openvk-example.yml b/openvk-example.yml index 47eacd24e..a289a27dc 100644 --- a/openvk-example.yml +++ b/openvk-example.yml @@ -33,6 +33,7 @@ openvk: maxViolations: 50 maxViolationsAge: 120 autoban: true + namesModeration: true registration: enable: true reason: "" # reason for disabling registration