Ability to block URL (#693)

This commit is contained in:
n1rwana 2022-09-05 18:57:41 +03:00 committed by GitHub
parent 8c8eef1329
commit 125c6b1b63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 414 additions and 5 deletions

View file

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User};
use openvk\Web\Models\Repositories\{Users};
use Nette\Database\Table\ActiveRow;
class BannedLink extends RowModel
{
protected $tableName = "links_banned";
private $overrideContentColumn = "reason";
function getId(): int
{
return $this->getRecord()->id;
}
function getDomain(): string
{
return $this->getRecord()->domain;
}
function getReason(): string
{
return $this->getRecord()->reason ?? tr("url_is_banned_default_reason");
}
function getInitiator(): ?User
{
return (new Users)->get($this->getRecord()->initiator);
}
function getComment(): string
{
return OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["showReason"]
? tr("url_is_banned_comment_r", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"], $this->getReason())
: tr("url_is_banned_comment", OPENVK_ROOT_CONF["openvk"]["appearance"]["name"]);
}
function getRegexpRule(): string
{
return addslashes("/" . $this->getDomain() . $this->getRawRegexp() . "/");
}
function getRawRegexp(): string
{
return $this->getRecord()->regexp_rule;
}
}

View file

@ -35,12 +35,12 @@ trait TRichText
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%", "%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string { (function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]); $href = str_replace("#", "&num;", $matches[1]);
$href = str_replace(";", "&#59;", $matches[1]); $href = rawurlencode(str_replace(";", "&#59;", $matches[1]));
$link = str_replace("#", "&num;", $matches[3]); $link = str_replace("#", "&num;", $matches[3]);
$link = str_replace(";", "&#59;", $matches[3]); $link = str_replace(";", "&#59;", $matches[3]);
$rel = $this->isAd() ? "sponsored" : "ugc"; $rel = $this->isAd() ? "sponsored" : "ugc";
return "<a href='$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]); return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
}), }),
$text $text
); );

View file

@ -0,0 +1,73 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection as DB;
use Nette\Database\Table\{ActiveRow, Selection};
use openvk\Web\Models\Entities\BannedLink;
class BannedLinks
{
private $context;
private $bannedLinks;
function __construct()
{
$this->context = DB::i()->getContext();
$this->bannedLinks = $this->context->table("links_banned");
}
function toBannedLink(?ActiveRow $ar): ?BannedLink
{
return is_null($ar) ? NULL : new BannedLink($ar);
}
function get(int $id): ?BannedLink
{
return $this->toBannedLink($this->bannedLinks->get($id));
}
function getList(?int $page = 1): \Traversable
{
foreach($this->bannedLinks->order("id DESC")->page($page, OPENVK_DEFAULT_PER_PAGE) as $link)
yield new BannedLink($link);
}
function getCount(int $page = 1): int
{
return sizeof($this->bannedLinks->fetch());
}
function getByDomain(string $domain): ?Selection
{
return $this->bannedLinks->where("domain", $domain);
}
function isDomainBanned(string $domain): bool
{
return sizeof($this->bannedLinks->where(["link" => $domain, "regexp_rule" => ""])) > 0;
}
function genLinks($rules): \Traversable
{
foreach ($rules as $rule)
yield $this->get($rule->id);
}
function genEntries($links, $uri): \Traversable
{
foreach($links as $link)
if (preg_match($link->getRegexpRule(), $uri))
yield $link->getId();
}
function check(string $url): ?array
{
$uri = strstr(str_replace(["https://", "http://"], "", $url), "/", true);
$domain = str_replace("www.", "", $uri);
$rules = $this->getByDomain($domain);
if (is_null($rules))
return NULL;
return iterator_to_array($this->genEntries($this->genLinks($rules), $uri));
}
}

View file

