add a manager to deal with rejected tags

This commit is contained in:
Spine
2025-09-17 08:02:08 +00:00
parent 2cebbe8168
commit 2076cae5d6
12 changed files with 245 additions and 35 deletions

View File

@@ -62,14 +62,54 @@ class Tag extends \Gazelle\BaseManager {
/**
* Check whether this name is allowed. Some tags we never want to see again.
* TODO: implement
*/
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
public function validName(string $name): bool {
return true;
return !$this->pg()->scalar("
select 1 from tag_reject where name = ?
", $name
);
}
// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
/**
* Add a rejected tag that will not be accepted on entry
* Returns the table id or 0 on failure
*/
public function createRejected(string $name, \Gazelle\User $user): int {
try {
$id = $this->pg()->insert("
insert into tag_reject
(name, id_user)
values (?, ?)
", $this->sanitize($name), $user->id
);
return $id;
} catch (\PDOException) {
// most likely a duplicate key due to a name that already exists
return 0;
}
}
public function rejectedList(): array {
return $this->pg()->all(<<<END_SQL
select tr.id_tag_reject as id,
tr.id_user,
um."Username" as creator,
tr.created,
tr.name
from tag_reject tr
inner join relay.users_main um on (um."ID" = tr.id_user)
order by tr.name
END_SQL
);
}
public function removeRejectedList(array $idList): int {
return $this->pg()->prepared_query("
delete from tag_reject
where id_tag_reject in (" . placeholders($idList) . ")",
...$idList
);
}
/**
* Get a tag ready for database input and display.

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class TagReject extends AbstractMigration {
public function up(): void {
$this->table('tag_reject', ['id' => false, 'primary_key' => 'id_tag_reject'])
->addColumn('id_tag_reject', 'integer', ['identity' => true])
->addColumn('id_user', 'integer')
->addColumn('created', 'timestamp', ['timezone' => true, 'default' => 'CURRENT_TIMESTAMP'])
->addColumn('name', 'string', ['length' => 40])
->save();
$this->query("
create unique index tr_name_uidx on tag_reject (name)
");
}
public function down(): void {
$this->table('tag_reject')->drop()->save();
}
}

View File

@@ -937,6 +937,18 @@ hr.tbx {
margin: 15px 6px 10px 6px;
}
div.tbx-tag-reject {
display: grid;
gap: 6px 2px;
grid-template-columns: repeat(7, 1fr);
padding-bottom: 24px;
}
div.tbx-tag-reject div.tbx-field {
border: 1px solid #808080;
width: 120px;
padding: 3px;
}
.pager-link {
margin: 0 2px;
padding: 1px 3px;

View File

@@ -22,19 +22,18 @@ if (is_null($tgroup)) {
//Delete cached tag used for undos
if (isset($_REQUEST['undo'])) {
$Cache->delete_value("deleted_tags_{$tgroup->id()}_{$Viewer->id()}");
$Cache->delete_value("deleted_tags_{$tgroup->id}_{$Viewer->id}");
}
$added = [];
$rejected = [];
$tagMan = new \Gazelle\Manager\Tag();
$Tags = array_unique(explode(',', $_REQUEST['tagname']));
$tagMan = new Manager\Tag();
foreach ($Tags as $tagName) {
foreach (array_unique(explode(',', $_REQUEST['tagname'])) as $tagName) {
$tagName = $tagMan->sanitize($tagName);
$resolved = $tagMan->resolve($tagName);
if (empty($resolved)) {
if (is_null($resolved)) {
$rejected[] = $tagName;
} else {
$tag = $tagMan->softCreate($resolved, $Viewer);
@@ -44,6 +43,7 @@ foreach ($Tags as $tagName) {
json_error('This tag is not allowed');
} else {
header('Location: ' . $tgroup->location());
exit;
}
}
if ($tag->hasVoteTGroup($tgroup, $Viewer)) {
@@ -52,8 +52,8 @@ foreach ($Tags as $tagName) {
json_error('you have already voted on this tag');
} else {
header('Location: ' . $tgroup->location());
exit;
}
exit;
}
$tag->addTGroup($tgroup, $Viewer, 3);
$tag->voteTGroup($tgroup, $Viewer, 'up');
@@ -64,7 +64,7 @@ foreach ($Tags as $tagName) {
}
$tgroup->refresh();
if (AJAX) {
if (defined('AJAX')) {
json_print('success', [
'added' => $added,
'rejected' => $rejected,

View File

@@ -201,6 +201,9 @@ switch ($_REQUEST['action'] ?? '') {
case 'tags_official':
include_once 'managers/tags_official.php';
break;
case 'tag-reject':
include_once 'managers/tag-reject.php';
break;
case 'tokens':
include_once 'managers/tokens.php';
break;

View File

@@ -0,0 +1,37 @@
<?php
/** @phpstan-var \Gazelle\User $Viewer */
/** @phpstan-var \Twig\Environment $Twig */
declare(strict_types=1);
namespace Gazelle;
if (!$Viewer->permitted('users_mod')) {
Error403::error();
}
$manager = new Manager\Tag();
$message = [];
if (isset($_POST['create']) && $_POST['create'] !== '') {
authorize();
$name = $manager->sanitize($_POST['create']);
if ($name !== '') {
$message[] = $manager->createRejected($name, $Viewer)
? "Created tag \"$name\""
: "Did not create tag \"$name\", check if it already exists";
}
}
if (isset($_POST['remove']) && $_POST['remove'] !== []) {
authorize();
$result = $manager->removeRejectedList(
array_map('intval', $_POST['remove'])
);
$message[] = "$result tag" . plural($result) . " removed";
}
echo $Twig->render('tag/reject.twig', [
'list' => $manager->rejectedList(),
'message' => $message,
'viewer' => $Viewer,
]);

View File

@@ -14,7 +14,7 @@ if (!$Viewer->permitted('users_mod')) {
echo $Twig->render('admin/toolbox.twig', [
'applicant_viewer' => (bool)array_filter(
new Manager\ApplicantRole()->publishedList(),
fn($r) => $r->isStaffViewer($Viewer)
fn ($r) => $r->isStaffViewer($Viewer)
),
'viewer' => $Viewer,
]);

View File

@@ -6,6 +6,15 @@ namespace Gazelle;
if (!empty($_REQUEST['action'])) {
switch ($_REQUEST['action']) {
case 'add_alias':
include_once 'add_alias.php';
break;
case 'add_tag':
include_once __DIR__ . '/../ajax/torrent_tag_add.php';
break;
case 'delete_alias':
include_once 'delete_alias.php';
break;
case 'edit':
include_once 'edit.php';
break;
@@ -83,12 +92,6 @@ if (!empty($_REQUEST['action'])) {
case 'tgroup-merge':
include_once 'merge.php';
break;
case 'add_alias':
include_once 'add_alias.php';
break;
case 'delete_alias':
include_once 'delete_alias.php';
break;
case 'delete':
include_once 'delete.php';
break;
@@ -158,7 +161,7 @@ if (!empty($_REQUEST['action'])) {
$torrentId = (int)$_GET['torrentid'];
$tgroup = $manager->findByTorrentId($torrentId);
if ($tgroup) {
header("Location: torrents.php?id={$tgroup->id()}&torrentid={$torrentId}#torrent{$torrentId}");
header("Location: torrents.php?id={$tgroup->id}&torrentid={$torrentId}#torrent{$torrentId}");
} else {
header("Location: log.php?search=Torrent+$torrentId");
}

View File

@@ -31,7 +31,6 @@
['Reports V1', 'reports.php', viewer.permittedAny('admin_reports', 'site_moderate_forums')],
['Staff page group manager', 'tools.php?action=staff_groups', viewer.permitted('admin_manage_permissions')],
['Torrent report configuration', 'tools.php?action=torrent_report_view', viewer.permitted('users_mod')],
['Userclass manager', 'tools.php?action=userclass', viewer.permitted('admin_manage_permissions')],
]) }}
{{ _self.category('Announcements', [
@@ -48,6 +47,13 @@
]) }}
{{ _self.category('Tags', [
['Batch tag editor', 'tools.php?action=tags', true],
['Tag aliases', 'tools.php?action=tags_aliases', true],
['Official tags manager', 'tools.php?action=tags_official', true],
['Rejected tags manager', 'tools.php?action=tag-reject', true],
]) }}
</div><div class="toolbox_container">
{{ _self.category('User management', [
@@ -65,6 +71,7 @@
['Registration log', 'tools.php?action=registration_log', viewer.permitted('users_view_email')],
['Send custom PM', 'tools.php?action=custom_pm', viewer.permitted('admin_site_debug')],
['User flow', 'tools.php?action=user_flow', viewer.permitted('site_view_flow')],
['Userclass manager', 'tools.php?action=userclass', viewer.permitted('admin_manage_permissions')],
]) }}
{{ _self.category('Community', [
@@ -76,12 +83,6 @@
['Stylesheet usage', 'tools.php?action=stylesheets', viewer.permitted('admin_manage_stylesheets')],
]) }}
{{ _self.category('Tags', [
['Batch tag editor', 'tools.php?action=tags', true],
['Tag aliases', 'tools.php?action=tags_aliases', true],
['Official tags manager', 'tools.php?action=tags_official', true],
]) }}
</div><div class="toolbox_container">
{{ _self.category('Finances', [

57
templates/tag/reject.twig Normal file
View File

@@ -0,0 +1,57 @@
{{ header('Rejected Tags Manager') }}
<div class="header">
<div class="linkbox">
<a href="?action=tags" class="brackets">Batch Tag Editor</a>
<a href="?action=tags_aliases" class="brackets">Tag Aliases</a>
<a href="?action=tags_official" class="brackets">Official Tags</a>
</div>
<h2>Rejected Tags Manager</h2>
</div>
<div class="thin box">
{% for m in message %}
{% if loop.first %}
<ul class="nobullet">
{% endif %}
<li>{{ m }}</li>
{% if loop.last %}
</ul>
<br />
{% endif %}
{% endfor %}
<form name="tag-reject" method="post">
{% for tag in list %}
{% if loop.first %}
<div class="tbx-tag-reject">
{% endif %}
<div class="tbx-field">
<span title="Created by {{ tag.creator }} on {{
tag.created|date('Y-m-d H:i:s') }}">{{ tag.name }}</span>
<div style="float: right">
<label><small>❌</small>
<input type="checkbox" name="remove[]" value="{{ tag.id }}" />
</label>
</div>
</div>
{% if loop.last %}
<div>Check any <small>❌</small> to remove</div>
</div>
{% endif %}
{% else %}
<div class="pad"><b>No rejected tags have been defined.</b></div>
{% endfor %}
<div class="tbx tbx-tag-reject-edit">
<div class="tbx-label">Create new entry</div>
<div class="tbx-field">
<input type="text" name="create" size="20" value="" placeholder="tag name" />
</div>
<div class="tbx-label"></div>
<div class="tbx-field">
<input type="hidden" name="action" value="tag-reject" />
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
<input type="submit" value="Update" />
</div>
</div>
</form>
{{ footer() }}

View File

@@ -15,29 +15,29 @@
{% endif %}
</div>
{% for tag in tgroup.tagList %}
{% if loop.first %}
{% if loop.first %}
<ul class="stats nobullet">
{% endif %}
{% endif %}
<li>
<a href="torrents.php?taglist={{ tag.name }}" style="float: left; display: block;">{{ tag.name }}</a>
<div style="float: right; display: block; letter-spacing: -1px;" class="edit_tags_votes">
<a href="torrents.php?action=vote_tag&amp;way=up&amp;groupid={{ tgroup_id }}&amp;tagid={{ tag.id }}&amp;auth={{ viewer.auth }}" title="Vote this tag up" class="tooltip vote_tag_up">&#x25b2;</a>
{{ tag.score }}
<a href="torrents.php?action=vote_tag&amp;way=down&amp;groupid={{ tgroup_id }}&amp;tagid={{ tag.id }}&amp;auth={{ viewer.auth }}" title="Vote this tag down" class="tooltip vote_tag_down">&#x25bc;</a>
{% if viewer.permitted('users_warn') %}
{% if viewer.permitted('users_warn') %}
<a href="user.php?id={{ tag.userId }}" title="View the profile of the user that added this tag" class="brackets tooltip view_tag_user">U</a>
{% endif %}
{% if not viewer.disableTagging and viewer.permitted('site_delete_tag') %}
{% endif %}
{% if not viewer.disableTagging and viewer.permitted('site_delete_tag') %}
<span class="remove remove_tag">
<a href="ajax.php?action=delete_tag&amp;groupid={{ tgroup_id }}&amp;tagid={{ tag.id }}&amp;auth={{ viewer.auth }}" class="brackets tooltip" title="Remove tag">X</a>
</span>
{% endif %}
{% endif %}
</div>
<br style="clear: both;" />
</li>
{% if loop.last %}
{% if loop.last %}
</ul>
{% endif %}
{% endif %}
{% else %}
<ul><li>There are no tags to display.</li></ul>
{% endfor %}
@@ -47,7 +47,7 @@
<div class="box box_addtag">
<div class="head"><strong>Add tag</strong></div>
<div class="body">
<form class="add_form" name="tags" action="ajax.php" method="post">
<form class="add_form" name="tags" action="torrents.php" method="post">
<input type="hidden" name="action" value="add_tag" />
<input type="hidden" name="auth" value="{{ viewer.auth }}" />
<input type="hidden" name="groupid" value="{{ tgroup_id }}>" />

View File

@@ -47,6 +47,7 @@ class TagTest extends TestCase {
public function testNormalize(): void {
$manager = new Manager\Tag();
$this->assertEquals('', $manager->normalize(' '), 'tag-normalize-space');
$this->assertEquals('dub', $manager->normalize('Dub dub DUB! '), 'tag-normalize-dup');
$this->assertEquals('neo.folk', $manager->normalize('neo...folk neo-folk'), 'tag-normalize-more');
$this->assertEquals('pop rock', $manager->normalize(' pop rock rock pop Rock'), 'tag-normalize-two');
@@ -144,6 +145,13 @@ class TagTest extends TestCase {
),
'tag-replace-alias'
);
$found = $manager->listAlias(false);
$this->assertGreaterThan(0, count($found), 'tag-alias-total');
$this->assertEquals(
['id' => $aliasId, 'bad' => $bad->name(), 'alias' => $good->name()],
$found[$aliasId],
'tag-alias-list',
);
$tagList = [
'include' => [$good->name()],
'exclude' => ['!' . $exclude->name()],
@@ -396,6 +404,10 @@ class TagTest extends TestCase {
}
public function testTop10(): void {
global $Cache;
$Cache->delete_multi([
"toptaguse_1", "toptagreq_1", "toptagvote_1",
]);
$manager = new Manager\Tag();
$this->assertGreaterThanOrEqual(0, count($manager->topTGroupList(1)), 'tag-top10-tgroup');
$this->assertGreaterThanOrEqual(0, count($manager->topRequestList(1)), 'tag-top10-request');
@@ -414,4 +426,26 @@ class TagTest extends TestCase {
$this->assertCount(0, new Json\Top10\Tag('bogus', 1, $manager)->payload(), 'tag-top10-bogus-payload');
}
public function testTagRejection(): void {
$this->user = Helper::makeUser('tag.' . randomString(6), 'tag');
$manager = new Manager\Tag();
$name = 'reject.' . randomString(16);
$id = $manager->createRejected($name, $this->user);
$this->assertGreaterThan(0, $id, 'tag-reject-create');
$this->assertEquals(
0,
$manager->createRejected($name, $this->user),
'tag-reject-duplicate',
);
$list = array_filter($manager->rejectedList(), fn ($t) => $t['name'] === $name);
$this->assertCount(1, $list, 'tag-reject-list-count');
$this->assertEquals($name, $list[0]['name'], 'tag-reject-list-name');
$this->assertEquals(
1,
$manager->removeRejectedList([$id]),
'tag-reject-remove',
);
}
}