Add Report Create module

WIP
This commit is contained in:
Hypolite Petovan 2023-02-01 08:34:00 -05:00
parent 76de49a25c
commit c312d11dab
13 changed files with 510 additions and 3 deletions

View file

@ -23,6 +23,7 @@ namespace Friendica\Moderation\Factory;
use Friendica\Capabilities\ICanCreateFromTableRow;
use Friendica\Core\System;
use Friendica\Model\Contact;
use Friendica\Moderation\Collection;
use Friendica\Moderation\Entity;
use Psr\Clock\ClockInterface;
@ -114,4 +115,33 @@ class Report extends \Friendica\BaseFactory implements ICanCreateFromTableRow
}, $ruleIds)),
);
}
public function createFromForm(array $rules, int $cid, int $reporterId, int $categoryId, array $ruleIds, string $comment, array $uriIds, bool $forward): Entity\Report
{
$contact = Contact::getById($cid, ['gsid']);
if (!$contact) {
throw new \InvalidArgumentException('Contact with id: ' . $cid . ' not found');
}
if (!in_array($categoryId, Entity\Report::CATEGORIES)) {
throw new \OutOfBoundsException('Category with id: ' . $categoryId . ' not found in set: [' . implode(', ', Entity\Report::CATEGORIES) . ']');
}
return new Entity\Report(
Contact::getPublicIdByUserId($reporterId),
$cid,
$contact['gsid'],
$this->clock->now(),
$categoryId,
$reporterId,
$comment,
$forward,
new Collection\Report\Posts(array_map(function ($uriId) {
return new Entity\Report\Post($uriId);
}, $uriIds)),
new Collection\Report\Rules(array_map(function ($lineId) use ($rules) {
return new Entity\Report\Rule($lineId, $rules[$lineId] ?? '');
}, $ruleIds)),
);
}
}

View file

@ -56,7 +56,7 @@ final class Report extends \Friendica\BaseRepository
return $this->_selectOne(['id' => $lastInsertId]);
}
public function save(\Friendica\Moderation\Entity\Report $Report)
public function save(\Friendica\Moderation\Entity\Report $Report): \Friendica\Moderation\Entity\Report
{
$fields = [
'reporter-id' => $Report->reporterCid,
@ -95,7 +95,7 @@ final class Report extends \Friendica\BaseRepository
$this->db->insert('report-rule', ['rid' => $newReportId, 'line-id' => $rule->lineId, 'text' => $rule->text]);
}
$Report = $this->selectOneById($this->db->lastInsertId());
$Report = $this->selectOneById($newReportId);
}
return $Report;

View file