@ -1,7 +1,8 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User}; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink};
use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts}; use openvk\Web\Models\Repositories\{Users, Clubs, Vouchers, Gifts, BannedLinks};
use Chandler\Database\DatabaseConnection;
final class AdminPresenter extends OpenVKPresenter final class AdminPresenter extends OpenVKPresenter
{ {
@ -9,13 +10,15 @@ final class AdminPresenter extends OpenVKPresenter
private $clubs; private $clubs;
private $vouchers; private $vouchers;
private $gifts; private $gifts;
private $bannedLinks;
function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts) function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks)
{ {
$this->users = $users; $this->users = $users;
$this->clubs = $clubs; $this->clubs = $clubs;
$this->vouchers = $vouchers; $this->vouchers = $vouchers;
$this->gifts = $gifts; $this->gifts = $gifts;
$this->bannedLinks = $bannedLinks;
parent::__construct(); parent::__construct();
} }
@ -375,4 +378,73 @@ final class AdminPresenter extends OpenVKPresenter
$user->adminNotify("⚠️ " . $this->queryParam("message")); $user->adminNotify("⚠️ " . $this->queryParam("message"));
exit(json_encode([ "message" => $this->queryParam("message") ])); exit(json_encode([ "message" => $this->queryParam("message") ]));
} }
function renderBannedLinks(): void
{
$this->template->links = $this->bannedLinks->getList((int) $this->queryParam("p") ?: 1);
$this->template->users = new Users;
}
function renderBannedLink(int $id): void
{
$this->template->form = (object) [];
if($id === 0) {
$this->template->form->id = 0;
$this->template->form->link = NULL;
$this->template->form->reason = NULL;
} else {
$link = (new BannedLinks)->get($id);
if(!$link)
$this->notFound();
$this->template->form->id = $link->getId();
$this->template->form->link = $link->getDomain();
$this->template->form->reason = $link->getReason();
$this->template->form->regexp = $link->getRawRegexp();
}
if($_SERVER["REQUEST_METHOD"] !== "POST")
return;
$link = (new BannedLinks)->get($id);
$new_domain = parse_url($this->postParam("link"))["host"];
$new_reason = $this->postParam("reason") ?: NULL;
$lid = $id;
if ($link) {
$link->setDomain($new_domain ?? $this->postParam("link"));
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->save();
} else {
if (!$new_domain)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_specified"));
$link = new BannedLink;
$link->setDomain($new_domain);
$link->setReason($new_reason);
$link->setRegexp_rule($this->postParam("regexp"));
$link->setInitiator($this->user->identity->getId());
$link->save();
$lid = $link->getId();
}
$this->redirect("/admin/bannedLink/id" . $lid);
}
function renderUnbanLink(int $id): void
{
$link = (new BannedLinks)->get($id);
if (!$link)
$this->flashFail("err", tr("error"), tr("admin_banned_link_not_found"));
$link->delete(false);
$this->redirect("/admin/bannedLinks");
}
} }

View file

@ -1,13 +1,29 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Repositories\BannedLinks;
use openvk\Web\Models\Entities\BannedLink;
final class AwayPresenter extends OpenVKPresenter final class AwayPresenter extends OpenVKPresenter
{ {
function renderAway(): void function renderAway(): void
{ {
$checkBanEntries = (new BannedLinks)->check($this->queryParam("to") . "/");
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["susLinks"]["warnings"])
if (sizeof($checkBanEntries) > 0)
$this->pass("openvk!Away->view", $checkBanEntries[0]);
header("HTTP/1.0 302 Found"); header("HTTP/1.0 302 Found");
header("X-Robots-Tag: noindex, nofollow, noarchive"); header("X-Robots-Tag: noindex, nofollow, noarchive");
header("Location: " . $this->queryParam("to")); header("Location: " . $this->queryParam("to"));
exit; exit;
} }
function renderView(int $lid) {
$this->template->link = (new BannedLinks)->get($lid);
if (!$this->template->link)
$this->notFound();
$this->template->to = $this->queryParam("to");
}
} }

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\BannedLink;
use openvk\Web\Models\Repositories\BannedLinks;
final class BannedLinkPresenter extends OpenVKPresenter
{
function renderView(int $lid) {
$this->template->link = (new BannedLinks)->get($lid);
$this->template->to = $this->queryParam("to");
}
}

