diff --git a/Web/Models/Entities/Application.php b/Web/Models/Entities/Application.php index 74569485..48975645 100644 --- a/Web/Models/Entities/Application.php +++ b/Web/Models/Entities/Application.php @@ -306,11 +306,14 @@ class Application extends RowModel function delete(bool $softly = true): void { if($softly) - throw new \UnexpectedValueException("Can't delete apps softly."); + throw new \UnexpectedValueException("Can't delete apps softly."); // why $cx = DatabaseConnection::i()->getContext(); $cx->table("app_users")->where("app", $this->getId())->delete(); parent::delete(false); } + + function getPublicationTime(): string + { return tr("recently"); } } \ No newline at end of file diff --git a/Web/Models/Entities/Ban.php b/Web/Models/Entities/Ban.php new file mode 100644 index 00000000..3962c6cb --- /dev/null +++ b/Web/Models/Entities/Ban.php @@ -0,0 +1,66 @@ +getRecord()->id; + } + + function getReason(): ?string + { + return $this->getRecord()->reason; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getInitiator(): ?User + { + return (new Users)->get($this->getRecord()->initiator); + } + + function getStartTime(): int + { + return $this->getRecord()->iat; + } + + function getEndTime(): int + { + return $this->getRecord()->exp; + } + + function getTime(): int + { + return $this->getRecord()->time; + } + + function isPermanent(): bool + { + return $this->getEndTime() === 0; + } + + function isRemovedManually(): bool + { + return (bool) $this->getRecord()->removed_manually; + } + + function isOver(): bool + { + return $this->isRemovedManually(); + } + + function whoRemoved(): ?User + { + return (new Users)->get($this->getRecord()->removed_by); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 06b2f49b..31485129 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -351,9 +351,21 @@ class Club extends RowModel } function getWebsite(): ?string - { - return $this->getRecord()->website; - } + { + return $this->getRecord()->website; + } + + function ban(string $reason): void + { + $this->setBlock_Reason($reason); + $this->save(); + } + + function unban(): void + { + $this->setBlock_Reason(null); + $this->save(); + } function getAlert(): ?string { diff --git a/Web/Models/Entities/NoSpamLog.php b/Web/Models/Entities/NoSpamLog.php new file mode 100644 index 00000000..48d723c9 --- /dev/null +++ b/Web/Models/Entities/NoSpamLog.php @@ -0,0 +1,71 @@ +getRecord()->id; + } + + function getUser(): ?User + { + return (new Users)->get($this->getRecord()->user); + } + + function getModel(): string + { + return $this->getRecord()->model; + } + + function getRegex(): ?string + { + return $this->getRecord()->regex; + } + + function getRequest(): ?string + { + return $this->getRecord()->request; + } + + function getCount(): int + { + return $this->getRecord()->count; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->time); + } + + function getItems(): ?array + { + return explode(",", $this->getRecord()->items); + } + + function getTypeRaw(): int + { + return $this->getRecord()->ban_type; + } + + function getType(): string + { + switch ($this->getTypeRaw()) { + case 1: return "О"; + case 2: return "Б"; + case 3: return "ОБ"; + default: return (string) $this->getTypeRaw(); + } + } + + function isRollbacked(): bool + { + return !is_null($this->getRecord()->rollback); + } +} diff --git a/Web/Models/Entities/Postable.php b/Web/Models/Entities/Postable.php index b743793f..23981cc1 100644 --- a/Web/Models/Entities/Postable.php +++ b/Web/Models/Entities/Postable.php @@ -34,7 +34,8 @@ abstract class Postable extends Attachable $oid = (int) $this->getRecord()->owner; if(!$real && $this->isAnonymous()) $oid = OPENVK_ROOT_CONF["openvk"]["preferences"]["wall"]["anonymousPosting"]["account"]; - + + $oid = abs($oid); if($oid > 0) return (new Users)->get($oid); else diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php new file mode 100644 index 00000000..4070952e --- /dev/null +++ b/Web/Models/Entities/Report.php @@ -0,0 +1,142 @@ +getRecord()->id; + } + + function getStatus(): int + { + return $this->getRecord()->status; + } + + function getContentType(): string + { + return $this->getRecord()->type; + } + + function getReason(): string + { + return $this->getRecord()->reason; + } + + function getTime(): DateTime + { + return new DateTime($this->getRecord()->date); + } + + function isDeleted(): bool + { + if ($this->getRecord()->deleted === 0) + { + return false; + } elseif ($this->getRecord()->deleted === 1) { + return true; + } + } + + function authorId(): int + { + return $this->getRecord()->user_id; + } + + function getUser(): User + { + return (new Users)->get((int) $this->getRecord()->user_id); + } + + function getContentId(): int + { + return (int) $this->getRecord()->target_id; + } + + function getContentObject() + { + if ($this->getContentType() == "post") return (new Posts)->get($this->getContentId()); + else if ($this->getContentType() == "photo") return (new Photos)->get($this->getContentId()); + else if ($this->getContentType() == "video") return (new Videos)->get($this->getContentId()); + else if ($this->getContentType() == "group") return (new Clubs)->get($this->getContentId()); + else if ($this->getContentType() == "comment") return (new Comments)->get($this->getContentId()); + else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); + else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); + else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else return null; + } + + function getAuthor(): RowModel + { + return (new Posts)->get($this->getContentId())->getOwner(); + } + + function getReportAuthor(): User + { + return (new Users)->get($this->getRecord()->user_id); + } + + function banUser($initiator) + { + $reason = $this->getContentType() !== "user" ? ("**content-" . $this->getContentType() . "-" . $this->getContentId() . "**") : ("Подозрительная активность"); + $this->getAuthor()->ban($reason, false, time() + $this->getAuthor()->getNewBanTime(), $initiator); + } + + function deleteContent() + { + if ($this->getContentType() !== "user") { + $pubTime = $this->getContentObject()->getPublicationTime(); + $name = $this->getContentObject()->getName(); + $this->getAuthor()->adminNotify("Ваш контент, который вы опубликовали $pubTime ($name) был удалён модераторами инстанса. За повторные или серьёзные нарушения вас могут заблокировать."); + $this->getContentObject()->delete($this->getContentType() !== "app"); + } + + $this->delete(); + } + + function getDuplicates(): \Traversable + { + return (new Reports)->getDuplicates($this->getContentType(), $this->getContentId(), $this->getId()); + } + + function getDuplicatesCount(): int + { + return count(iterator_to_array($this->getDuplicates())); + } + + function hasDuplicates(): bool + { + return $this->getDuplicatesCount() > 0; + } + + function getContentName(): string + { + if (method_exists($this->getContentObject(), "getCanonicalName")) + return $this->getContentObject()->getCanonicalName(); + + return $this->getContentType() . " #" . $this->getContentId(); + } + + public function delete(bool $softly = true): void + { + if ($this->hasDuplicates()) { + foreach ($this->getDuplicates() as $duplicate) { + $duplicate->setDeleted(1); + $duplicate->save(); + } + } + + $this->setDeleted(1); + $this->save(); + } +} diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index db5cf055..e5b8da06 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -5,7 +5,7 @@ use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; -use openvk\Web\Models\Repositories\{Photos, Users, Clubs, Albums, Gifts, Notifications}; +use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; use Chandler\Database\DatabaseConnection; @@ -241,11 +241,60 @@ class User extends RowModel return $this->getRecord()->alert; } - function getBanReason(): ?string + function getTextForContentBan(string $type): string + { + switch ($type) { + case "post": return "за размещение от Вашего лица таких записей:"; + case "photo": return "за размещение от Вашего лица таких фотографий:"; + case "video": return "за размещение от Вашего лица таких видеозаписей:"; + case "group": return "за подозрительное вступление от Вашего лица в группу:"; + case "comment": return "за размещение от Вашего лица таких комментариев:"; + case "note": return "за размещение от Вашего лица таких заметок:"; + case "app": return "за создание от Вашего имени подозрительных приложений."; + default: return "за размещение от Вашего лица такого контента:"; + } + } + + function getRawBanReason(): ?string { return $this->getRecord()->block_reason; } + function getBanReason(?string $for = null) + { + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver()) return null; + + $reason = $ban->getReason(); + + preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate|user)-(\d+)\*\*$/', $reason, $matches); + if (sizeof($matches) === 3) { + $content_type = $matches[1]; $content_id = (int) $matches[2]; + if (in_array($content_type, ["noSpamTemplate", "user"])) { + $reason = "Подозрительная активность"; + } else { + if ($for !== "banned") { + $reason = "Подозрительная активность"; + } else { + $reason = [$this->getTextForContentBan($content_type), $content_type]; + switch ($content_type) { + case "post": $reason[] = (new Posts)->get($content_id); break; + case "photo": $reason[] = (new Photos)->get($content_id); break; + case "video": $reason[] = (new Videos)->get($content_id); break; + case "group": $reason[] = (new Clubs)->get($content_id); break; + case "comment": $reason[] = (new Comments)->get($content_id); break; + case "note": $reason[] = (new Notes)->get($content_id); break; + case "app": $reason[] = (new Applications)->get($content_id); break; + case "user": $reason[] = (new Users)->get($content_id); break; + default: $reason[] = null; + } + } + } + } + + return $reason; + } + function getBanInSupportReason(): ?string { return $this->getRecord()->block_in_support_reason; @@ -833,7 +882,7 @@ class User extends RowModel ]); } - function ban(string $reason, bool $deleteSubscriptions = true, ?int $unban_time = NULL): void + function ban(string $reason, bool $deleteSubscriptions = true, $unban_time = NULL, ?int $initiator = NULL): void { if($deleteSubscriptions) { $subs = DatabaseConnection::i()->getContext()->table("subscriptions"); @@ -846,8 +895,33 @@ class User extends RowModel $subs->delete(); } - $this->setBlock_Reason($reason); - $this->setUnblock_time($unban_time); + $iat = time(); + $ban = new Ban; + $ban->setUser($this->getId()); + $ban->setReason($reason); + $ban->setInitiator($initiator); + $ban->setIat($iat); + $ban->setExp($unban_time !== "permanent" ? $unban_time : 0); + $ban->setTime($unban_time === "permanent" ? 0 : ($unban_time ? ($unban_time - $iat) : 0)); + $ban->save(); + + $this->setBlock_Reason($ban->getId()); + // $this->setUnblock_time($unban_time); + $this->save(); + } + + function unban(int $removed_by): void + { + $ban = (new Bans)->get((int) $this->getRawBanReason()); + if (!$ban || $ban->isOver()) + return; + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($removed_by); + $ban->save(); + + $this->setBlock_Reason(NULL); + // $user->setUnblock_time(NULL); $this->save(); } @@ -1099,7 +1173,11 @@ class User extends RowModel function getUnbanTime(): ?string { - return !is_null($this->getRecord()->unblock_time) ? date('d.m.Y', $this->getRecord()->unblock_time) : NULL; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return null; + if ($this->canUnbanThemself()) return tr("today"); + + return date('d.m.Y', $ban->getEndTime()); } function canUnbanThemself(): bool @@ -1107,10 +1185,40 @@ class User extends RowModel if (!$this->isBanned()) return false; - if ($this->getRecord()->unblock_time > time() || $this->getRecord()->unblock_time == 0) - return false; + $ban = (new Bans)->get((int) $this->getRecord()->block_reason); + if (!$ban || $ban->isOver() || $ban->isPermanent()) return false; - return true; + return $ban->getEndTime() <= time() && !$ban->isPermanent(); + } + + function getNewBanTime() + { + $bans = iterator_to_array((new Bans)->getByUser($this->getid())); + if (!$bans || count($bans) === 0) + return 0; + + $last_ban = end($bans); + if (!$last_ban) return 0; + + if ($last_ban->isPermanent()) return "permanent"; + + $values = [0, 3600, 7200, 86400, 172800, 604800, 1209600, 3024000, 9072000]; + $response = 0; + $i = 0; + + foreach ($values as $value) { + $i++; + if ($last_ban->getTime() === 0 && $value === 0) continue; + if ($last_ban->getTime() < $value) { + $response = $value; + break; + } else if ($last_ban->getTime() >= $value) { + if ($i < count($values)) continue; + $response = "permanent"; + break; + } + } + return $response; } function toVkApiStruct(): object diff --git a/Web/Models/Repositories/Bans.php b/Web/Models/Repositories/Bans.php new file mode 100644 index 00000000..7123459d --- /dev/null +++ b/Web/Models/Repositories/Bans.php @@ -0,0 +1,33 @@ +context = DB::i()->getContext(); + $this->bans = $this->context->table("bans"); + } + + function toBan(?ActiveRow $ar): ?Ban + { + return is_null($ar) ? NULL : new Ban($ar); + } + + function get(int $id): ?Ban + { + return $this->toBan($this->bans->get($id)); + } + + function getByUser(int $user_id): \Traversable + { + foreach ($this->bans->where("user", $user_id) as $ban) + yield new Ban($ban); + } +} \ No newline at end of file diff --git a/Web/Models/Repositories/ChandlerUsers.php b/Web/Models/Repositories/ChandlerUsers.php index a827afac..510e5860 100644 --- a/Web/Models/Repositories/ChandlerUsers.php +++ b/Web/Models/Repositories/ChandlerUsers.php @@ -28,7 +28,8 @@ class ChandlerUsers function getById(string $UUID): ?ChandlerUser { - return new ChandlerUser($this->users->where("id", $UUID)->fetch()); + $user = $this->users->where("id", $UUID)->fetch(); + return $user ? new ChandlerUser($user) : NULL; } function getList(int $page = 1): \Traversable diff --git a/Web/Models/Repositories/NoSpamLogs.php b/Web/Models/Repositories/NoSpamLogs.php new file mode 100644 index 00000000..f8dd4980 --- /dev/null +++ b/Web/Models/Repositories/NoSpamLogs.php @@ -0,0 +1,34 @@ +context = DatabaseConnection::i()->getContext(); + $this->noSpamLogs = $this->context->table("noSpam_templates"); + } + + private function toNoSpamLog(?ActiveRow $ar): ?NoSpamLog + { + return is_null($ar) ? NULL : new NoSpamLog($ar); + } + + function get(int $id): ?NoSpamLog + { + return $this->toNoSpamLog($this->noSpamLogs->get($id)); + } + + function getList(array $filter = []): \Traversable + { + foreach ($this->noSpamLogs->where($filter)->order("`id` DESC") as $log) + yield new NoSpamLog($log); + } +} diff --git a/Web/Models/Repositories/Reports.php b/Web/Models/Repositories/Reports.php new file mode 100644 index 00000000..edce8980 --- /dev/null +++ b/Web/Models/Repositories/Reports.php @@ -0,0 +1,67 @@ +context = DatabaseConnection::i()->getContext(); + $this->reports = $this->context->table("reports"); + } + + private function toReport(?ActiveRow $ar): ?Report + { + return is_null($ar) ? NULL : new Report($ar); + } + + function getReports(int $state = 0, int $page = 1, ?string $type = NULL, ?bool $pagination = true): \Traversable + { + $filter = ["deleted" => 0]; + if ($type) $filter["type"] = $type; + + $reports = $this->reports->where($filter)->order("created DESC")->group("target_id, type"); + if ($pagination) + $reports = $reports->page($page, 15); + + foreach($reports as $t) + yield new Report($t); + } + + function getReportsCount(int $state = 0): int + { + return sizeof($this->reports->where(["deleted" => 0, "type" => $state])->group("target_id, type")); + } + + function get(int $id): ?Report + { + return $this->toReport($this->reports->get($id)); + } + + function getByContentId(int $id): ?Report + { + $post = $this->reports->where(["deleted" => 0, "content_id" => $id])->fetch(); + + if($post) + return new Report($post); + else + return null; + } + + function getDuplicates(string $type, int $target_id, ?int $orig = NULL, ?int $user_id = NULL): \Traversable + { + $filter = ["deleted" => 0, "type" => $type, "target_id" => $target_id]; + if ($orig) $filter[] = "id != $orig"; + if ($user_id) $filter["user_id"] = $user_id; + + foreach ($this->reports->where($filter) as $report) + yield new Report($report); + } + + use \Nette\SmartObject; +} diff --git a/Web/Models/Repositories/Users.php b/Web/Models/Repositories/Users.php index 6c165aa3..de0d341d 100644 --- a/Web/Models/Repositories/Users.php +++ b/Web/Models/Repositories/Users.php @@ -44,9 +44,9 @@ class Users return $alias->getUser(); } - function getByChandlerUser(ChandlerUser $user): ?User + function getByChandlerUser(?ChandlerUser $user): ?User { - return $this->toUser($this->users->where("user", $user->getId())->fetch()); + return $user ? $this->toUser($this->users->where("user", $user->getId())->fetch()) : NULL; } function find(string $query, array $pars = [], string $sort = "id DESC"): Util\EntityStream diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index bba2ef31..f5c40bcc 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -360,13 +360,19 @@ final class AdminPresenter extends OpenVKPresenter { $this->assertNoCSRF(); + if (str_contains($this->queryParam("reason"), "*")) + exit(json_encode([ "error" => "Incorrect reason" ])); + $unban_time = strtotime($this->queryParam("date")) ?: NULL; $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - - $user->ban($this->queryParam("reason"), true, $unban_time); + + if ($this->queryParam("incr")) + $unban_time = time() + $user->getNewBanTime(); + + $user->ban($this->queryParam("reason"), true, $unban_time, $this->user->identity->getId()); exit(json_encode([ "success" => true, "reason" => $this->queryParam("reason") ])); } @@ -377,9 +383,17 @@ final class AdminPresenter extends OpenVKPresenter $user = $this->users->get($id); if(!$user) exit(json_encode([ "error" => "User does not exist" ])); - + + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver()) + exit(json_encode([ "error" => "User is not banned" ])); + + $ban->setRemoved_Manually(true); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); + $user->setBlock_Reason(NULL); - $user->setUnblock_time(NULL); + // $user->setUnblock_time(NULL); $user->save(); exit(json_encode([ "success" => true ])); } @@ -465,6 +479,14 @@ final class AdminPresenter extends OpenVKPresenter $this->redirect("/admin/bannedLinks"); } + function renderBansHistory(int $user_id) :void + { + $user = (new Users)->get($user_id); + if (!$user) $this->notFound(); + + $this->template->bans = (new Bans)->getByUser($user_id); + } + function renderChandlerGroups(): void { $this->template->groups = (new ChandlerGroups)->getList(); diff --git a/Web/Presenters/AuthPresenter.php b/Web/Presenters/AuthPresenter.php index f569dd63..23b55dc9 100644 --- a/Web/Presenters/AuthPresenter.php +++ b/Web/Presenters/AuthPresenter.php @@ -1,7 +1,7 @@ flashFail("err", tr("error"), tr("forbidden")); $user = $this->users->get($this->user->id); + $ban = (new Bans)->get((int)$user->getRawBanReason()); + if (!$ban || $ban->isOver() || $ban->isPermanent()) + $this->flashFail("err", tr("error"), tr("forbidden")); + + $ban->setRemoved_Manually(2); + $ban->setRemoved_By($this->user->identity->getId()); + $ban->save(); $user->setBlock_Reason(NULL); - $user->setUnblock_Time(NULL); + // $user->setUnblock_Time(NULL); $user->save(); $this->flashFail("succ", tr("banned_unban_title"), tr("banned_unban_description")); diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 970b8f19..7bb3e2be 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -3,6 +3,8 @@ namespace openvk\Web\Presenters; final class BlobPresenter extends OpenVKPresenter { + protected $banTolerant = true; + private function getDirName($dir): string { if(gettype($dir) === "integer") { diff --git a/Web/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php new file mode 100644 index 00000000..1560ba63 --- /dev/null +++ b/Web/Presenters/NoSpamPresenter.php @@ -0,0 +1,377 @@ +assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $targetDir = __DIR__ . '/../Models/Entities/'; + $mode = in_array($this->queryParam("act"), ["form", "templates", "rollback", "reports"]) ? $this->queryParam("act") : "form"; + + if ($mode === "form") { + $this->template->_template = "NoSpam/Index"; + $foundClasses = []; + foreach (Finder::findFiles('*.php')->from($targetDir) as $file) { + $content = file_get_contents($file->getPathname()); + $namespacePattern = '/namespace\s+([^\s;]+)/'; + $classPattern = '/class\s+([^\s{]+)/'; + preg_match($namespacePattern, $content, $namespaceMatches); + preg_match($classPattern, $content, $classMatches); + + if (isset($namespaceMatches[1]) && isset($classMatches[1])) { + $classNamespace = trim($namespaceMatches[1]); + $className = trim($classMatches[1]); + $fullClassName = $classNamespace . '\\' . $className; + + if ($classNamespace === NoSpamPresenter::ENTITIES_NAMESPACE && class_exists($fullClassName)) { + $foundClasses[] = $className; + } + } + } + + $models = []; + + foreach ($foundClasses as $class) { + $r = new \ReflectionClass(NoSpamPresenter::ENTITIES_NAMESPACE . "\\$class"); + if (!$r->isAbstract() && $r->getName() !== NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") + $models[] = $class; + } + $this->template->models = $models; + } else if ($mode === "templates") { + $this->template->_template = "NoSpam/Templates.xml"; + $filter = []; + if ($this->queryParam("id")) { + $filter["id"] = (int)$this->queryParam("id"); + } + $this->template->templates = iterator_to_array((new NoSpamLogs)->getList($filter)); + } else if ($mode === "reports") { + $this->redirect("/scumfeed"); + } else { + $template = (new NoSpamLogs)->get((int)$this->postParam("id")); + if (!$template || $template->isRollbacked()) + $this->returnJson(["success" => false, "error" => "Шаблон не найден"]); + + $model = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $template->getModel(); + $items = $template->getItems(); + if (count($items) > 0) { + $db = DatabaseConnection::i()->getContext(); + + $unbanned_ids = []; + foreach ($items as $_item) { + try { + $item = new $model; + $table_name = $item->getTableName(); + $item = $db->table($table_name)->get((int)$_item); + if (!$item) continue; + + $item = new $model($item); + + if (key_exists("deleted", $item->unwrap()) && $item->isDeleted()) { + $item->setDeleted(0); + $item->save(); + } + + if (in_array($template->getTypeRaw(), [2, 3])) { + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($item, "ban")) { + $owner = $item; + } else { + foreach ($methods as $method) { + if (method_exists($item, $method)) { + $owner = $item->$method(); + break; + } + } + } + + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + + if (!in_array($_id, $unbanned_ids)) { + $owner->unban($this->user->id); + $unbanned_ids[] = $_id; + } + } + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } + } else { + $this->returnJson(["success" => false, "error" => "Объекты не найдены"]); + } + + $template->setRollback(true); + $template->save(); + + $this->returnJson(["success" => true]); + } + } + + function renderSearch(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + $this->assertNoCSRF(); + $this->willExecuteWriteAction(); + + function searchByAdditionalParams(?string $table = NULL, ?string $where = NULL, ?string $ip = NULL, ?string $useragent = NULL, ?int $ts = NULL, ?int $te = NULL, $user = NULL) + { + $db = DatabaseConnection::i()->getContext(); + if ($table && ($ip || $useragent || $ts || $te || $user)) { + $conditions = []; + + if ($ip) $conditions[] = "`ip` REGEXP '$ip'"; + if ($useragent) $conditions[] = "`useragent` REGEXP '$useragent'"; + if ($ts) $conditions[] = "`ts` < $ts"; + if ($te) $conditions[] = "`ts` > $te"; + if ($user) { + $users = new Users; + + $_user = $users->getByChandlerUser((new ChandlerUsers)->getById($user)) + ?? $users->get((int)$user) + ?? $users->getByAddress($user) + ?? NULL; + + if ($_user) { + $conditions[] = "`user` = '" . $_user->getChandlerGUID() . "'"; + } + } + + $whereStart = "WHERE `object_table` = '$table'"; + if ($table === "profiles") { + $whereStart .= "AND `type` = 0"; + } + + $conditions = count($conditions) > 0 ? "AND (" . implode(" AND ", $conditions) . ")" : ""; + $response = []; + + if ($conditions) { + $logs = $db->query("SELECT * FROM `ChandlerLogs` $whereStart $conditions GROUP BY `object_id`, `object_model`"); + + if (!$where) { + foreach ($logs as $log) { + $log = (new Logs)->get($log->id); + $response[] = $log->getObject()->unwrap(); + } + } else { + foreach ($logs as $log) { + $log = (new Logs)->get($log->id); + $object = $log->getObject()->unwrap(); + + if (!$object) continue; + if (str_starts_with($where, " AND")) { + $where = substr_replace($where, "", 0, strlen(" AND")); + } + + foreach ($db->query("SELECT * FROM `$table` WHERE $where")->fetchAll() as $o) { + if ($object->id === $o["id"]) { + $response[] = $object; + } + } + } + } + } + + return $response; + } + } + + try { + $response = []; + $processed = 0; + + $where = $this->postParam("where"); + $ip = $this->postParam("ip"); + $useragent = $this->postParam("useragent"); + $searchTerm = $this->postParam("q"); + $ts = (int)$this->postParam("ts"); + $te = (int)$this->postParam("te"); + $user = $this->postParam("user"); + + if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm && !$user) + $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); + + $models = explode(",", $this->postParam("models")); + + foreach ($models as $_model) { + $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $_model; + if (!class_exists($model_name)) { + continue; + } + + $model = new $model_name; + + $c = new \ReflectionClass($model_name); + if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") { + continue; + } + + $db = DatabaseConnection::i()->getContext(); + $table = $model->getTableName(); + $columns = $db->getStructure()->getColumns($table); + + if ($searchTerm) { + $conditions = []; + $need_deleted = false; + foreach ($columns as $column) { + if ($column["name"] == "deleted") { + $need_deleted = true; + } else { + $conditions[] = "`$column[name]` REGEXP '$searchTerm'"; + } + } + $conditions = implode(" OR ", $conditions); + + $where = ($this->postParam("where") ? " AND ($conditions)" : "($conditions)"); + if ($need_deleted) $where .= " AND (`deleted` = 0)"; + } + + $rows = []; + if ($ip || $useragent || $ts || $te || $user) { + $rows = searchByAdditionalParams($table, $where, $ip, $useragent, $ts, $te, $user); + } + + if (count($rows) === 0) { + if (!$searchTerm) { + if (str_starts_with($where, " AND")) { + if ($searchTerm && !$this->postParam("where")) { + $where = substr_replace($where, "", 0, strlen(" AND")); + } else { + $where = "(" . $this->postParam("where") . ")" . $where; + } + } + + if (!$where) { + $rows = []; + } else { + $result = $db->query("SELECT * FROM `$table` WHERE $where"); + $rows = $result->fetchAll(); + } + } + } + + if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { + foreach ($rows as $key => $object) { + $object = (array)$object; + $_obj = []; + foreach ($object as $key => $value) { + foreach ($columns as $column) { + if ($column["name"] === $key && in_array(strtoupper($column["nativetype"]), ["BLOB", "BINARY", "VARBINARY", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB"])) { + $value = "[BINARY]"; + break; + } + } + + $_obj[$key] = $value; + $_obj["__model_name"] = $_model; + } + $response[] = $_obj; + } + } else { + $ids = []; + + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + $ids[] = $object->getId(); + } + + $log = new NoSpamLog; + $log->setUser($this->user->id); + $log->setModel($_model); + if ($searchTerm) { + $log->setRegex($searchTerm); + } else { + $log->setRequest($where); + } + $log->setBan_Type((int)$this->postParam("ban")); + $log->setCount(count($rows)); + $log->setTime(time()); + $log->setItems(implode(",", $ids)); + $log->save(); + + $banned_ids = []; + foreach ($rows as $object) { + $object = new $model_name($db->table($table)->get($object->id)); + if (!$object) continue; + + $owner = NULL; + $methods = ["getOwner", "getUser", "getRecipient", "getInitiator"]; + + if (method_exists($object, "ban")) { + $owner = $object; + } else { + foreach ($methods as $method) { + if (method_exists($object, $method)) { + $owner = $object->$method(); + break; + } + } + } + + if ($owner instanceof User && $owner->getId() === $this->user->id) { + if (count($rows) === 1) { + $this->returnJson(["success" => false, "error" => "\"Производственная травма\" — Вы не можете блокировать или удалять свой же контент"]); + } else { + continue; + } + } + + if (in_array((int)$this->postParam("ban"), [2, 3])) { + if ($owner) { + $_id = ($owner instanceof Club ? $owner->getId() * -1 : $owner->getId()); + if (!in_array($_id, $banned_ids)) { + if ($owner instanceof User) { + $owner->ban("**content-noSpamTemplate-" . $log->getId() . "**", false, time() + $owner->getNewBanTime(), $this->user->id); + } else { + $owner->ban("Подозрительная активность"); + } + + $banned_ids[] = $_id; + } + } + } + + if (in_array((int)$this->postParam("ban"), [1, 3])) + $object->delete(); + } + + $processed++; + } + } + + $this->returnJson(["success" => true, "processed" => $processed, "count" => count($response), "list" => $response]); + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } +} diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php old mode 100755 new mode 100644 index b4444bda..80ab0621 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -7,7 +7,7 @@ use Chandler\Security\Authenticator; use Latte\Engine as TemplatingEngine; use openvk\Web\Models\Entities\IP; use openvk\Web\Themes\Themepacks; -use openvk\Web\Models\Repositories\{CurrentUser, IPs, Users, APITokens, Tickets}; +use openvk\Web\Models\Repositories\{IPs, Users, APITokens, Tickets, Reports, CurrentUser}; use WhichBrowser; abstract class OpenVKPresenter extends SimplePresenter @@ -260,8 +260,10 @@ abstract class OpenVKPresenter extends SimplePresenter } $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->reportNotAnsweredCount = (new Reports)->getReportsCount(0); + } } header("X-OpenVK-User-Validated: $userValidated"); diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php new file mode 100644 index 00000000..68d27861 --- /dev/null +++ b/Web/Presenters/ReportPresenter.php @@ -0,0 +1,151 @@ +reports = $reports; + + parent::__construct(); + } + + function renderList(): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + if ($_SERVER["REQUEST_METHOD"] === "POST") + $this->assertNoCSRF(); + + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + + if (!$this->queryParam("orig")) { + $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); + $this->template->count = $this->reports->getReportsCount(); + } else { + $orig = $this->reports->get((int) $this->queryParam("orig")); + if (!$orig) $this->redirect("/scumfeed"); + + $this->template->reports = $orig->getDuplicates(); + $this->template->count = $orig->getDuplicatesCount(); + $this->template->orig = $orig->getId(); + } + $this->template->paginatorConf = (object) [ + "count" => $this->template->count, + "page" => $this->queryParam("p") ?? 1, + "amount" => NULL, + "perPage" => 15, + ]; + $this->template->mode = $act ?? "all"; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $reports = []; + foreach ($this->reports->getReports(0, 0, $act, false) as $report) { + $reports[] = [ + "id" => $report->getId(), + "author" => [ + "id" => $report->getReportAuthor()->getId(), + "url" => $report->getReportAuthor()->getURL(), + "name" => $report->getReportAuthor()->getCanonicalName(), + "is_female" => $report->getReportAuthor()->isFemale() + ], + "content" => [ + "name" => $report->getContentName(), + "type" => $report->getContentType(), + "id" => $report->getContentId(), + "url" => $report->getContentType() === "user" ? (new Users)->get((int) $report->getContentId())->getURL() : NULL + ], + "duplicates" => $report->getDuplicatesCount(), + ]; + } + $this->returnJson(["reports" => $reports]); + } + } + + function renderView(int $id): void + { + $this->assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) + $this->notFound(); + + $this->template->report = $report; + } + + function renderCreate(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + if(!$id) + exit(json_encode([ "error" => tr("error_segmentation") ])); + + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { + if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { + $report = new Report; + $report->setUser_id($this->user->id); + $report->setTarget_id($id); + $report->setType($this->queryParam("type")); + $report->setReason($this->queryParam("reason")); + $report->setCreated(time()); + $report->save(); + } + + exit(json_encode([ "reason" => $this->queryParam("reason") ])); + } else { + exit(json_encode([ "error" => "Unable to submit a report on this content type" ])); + } + } + + function renderAction(int $id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $report = $this->reports->get($id); + if(!$report || $report->isDeleted()) $this->notFound(); + + if ($this->postParam("ban")) { + $report->deleteContent(); + $report->banUser($this->user->identity->getId()); + + $this->flash("suc", "Смэрть...", "Пользователь успешно забанен."); + } else if ($this->postParam("delete")) { + $report->deleteContent(); + + $this->flash("suc", "Нехай живе!", "Контент удалён, а пользователю прилетело предупреждение."); + } else if ($this->postParam("ignore")) { + $report->delete(); + + $this->flash("suc", "Нехай живе!", "Жалоба проигнорирована."); + } else if ($this->postParam("banClubOwner") || $this->postParam("banClub")) { + if ($report->getContentType() !== "group") + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + + $club = $report->getContentObject(); + if (!$club || $club->isBanned()) + $this->flashFail("err", "Ошибка доступа", "Недостаточно прав для модификации данного ресурса."); + + if ($this->postParam("banClubOwner")) { + $club->getOwner()->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**", false, $club->getOwner()->getNewBanTime(), $this->user->identity->getId()); + } else { + $club->ban("**content-" . $report->getContentType() . "-" . $report->getContentId() . "**"); + } + + $report->delete(); + + $this->flash("suc", "Смэрть...", ($this->postParam("banClubOwner") ? "Создатель сообщества успешно забанен." : "Сообщество успешно забанено")); + } + + $this->redirect("/scumfeed"); + } +} diff --git a/Web/Presenters/templates/@banned.xml b/Web/Presenters/templates/@banned.xml index 48c29ddb..7640838c 100644 --- a/Web/Presenters/templates/@banned.xml +++ b/Web/Presenters/templates/@banned.xml @@ -10,8 +10,19 @@ {_banned_alt}

- {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
- {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {var $ban = $thisUser->getBanReason("banned")} + {if is_string($ban)} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape}
+ {tr("banned_2", htmlentities($thisUser->getBanReason()))|noescape} + {else} + {tr("banned_1", htmlentities($thisUser->getCanonicalName()))|noescape} +

+ Эта страница была заморожена {$ban[0]|noescape} + {if $ban[1] !== "app"} + {include "Report/ViewContent.xml", type => $ban[1], object => $ban[2]} + {/if} +
+ {/if} {if !$thisUser->getUnbanTime()} {_banned_perm} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 1718c499..c79a8e89 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -209,6 +209,25 @@ ({$helpdeskTicketNotAnsweredCount}) {/if} + {tr("reports")} + {if $reportNotAnsweredCount > 0} + ({$reportNotAnsweredCount}) + {/if} + + + noSpam + + {$menuItem["name"]} +
+ + {$club->getName()} {strpos($menuItem["name"], "@") === 0 ? tr(substr($menuItem["name"], 1)) : $menuItem["name"]} @@ -283,8 +302,13 @@ {if !OPENVK_ROOT_CONF['openvk']['preferences']['security']['disablePasswordRestoring']}{_forgot_password}{/if} {/ifset} +
- + {ifset $thisUser} + {if !$thisUser->isBanned()} + + {/if} + {/ifset}
diff --git a/Web/Presenters/templates/@listView.xml b/Web/Presenters/templates/@listView.xml index 34739b59..f2342a85 100644 --- a/Web/Presenters/templates/@listView.xml +++ b/Web/Presenters/templates/@listView.xml @@ -12,16 +12,24 @@ {include size, x => $dat} {/ifset} + {ifset before_content} + {include before_content, x => $dat} + {/ifset} + {ifset specpage} {include specpage, x => $dat} {else}
{var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {ifset top} + {include top, x => $dat} + {/ifset} + {if sizeof($data) > 0}
- +
diff --git a/Web/Presenters/templates/Admin/BansHistory.xml b/Web/Presenters/templates/Admin/BansHistory.xml new file mode 100644 index 00000000..c0dc1b64 --- /dev/null +++ b/Web/Presenters/templates/Admin/BansHistory.xml @@ -0,0 +1,86 @@ +{extends "./@layout.xml"} + +{block title} + История блокировок +{/block} + +{block heading} + {include title} +{/block} + +{block content} + + + + + + + + + + + + + + + + + + + + + + + + + +
IDЗабаненныйИнициаторНачалоКонецВремяПричинаСнята
{$ban->getId()} + + + {$ban->getUser()->getCanonicalName()} + + + + {$ban->getUser()->getCanonicalName()} + + + {_admin_banned} + + + + + {$ban->getInitiator()->getCanonicalName()} + + + + {$ban->getInitiator()->getCanonicalName()} + + {_admin_banned} + + {date('d.m.Y в H:i:s', $ban->getStartTime())}{date('d.m.Y в H:i:s', $ban->getEndTime())}{$ban->getTime()} + {$ban->getReason()} + + {if $ban->isRemovedManually()} + + + {$ban->whoRemoved()->getCanonicalName()} + + + + {$ban->whoRemoved()->getCanonicalName()} + + + {_admin_banned} + + {else} + Активная блокировка + {/if} +
+{/block} \ No newline at end of file diff --git a/Web/Presenters/templates/Apps/Play.xml b/Web/Presenters/templates/Apps/Play.xml index 91cfe5d5..facaa273 100644 --- a/Web/Presenters/templates/Apps/Play.xml +++ b/Web/Presenters/templates/Apps/Play.xml @@ -1,4 +1,5 @@ {extends "../@layout.xml"} +{var $canReport = $owner->getId() !== $thisUser->getId()} {block title} {$name} @@ -6,6 +7,7 @@ {block header} {$name} +
Пожаловаться {/block} {block content} @@ -33,5 +35,29 @@ window.appOrigin = {$origin}; + + {script "js/al_games.js"} {/block} diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 10d1f0dd..42cdbde3 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -141,6 +141,34 @@ {/if} + {var $canReport = $thisUser->getId() != $club->getOwner()->getId()} + {if $canReport} + {_report} + + + {/if}
diff --git a/Web/Presenters/templates/NoSpam/Index.xml b/Web/Presenters/templates/NoSpam/Index.xml new file mode 100644 index 00000000..746f86d5 --- /dev/null +++ b/Web/Presenters/templates/NoSpam/Index.xml @@ -0,0 +1,315 @@ +{extends "../@layout.xml"} + +{block title}noSpam{/block} +{block header}{include title}{/block} + +{block content} + +
{include "Tabs.xml", mode => "form"}
+
+
+
+ + + + + + + +
+ Раздел: + +
+ +
+
+