@ -0,0 +1,344 @@
<?php
/**
* @copyright Copyright (C) 2010-2023, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Moderation\Report;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Content\Conversation as ConversationContent;
use Friendica\Content\Pager;
use Friendica\Content\Text\BBCode;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\Session\Model\UserSession;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\Contact;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Moderation\Entity\Report;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
use Friendica\Network\HTTPException\ForbiddenException;
use Friendica\Util\Network;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
class Create extends BaseModule
{
const CONTACT_ACTION_NONE = 0;
const CONTACT_ACTION_COLLAPSE = 1;
const CONTACT_ACTION_IGNORE = 2;
const CONTACT_ACTION_BLOCK = 3;
/** @var SystemMessages */
private $systemMessages;
/** @var App\Page */
private $page;
/** @var UserSession */
private $session;
/** @var \Friendica\Moderation\Factory\Report */
private $factory;
/** @var \Friendica\Moderation\Repository\Report */
private $repository;
public function __construct(\Friendica\Moderation\Repository\Report $repository, \Friendica\Moderation\Factory\Report $factory, UserSession $session, App\Page $page, SystemMessages $systemMessages, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->systemMessages = $systemMessages;
$this->page = $page;
$this->session = $session;
$this->factory = $factory;
$this->repository = $repository;
}
protected function post(array $request = [])
{
if (!$this->session->getLocalUserId()) {
throw new ForbiddenException();
}
$report = [];
foreach (['cid', 'category', 'rule-ids', 'uri-ids'] as $key) {
if (isset($request[$key])) {
$report[$key] = $request[$key];
}
}
if (isset($request['url'])) {
$cid = Contact::getIdForURL($request['url']);
if ($cid) {
$report['cid'] = $cid;
} else {
$report['url'] = $request['url'];
$this->systemMessages->addNotice($this->t('Contact not found or their server is already blocked on this node.'));
}
}
if (isset($request['comment'])) {
$this->session->set('report_comment', $request['comment']);
unset($request['comment']);
}
if (isset($request['report_create'])) {
$report = $this->factory->createFromForm(
System::getRules(true),
$request['cid'],
$this->session->getLocalUserId(),
$request['category'],
!empty($request['rule-ids']) ? explode(',', $request['rule-ids']) : [],
$this->session->get('report_comment') ?? '',
!empty($request['uri-ids']) ? explode(',', $request['uri-ids']) : [],
(bool)($request['forward'] ?? false),
);
$this->repository->save($report);
switch ($request['contact_action'] ?? 0) {
case self::CONTACT_ACTION_COLLAPSE:
Contact\User::setCollapsed($request['cid'], $this->session->getLocalUserId(), true);
break;
case self::CONTACT_ACTION_IGNORE:
Contact\User::setIgnored($request['cid'], $this->session->getLocalUserId(), true);
break;
case self::CONTACT_ACTION_BLOCK:
Contact\User::setBlocked($request['cid'], $this->session->getLocalUserId(), true);
break;
}
}
$this->baseUrl->redirect($this->args->getCommand() . '?' . http_build_query($report));
}
protected function content(array $request = []): string
{
if (!$this->session->getLocalUserId()) {
throw new ForbiddenException($this->t('Please login to access this page.'));
}
$this->page['aside'] = $this->getAside($request);
if (empty($request['cid'])) {
return $this->pickContact($request);
}
if (empty($request['category'])) {
return $this->pickCategory($request);
}
if ($request['category'] == Report::CATEGORY_VIOLATION && !isset($request['rule-ids'])) {
return $this->pickRules($request);
}
if (!isset($request['uri-ids'])) {
return $this->pickPosts($request);
}
return $this->summary($request);
}
private function pickContact(array $request): string
{
$tpl = Renderer::getMarkupTemplate('moderation/report/create/pick_contact.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Create Moderation Report'),
'page' => $this->t('Pick Contact'),
'description' => $this->t('Please enter below the contact address or profile URL you would like to create a moderation report about.'),
'submit' => $this->t('Submit'),
],
'$url' => ['url', $this->t('Contact address/URL'), $request['url'] ?? ''],
]);
}
private function pickCategory(array $request): string
{
$tpl = Renderer::getMarkupTemplate('moderation/report/create/pick_category.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Create Moderation Report'),
'page' => $this->t('Pick Category'),
'description' => $this->t('Please pick below the category of your report.'),
'submit' => $this->t('Submit'),
],
'$category_spam' => ['category', $this->t('Spam') , Report::CATEGORY_SPAM , $this->t('This contact is publishing many repeated/overly long posts/replies or advertising their product/websites in otherwise irrelevant conversations.'), $request['category'] == Report::CATEGORY_SPAM],
'$category_illegal' => ['category', $this->t('Illegal Content') , Report::CATEGORY_ILLEGAL , $this->t("This contact is publishing content that is considered illegal in this node's hosting juridiction."), $request['category'] == Report::CATEGORY_ILLEGAL],
'$category_safety' => ['category', $this->t('Community Safety') , Report::CATEGORY_SAFETY , $this->t("This contact aggravated you or other people, by being provocative or insensitive, intentionally or not. This includes disclosing people's private information (doxxing), posting threats or offensive pictures in posts or replies."), $request['category'] == Report::CATEGORY_SAFETY],
'$category_unwanted' => ['category', $this->t('Unwanted Content/Behavior'), Report::CATEGORY_UNWANTED , $this->t("This contact has repeatedly published content irrelevant to the node's theme or is openly criticizing the node's administration/moderation without directly engaging with the relevant people for example or repeatedly nitpicking on a sensitive topic."), $request['category'] == Report::CATEGORY_UNWANTED],
'$category_violation' => ['category', $this->t('Rules Violation') , Report::CATEGORY_VIOLATION, $this->t('This contact violated one or more rules of this node. You will be able to pick which one(s) in the next step.'), $request['category'] == Report::CATEGORY_VIOLATION],
'$category_other' => ['category', $this->t('Other') , Report::CATEGORY_OTHER , $this->t('Please elaborate below why you submitted this report. The more details you provide, the better your report can be handled.'), $request['category'] == Report::CATEGORY_OTHER],
'$comment' => ['comment', $this->t('Additional Information'), $this->session->get('report_comment') ?? '', $this->t('Please provide any additional information relevant to this particular report. You will be able to attach posts by this contact in the next step, but any context is welcome.')],
]);
}
private function pickRules(array $request): string
{
$rules = [];
foreach (System::getRules(true) as $rule_line => $rule_text) {
$rules[] = ['rule-ids[]', $rule_line, $rule_text, in_array($rule_line, $request['rule_ids'] ?? [])];
}
$tpl = Renderer::getMarkupTemplate('moderation/report/create/pick_rules.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Create Moderation Report'),
'page' => $this->t('Pick Rules'),
'description' => $this->t('Please pick below the node rules you believe this contact violated.'),
'submit' => $this->t('Submit'),
],
'$rules' => $rules,
]);
}
private function pickPosts(array $request): string
{
$threads = [];
$contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $request['cid']]);
if (DBA::isResult($contact)) {
$contact_field = $contact['contact-type'] == Contact::TYPE_COMMUNITY || $contact['network'] == Protocol::MAIL ? 'owner-id' : 'author-id';
$condition = [
$contact_field => $request['cid'],
'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT],
];
if (empty($contact['network']) || in_array($contact['network'], Protocol::FEDERATED)) {
$condition = DBA::mergeConditions($condition, ['(`uid` = 0 OR (`uid` = ? AND NOT `global`))', DI::userSession()->getLocalUserId()]);
} else {
$condition['uid'] = DI::userSession()->getLocalUserId();
}
if (DI::mode()->isMobile()) {
$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network',
DI::config()->get('system', 'itemspage_network_mobile'));
} else {
$itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network',
DI::config()->get('system', 'itemspage_network'));
}
$pager = new Pager(DI::l10n(), DI::args()->getQueryString(), $itemsPerPage);
$params = ['order' => ['received' => true], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
$fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']);
$items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), $fields, $condition, $params));
$formSecurityToken = BaseModule::getFormSecurityToken('contact_action');
$threads = DI::conversation()->getContextLessThreadList($items, ConversationContent::MODE_CONTACT_POSTS, false, false, $formSecurityToken);
}
$tpl = Renderer::getMarkupTemplate('moderation/report/create/pick_posts.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Create Moderation Report'),
'page' => $this->t('Pick Posts'),
'description' => $this->t('Please optionally pick posts to attach to your report.'),
'submit' => $this->t('Submit'),
],
'$threads' => $threads,
]);
}
private function summary(array $request): string
{
$this->page['aside'] = '';
$contact = Contact::getById($request['cid'], ['url']);
$tpl = Renderer::getMarkupTemplate('moderation/report/create/summary.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Create Moderation Report'),
'page' => $this->t('Summary'),
'submit' => $this->t('Submit Report'),
'contact_action_title' => $this->t('Further Action'),
'contact_action_desc' => $this->t('You can also perform one of the following action on the contact you reported:'),
],
'$cid' => $request['cid'],
'$category' => $request['category'],
'$ruleIds' => implode(',', $request['rule-ids'] ?? []),
'$uriIds' => implode(',', $request['uri-ids'] ?? []),
'$nothing' => ['contact_action', $this->t('Nothing'), self::CONTACT_ACTION_NONE, '', true],
'$collapse' => ['contact_action', $this->t('Collapse contact'), self::CONTACT_ACTION_COLLAPSE, $this->t('Their posts and replies will keep appearing in your Network page but their content will be collapsed by default.')],
'$ignore' => ['contact_action', $this->t('Ignore contact'), self::CONTACT_ACTION_IGNORE, $this->t("Their posts won't appear in your Network page anymore, but their replies can appear in forum threads. They still can follow you.")],
'$block' => ['contact_action', $this->t('Block contact'), self::CONTACT_ACTION_BLOCK, $this->t("Their posts won't appear in your Network page anymore, but their replies can appear in forum threads, with their content collapsed by default. They cannot follow you but still can have access to your public posts by other means.")],
'$display_forward' => !Network::isLocalLink($contact['url']),
'$forward' => ['report_forward', $this->t('Forward report'), self::CONTACT_ACTION_BLOCK, $this->t('Would you ike to forward this report to the remote server?')],
'$summary' => $this->getAside($request),
]);
}
private function getAside(array $request): string
{
$contact = null;
if (!empty($request['cid'])) {
$contact = Contact::getById($request['cid']);
}
switch ($request['category'] ?? 0) {
case Report::CATEGORY_SPAM: $category = $this->t('Spam'); break;
case Report::CATEGORY_ILLEGAL: $category = $this->t('Illegal Content'); break;
case Report::CATEGORY_SAFETY: $category = $this->t('Community Safety'); break;
case Report::CATEGORY_UNWANTED: $category = $this->t('Unwanted Content/Behavior'); break;
case Report::CATEGORY_VIOLATION: $category = $this->t('Rules Violation'); break;
case Report::CATEGORY_OTHER: $category = $this->t('Other'); break;
default: $category = '';
}
if (!empty($request['rule-ids'])) {
$rules = array_filter(System::getRules(true), function ($rule_id) use ($request) {
return in_array($rule_id, $request['rule-ids']);
}, ARRAY_FILTER_USE_KEY);
}
$tpl = Renderer::getMarkupTemplate('moderation/report/create/aside.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'contact_title' => $this->t('1. Pick a contact'),
'category_title' => $this->t('2. Pick a category'),
'rules_title' => $this->t('2a. Pick rules'),
'comment_title' => $this->t('2b. Add comment'),
'posts_title' => $this->t('3. Pick posts'),
],
'$contact' => $contact,
'$category' => $category,
'$rules' => $rules ?? [],
'$comment' => BBCode::convert($this->session->get('report_comment') ?? '', false, ),
'$posts' => count($request['uri-ids']),
]);
}
}