View file

@ -56,6 +56,9 @@
<li> <li>
<a href="/admin/clubs">{_groups}</a> <a href="/admin/clubs">{_groups}</a>
</li> </li>
<li>
<a href="/admin/bannedLinks">{_admin_banned_links}</a>
</li>
</ul> </ul>
<div class="aui-nav-heading"> <div class="aui-nav-heading">
<strong>{_admin_services}</strong> <strong>{_admin_services}</strong>

View file

@ -0,0 +1,48 @@
{extends "@layout.xml"}
{block title}
{_edit}
{/block}
{block heading}
{_edit} #{$form->id ?? "undefined"}
{/block}
{block content}
<div style="margin: 8px -8px;" class="aui-tabs horizontal-tabs">
<ul class="tabs-menu">
<li class="menu-item active-tab">
<a href="#info">{_admin_banned_link}</a>
</li>
</ul>
<div class="tabs-pane active-pane" id="info">
<form class="aui" method="POST">
<div class="field-group">
<label for="id">ID</label>
<input class="text long-field" type="number" id="id" name="id" disabled value="{$form->id}" />
</div>
<div class="field-group">
<label for="token">{_admin_banned_domain}</label>
<input class="text long-field" type="text" id="link" name="link" value="{$form->link}" />
<div class="description">{_admin_banned_link_description}</div>
</div>
<div class="field-group">
<label for="token">{_admin_banned_link_regexp}</label>
<input class="text long-field" type="text" id="regexp" name="regexp" value="{$form->regexp}" />
<div class="description">{_admin_banned_link_regexp_description}</div>
</div>
<div class="field-group">
<label for="coins">{_admin_banned_link_reason}</label>
<input class="text long-field" type="text" id="reason" name="reason" value="{$form->reason}" />
</div>
<div class="buttons-container">
<div class="buttons">
<input type="hidden" name="hash" value="{$csrfToken}" />
<input class="aui-button aui-button-primary submit" type="submit" value="{_save}">
<a class="aui-button aui-button-secondary" href="/admin/bannedLink/id{$form->id}/unban">{_delete}</a>
</div>
</div>
</form>
</div>
</div>
{/block}

View file

@ -0,0 +1,46 @@
{extends "@layout.xml"}
{block title}
{_admin_banned_links}
{/block}
{block heading}
<a style="float: right;" class="aui-button aui-button-primary" href="/admin/bannedLink/id0">
{_create}
</a>
{_admin_banned_links}
{/block}
{block content}
<table class="aui aui-table-list">
<thead>
<tr>
<th>ID</th>
<th>{_admin_banned_domain}</th>
<th>REGEXP</th>
<th>{_admin_banned_link_reason}</th>
<th>{_admin_banned_link_initiator}</th>
<th>{_admin_actions}</th>
</tr>
</thead>
<tbody>
<tr n:foreach="$links as $link">
<td>{$link->getId()}</td>
<td>{$link->getDomain()}</td>
<td>{$link->getRegexpRule()}</td>
<td>{$link->getReason() ?? "-"}</td>
<td>{$link->getInitiator()->getCanonicalName()}</td>
<td>
<a class="aui-button aui-button-primary" href="/admin/bannedLink/id{$link->getId()}">
<span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span>
</a>
</td>
</tr>
</tbody>
</table>
<div align="right">
<a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="?p={($_GET['p'] ?? 1) - 1}">«</a>
<a class="aui-button" href="?p={($_GET['p'] ?? 1) + 1}">»</a>
</div>
{/block}

View file

