diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index 4f90b31e..35fab4c5 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -361,6 +361,12 @@ class Club extends RowModel $this->save(); } + function unban(): void + { + $this->setBlock_Reason(null); + $this->save(); + } + function getAlert(): ?string { return $this->getRecord()->alert; 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/User.php b/Web/Models/Entities/User.php index 58a1bf51..ffc30a51 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -264,22 +264,26 @@ class User extends RowModel $reason = $ban->getReason(); - preg_match('/\*\*content-(post|photo|video|group|comment|note|app)-(\d+)\*\*$/', $reason, $matches); + preg_match('/\*\*content-(post|photo|video|group|comment|note|app|noSpamTemplate)-(\d+)\*\*$/', $reason, $matches); if (sizeof($matches) === 3) { - if ($for !== "banned") { + $content_type = $matches[1]; $content_id = (int) $matches[2]; + if ($content_type === "noSpamTemplate") { $reason = "Подозрительная активность"; } else { - $content_type = $matches[1]; $content_id = (int) $matches[2]; - $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; - default: $reason[] = null; + 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; + default: $reason[] = null; + } } } } @@ -902,6 +906,21 @@ class User extends RowModel $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(); + } + function deactivate(?string $reason): void { $this->setDeleted(1); 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/Presenters/NoSpamPresenter.php b/Web/Presenters/NoSpamPresenter.php new file mode 100644 index 00000000..2f3f3602 --- /dev/null +++ b/Web/Presenters/NoSpamPresenter.php @@ -0,0 +1,276 @@ +assertUserLoggedIn(); + $this->assertPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0); + + $targetDir = __DIR__ . '/../Models/Entities/'; + $mode = in_array($this->queryParam("act"), ["form", "templates", "rollback"]) ? $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 { + $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(); + + try { + $where = $this->postParam("where"); + $ip = $this->postParam("ip"); + $useragent = $this->postParam("useragent"); + $searchTerm = $this->postParam("q"); + $ts = $this->postParam("ts"); + $te = $this->postParam("te"); + + if (!$ip && !$useragent && !$searchTerm && !$ts && !$te && !$where && !$searchTerm) + $this->returnJson(["success" => false, "error" => "Нет запроса. Заполните поле \"подстрока\" или введите запрос \"WHERE\" в поле под ним."]); + + $model_name = NoSpamPresenter::ENTITIES_NAMESPACE . "\\" . $this->postParam("model"); + if (!class_exists($model_name)) + $this->returnJson(["success" => false, "error" => "Модель не найдена"]); + + $model = new $model_name; + + $c = new \ReflectionClass($model_name); + if ($c->isAbstract() || $c->getName() == NoSpamPresenter::ENTITIES_NAMESPACE . "\\Correspondence") + $this->returnJson(["success" => false, "error" => "No."]); + + $db = DatabaseConnection::i()->getContext(); + $table = $model->getTableName(); + $columns = $db->getStructure()->getColumns($table); + + $rows = []; + if (!$where) { + $conditions = []; + $need_deleted = false; + foreach ($columns as $column) { + if ($column["name"] == "deleted") { + $need_deleted = true; + } else { + $conditions[] = "`$column[name]` REGEXP '$searchTerm'"; + } + } + + $where = "(" . implode(" OR ", $conditions) . ")"; + if ($need_deleted) $where .= " AND `deleted` = 0"; + } + + $result = $db->query("SELECT * FROM `$table` WHERE $where"); + $rows = $result->fetchAll(); + + if (!in_array((int)$this->postParam("ban"), [1, 2, 3])) { + $response = []; + 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; + } + $response[] = $_obj; + } + + $this->returnJson(["success" => true, "count" => count($response), "list" => $response]); + } 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($this->postParam("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(); + } + + $this->returnJson(["success" => true]); + } + } catch (\Throwable $e) { + $this->returnJson(["success" => false, "error" => $e->getMessage()]); + } + } +} diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index 992139f4..c79a8e89 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -214,6 +214,9 @@ ({$reportNotAnsweredCount}) {/if} + + noSpam + "form"} +
+
+
+ + + + + + + +
+ Раздел: + + +
+
+ + + + + + + {* "Это могли бы быть мы с тобой, но" в овк нет логов :( *} + {* + + + + + + + + + + + + + + + *} + +
+ Подстрока: + + +
+ IP: + + +
+ Юзер-агент: + + +
+ Время раньше, чем: + + +
+ Время позже, чем: + + +
+
+
ИЛИ
+
+