View file

@ -255,6 +255,7 @@ class Post
$block = false;
$ignore = false;
$collapse = false;
$report = false;
if (DI::userSession()->getLocalUserId()) {
$drop = [
'dropping' => $dropping,
@ -280,6 +281,10 @@ class Post
'collapse' => DI::l10n()->t('Collapse %s', $item['author-name']),
'author_id' => $item['author-id'],
];
$report = [
'label' => DI::l10n()->t('Report post'),
'href' => 'moderation/report/create?' . http_build_query(['cid' => $item['author-id'], 'uri-ids' => [$item['uri-id']]]),
];
}
$filer = DI::userSession()->getLocalUserId() ? DI::l10n()->t('Save to folder') : false;
@ -554,6 +559,7 @@ class Post
'block' => $block,
'ignore_author' => $ignore,
'collapse' => $collapse,
'report' => $report,
'vote' => $buttons,
'like_html' => $responses['like']['output'],
'dislike_html' => $responses['dislike']['output'],

View file

@ -508,6 +508,8 @@ return [
'/item/delete' => [Module\Moderation\Item\Delete::class, [R::GET, R::POST]],
'/item/source[/{guid}]' => [Module\Moderation\Item\Source::class, [R::GET, R::POST]],
'/report/create' => [Module\Moderation\Report\Create::class, [R::GET, R::POST]],
'/users[/{action}/{uid}]' => [Module\Moderation\Users\Index::class, [R::GET, R::POST]],
'/users/active[/{action}/{uid}]' => [Module\Moderation\Users\Active::class, [R::GET, R::POST]],
'/users/pending[/{action}/{uid}]' => [Module\Moderation\Users\Pending::class, [R::GET, R::POST]],

View file

@ -0,0 +1,25 @@
<div class="widget">
<p><strong>{{$l10n.contact_title}}</strong></p>
{{if $contact}}
{{include file="contact/entry.tpl"}}
{{/if}}
<p><strong>{{$l10n.category_title}}</strong></p>
{{if $category}}
<p>{{$category}}</p>
{{/if}}
{{if $rules}}
<p><strong>{{$l10n.rules_title}}</strong></p>
<ol>
{{foreach $rules as $rule_id => $rule_text}}
<li value="{{$rule_id}}">{{$rule_text}}</li>
{{/foreach}}
</ol>
{{/if}}
{{if $comment}}
<p><strong>{{$l10n.comment_title}}</strong></p>
<p>{{$comment nofilter}}</p>
{{/if}}
{{if $posts}}
<p><strong>{{$l10n.posts_title}} ({{$posts}})</strong></p>
{{/if}}
</div>

View file

@ -0,0 +1,16 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}} - {{$l10n.page}}</h1>
<p>{{$l10n.description}}</p>
<form action="" method="post">
{{include file="field_radio.tpl" field=$category_spam}}
{{include file="field_radio.tpl" field=$category_illegal}}
{{include file="field_radio.tpl" field=$category_safety}}
{{include file="field_radio.tpl" field=$category_unwanted}}
{{include file="field_radio.tpl" field=$category_violation}}
{{include file="field_radio.tpl" field=$category_other}}
{{include file="field_textarea.tpl" field=$comment}}
<p><button type="submit" class="btn btn-primary">{{$l10n.submit}}</button></p>
</form>
</div>