@ -0,0 +1,22 @@
{extends "../@layout.xml"}
{block title}Переход по ссылке заблокирован{/block}
{block header}
Предупреждение
{/block}
{block content}
<div style="min-height: 120px;">
<img src="/assets/packages/static/openvk/img/oof.apng" width="110" height="110" style="margin-left: 20px;">
<div style="padding-left: 150px; margin-top: -100px;">
<h4 style="font-size: 14px; margin-bottom: 8px;">{_url_is_banned_title}</h4>
<span>
{$link->getComment()|noescape}
</span>
<br><br>
<a href="{$to}" class="button" target="_blank">{_url_is_banned_proceed}</a>
</div>
</div>
{/block}

View file

@ -23,6 +23,7 @@ services:
- openvk\Web\Presenters\AppsPresenter - openvk\Web\Presenters\AppsPresenter
- openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\ThemepacksPresenter
- openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\VKAPIPresenter
- openvk\Web\Presenters\BannedLinkPresenter
- openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Users
- openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Posts
- openvk\Web\Models\Repositories\Photos - openvk\Web\Models\Repositories\Photos
@ -42,3 +43,4 @@ services:
- openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\Topics
- openvk\Web\Models\Repositories\Applications - openvk\Web\Models\Repositories\Applications
- openvk\Web\Models\Repositories\ContentSearchRepository - openvk\Web\Models\Repositories\ContentSearchRepository
- openvk\Web\Models\Repositories\BannedLinks

View file

@ -257,6 +257,8 @@ routes:
handler: "About->invite" handler: "About->invite"
- url: "/away.php" - url: "/away.php"
handler: "Away->away" handler: "Away->away"
- url: "/away.php/{num}"
handler: "Away->view"
- url: "/gift{num}_{num}.png" - url: "/gift{num}_{num}.png"
handler: "Gifts->giftImage" handler: "Gifts->giftImage"
- url: "/gifts{num}" - url: "/gifts{num}"
@ -303,6 +305,12 @@ routes:
handler: "Support->quickBanInSupport" handler: "Support->quickBanInSupport"
- url: "/admin/support/unban/{num}" - url: "/admin/support/unban/{num}"
handler: "Support->quickUnbanInSupport" handler: "Support->quickUnbanInSupport"
- url: "/admin/bannedLinks"
handler: "Admin->bannedLinks"
- url: "/admin/bannedLink/id{num}"
handler: "Admin->bannedLink"
- url: "/admin/bannedLink/id{num}/unban"
handler: "Admin->unbanLink"
- url: "/upload/photo/{text}" - url: "/upload/photo/{text}"
handler: "VKAPI->photoUpload" handler: "VKAPI->photoUpload"
- url: "/method/{text}.{text}" - url: "/method/{text}.{text}"

View file

@ -0,0 +1,14 @@
CREATE TABLE `links_banned` (
`id` bigint UNSIGNED NOT NULL,
`domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`regexp_rule` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,
`initiator` bigint UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ALTER TABLE `links_banned`
ADD PRIMARY KEY (`id`);
ALTER TABLE `links_banned`
MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -1074,6 +1074,17 @@
"admin_commerce_disabled" = "Commerce has been disabled by the system administrator"; "admin_commerce_disabled" = "Commerce has been disabled by the system administrator";
"admin_commerce_disabled_desc" = "The voucher and gift settings will be saved, but will have no effect."; "admin_commerce_disabled_desc" = "The voucher and gift settings will be saved, but will have no effect.";
"admin_banned_links" = "Blocked links";
"admin_banned_link" = "Link";
"admin_banned_domain" = "Domain";
"admin_banned_link_description" = "With the protocol (https://example.com/)";
"admin_banned_link_regexp" = "Regular expression";
"admin_banned_link_regexp_description" = "It is substituted after the domain specified above. Don't fill it out if you want to block the entire domain";
"admin_banned_link_reason" = "Reason";
"admin_banned_link_initiator" = "Initiator";
"admin_banned_link_not_specified" = "The link is not specified";
"admin_banned_link_not_found" = "Link not found";
/* Paginator (deprecated) */ /* Paginator (deprecated) */
"paginator_back" = "Back"; "paginator_back" = "Back";
@ -1127,3 +1138,12 @@
"cookies_popup_content" = "All kids love cookie, so this website uses Cookies to identify your session and nothing more. Check <a href='/privacy'>our privacy policy</a> for more information."; "cookies_popup_content" = "All kids love cookie, so this website uses Cookies to identify your session and nothing more. Check <a href='/privacy'>our privacy policy</a> for more information.";
"cookies_popup_agree" = "Accept"; "cookies_popup_agree" = "Accept";
/* Away */
"url_is_banned" = "Link is not allowed";
"url_is_banned_comment" = "The <b>$1</b> administration recommends not to follow this link.";
"url_is_banned_comment_r" = "The <b>$1</b> administration recommends not to follow this link.<br><br>The reason is: <b>$2</b>";
"url_is_banned_default_reason" = "The link you are trying to open may lead you to a site that was created for the purpose of deceiving users with the intention of gaining profit.";
"url_is_banned_title" = "Link to a suspicious site";
"url_is_banned_proceed" = "Follow the link";

View file

@ -1123,6 +1123,17 @@
"admin_commerce_disabled" = "Коммерция отключена системным администратором"; "admin_commerce_disabled" = "Коммерция отключена системным администратором";
"admin_commerce_disabled_desc" = "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния."; "admin_commerce_disabled_desc" = "Настройки ваучеров и подарков будут сохранены, но не будут оказывать никакого влияния.";
"admin_banned_links" = "Заблокированные ссылки";
"admin_banned_link" = "Ссылка";
"admin_banned_domain" = "Домен";
"admin_banned_link_description" = "С протоколом (https://example.com/)";
"admin_banned_link_regexp" = "Регулярное выражение";
"admin_banned_link_regexp_description" = "Подставляется после домена, указанного выше. Не заполняйте, если хотите заблокировать весь домен";
"admin_banned_link_reason" = "Причина";
"admin_banned_link_initiator" = "Инициатор";
"admin_banned_link_not_specified" = "Ссылка не указана";
"admin_banned_link_not_found" = "Ссылка не найдена";
/* Paginator (deprecated) */ /* Paginator (deprecated) */
"paginator_back" = "Назад"; "paginator_back" = "Назад";
@ -1186,3 +1197,12 @@
"cookies_popup_content" = "Все дети любят печенье, поэтому этот веб-сайт использует Cookies для того, чтобы идентифицировать вашу сессию и ничего более. Ознакомьтесь с нашей <a href='/privacy'>политикой конфиденциальности</a> для получения дополнительной информации."; "cookies_popup_content" = "Все дети любят печенье, поэтому этот веб-сайт использует Cookies для того, чтобы идентифицировать вашу сессию и ничего более. Ознакомьтесь с нашей <a href='/privacy'>политикой конфиденциальности</a> для получения дополнительной информации.";
"cookies_popup_agree" = "Согласен"; "cookies_popup_agree" = "Согласен";
/* Away */
"url_is_banned" = "Переход невозможен";
"url_is_banned_comment" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке.";
"url_is_banned_comment_r" = "Администрация <b>$1</b> не рекомендует переходить по этой ссылке.<br><br>Причина: <b>$2</b>";
"url_is_banned_default_reason" = "Ссылка, по которой вы попытались перейти, может вести на сайт, который был создан с целью обмана пользователей и получения за счёт этого прибыли.";
"url_is_banned_title" = "Ссылка на подозрительный сайт";
"url_is_banned_proceed" = "Перейти по ссылке";

View file

@ -55,6 +55,9 @@ openvk:
processingLimit: 3000 processingLimit: 3000
emojiProcessingLimit: 1000 emojiProcessingLimit: 1000
commerce: false commerce: false
susLinks:
warnings: true
showReason: true
ton: ton:
enabled: false enabled: false
address: "🅿" address: "🅿"