View file

@ -0,0 +1,9 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}} - {{$l10n.page}}</h1>
<p>{{$l10n.description}}</p>
<form action="" method="post">
{{include file="field_input.tpl" field=$url}}
<p><button type="submit" class="btn btn-primary">{{$l10n.submit}}</button></p>
</form>
</div>

View file

@ -0,0 +1,24 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}} - {{$l10n.page}}</h1>
<p>{{$l10n.description}}</p>
<form action="" method="post">
<table class="table-striped table-condensed">
{{foreach $threads as $thread}}
<tr>
<td>
<div id="tread-wrapper-{{$thread.id}}" class="tread-wrapper panel toplevel_item">
{{foreach $thread.items as $item}}
{{include file="{{$item.template}}"}}
{{/foreach}}
</div>
</td>
<td>
<input type="checkbox" name="uri-ids[]" value="{{$thread.items[0].uriid}}">
</td>
</tr>
{{/foreach}}
</table>
<p><button type="submit" class="btn btn-primary">{{$l10n.submit}}</button></p>
</form>
</div>

View file

@ -0,0 +1,16 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}} - {{$l10n.page}}</h1>
<p>{{$l10n.description}}</p>
<form action="" method="post">
{{foreach $rules as $rule}}
<div class="field checkbox" id="div_id_{{$rule.0}}_{{$rule.1}}">
<input type="checkbox" name="{{$rule.0}}" id="id_{{$rule.0}}_{{$rule.1}}" value="{{$rule.1}}" {{if $rule.3}}checked="checked"{{/if}}>
<label for="id_{{$rule.0}}_{{$rule.1}}">
{{$rule.2}}
</label>
</div>
{{/foreach}}
<p><button type="submit" class="btn btn-primary">{{$l10n.submit}}</button></p>
</form>
</div>

View file

@ -0,0 +1,28 @@
<div class="generic-page-wrapper">
<h1>{{$l10n.title}} - {{$l10n.page}}</h1>
<p>{{$l10n.description}}</p>
<div>
{{$summary nofilter}}
</div>
<h2>{{$l10n.contact_action_title}}</h2>
<p>{{$l10n.contact_action_desc}}</p>
<form action="" method="post">
<input type="hidden" name="cid" value="{{$cid}}">
<input type="hidden" name="category" value="{{$category}}">
<input type="hidden" name="rule-ids" value="{{$ruleIds}}">
<input type="hidden" name="uri-ids" value="{{$uriIds}}">
{{include file="field_radio.tpl" field=$nothing}}
{{include file="field_radio.tpl" field=$collapse}}
{{include file="field_radio.tpl" field=$ignore}}
{{include file="field_radio.tpl" field=$block}}
{{if $display_forward}}
{{include file="field_checkbox.tpl" field=$forward}}
{{/if}}
<p><button type="submit" name="report_create" class="btn btn-primary">{{$l10n.submit}}</button></p>
</form>
</div>

View file

@ -161,13 +161,15 @@
</div>
<div class="wall-item-actions-tools">
{{if $item.drop && $item.drop.pagedrop}}
<input type="checkbox" title="{{$item.drop.select}}" name="itemselected[]" class="item-select" value="{{$item.id}}" />
{{/if}}
{{if $item.drop && $item.drop.dropping}}
<a role="button" href="item/drop/{{$item.id}}/{{$item.return}}" onclick="return confirmDelete();" title="{{$item.drop.delete}}"><i class="icon-trash icon-large"><span class="sr-only">{{$item.drop.delete}}</span></i></a>
{{/if}}
{{if $item.report}}
<a href="{{$item.report.href}}" title="{{$item.report.label}}"><i class="icon-flag icon-large"><span class="sr-only">{{$item.report.label}}</span></i></a>
{{/if}}
{{if $item.edpost}}
<a role="button" href="{{$item.edpost.0}}" title="{{$item.edpost.1}}"><i class="icon-edit icon-large"><span class="sr-only">{{$item.edpost.1}}</span></i></a>
{{/if}}

View file

@ -409,6 +409,11 @@ as the value of $top_child_total (this is done at the end of this file)
<a class="btn-link navicon collapse" href="javascript:collapseAuthor('item/collapse/{{$item.id}}/{{$item.return}}', 'item-{{$item.guid}}');" title="{{$item.collapse.collapse}}"><i class="fa fa-ban" aria-hidden="true"></i> {{$item.collapse.collapse}}</a>
</li>
{{/if}}
{{if $item.report}}
<li role="menuitem">
<a class="btn-link navicon ignore" href="{{$item.report.href}}"><i class="fa fa-flag" aria-hidden="true"></i> {{$item.report.label}}</a>
</li>
{{/if}}
</ul>
</span>
</span>