diff --git a/src/Moderation/Factory/Report.php b/src/Moderation/Factory/Report.php index 1c1e9cfb7f..4d2e0bb75b 100644 --- a/src/Moderation/Factory/Report.php +++ b/src/Moderation/Factory/Report.php @@ -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)), + ); + } } diff --git a/src/Moderation/Repository/Report.php b/src/Moderation/Repository/Report.php index b4f2bf2dea..4681ea3b98 100644 --- a/src/Moderation/Repository/Report.php +++ b/src/Moderation/Repository/Report.php @@ -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; diff --git a/src/Module/Moderation/Report/Create.php b/src/Module/Moderation/Report/Create.php new file mode 100644 index 0000000000..0e0e4c925e --- /dev/null +++ b/src/Module/Moderation/Report/Create.php @@ -0,0 +1,344 @@ +. + * + */ + +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']), + ]); + } +} diff --git a/src/Object/Post.php b/src/Object/Post.php index 28db238949..4c377e946c 100644 --- a/src/Object/Post.php +++ b/src/Object/Post.php @@ -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'], diff --git a/static/routes.config.php b/static/routes.config.php index 157f141b16..71595e5a82 100644 --- a/static/routes.config.php +++ b/static/routes.config.php @@ -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]], diff --git a/view/templates/moderation/report/create/aside.tpl b/view/templates/moderation/report/create/aside.tpl new file mode 100644 index 0000000000..f590433f89 --- /dev/null +++ b/view/templates/moderation/report/create/aside.tpl @@ -0,0 +1,25 @@ +
+

{{$l10n.contact_title}}

+{{if $contact}} + {{include file="contact/entry.tpl"}} +{{/if}} +

{{$l10n.category_title}}

+{{if $category}} +

{{$category}}

+{{/if}} +{{if $rules}} +

{{$l10n.rules_title}}

+
    +{{foreach $rules as $rule_id => $rule_text}} +
  1. {{$rule_text}}
  2. +{{/foreach}} +
+{{/if}} +{{if $comment}} +

{{$l10n.comment_title}}

+

{{$comment nofilter}}

+{{/if}} +{{if $posts}} +

{{$l10n.posts_title}} ({{$posts}})

+{{/if}} +
\ No newline at end of file diff --git a/view/templates/moderation/report/create/pick_category.tpl b/view/templates/moderation/report/create/pick_category.tpl new file mode 100644 index 0000000000..66d4a275bd --- /dev/null +++ b/view/templates/moderation/report/create/pick_category.tpl @@ -0,0 +1,16 @@ +
+

{{$l10n.title}} - {{$l10n.page}}

+

{{$l10n.description}}

+ +
+ {{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}} +

+
+
diff --git a/view/templates/moderation/report/create/pick_contact.tpl b/view/templates/moderation/report/create/pick_contact.tpl new file mode 100644 index 0000000000..38c41e7f17 --- /dev/null +++ b/view/templates/moderation/report/create/pick_contact.tpl @@ -0,0 +1,9 @@ +
+

{{$l10n.title}} - {{$l10n.page}}

+

{{$l10n.description}}

+ +
+ {{include file="field_input.tpl" field=$url}} +

+
+
diff --git a/view/templates/moderation/report/create/pick_posts.tpl b/view/templates/moderation/report/create/pick_posts.tpl new file mode 100644 index 0000000000..4dca61a8dc --- /dev/null +++ b/view/templates/moderation/report/create/pick_posts.tpl @@ -0,0 +1,24 @@ +
+

{{$l10n.title}} - {{$l10n.page}}

+

{{$l10n.description}}

+ +
+ +{{foreach $threads as $thread}} + + + + +{{/foreach}} +
+
+ {{foreach $thread.items as $item}} + {{include file="{{$item.template}}"}} + {{/foreach}} +
+
+ +
+

+
+
diff --git a/view/templates/moderation/report/create/pick_rules.tpl b/view/templates/moderation/report/create/pick_rules.tpl new file mode 100644 index 0000000000..e9996c386b --- /dev/null +++ b/view/templates/moderation/report/create/pick_rules.tpl @@ -0,0 +1,16 @@ +
+

{{$l10n.title}} - {{$l10n.page}}

+

{{$l10n.description}}

+ +
+ {{foreach $rules as $rule}} +
+ + +
+ {{/foreach}} +

+
+
diff --git a/view/templates/moderation/report/create/summary.tpl b/view/templates/moderation/report/create/summary.tpl new file mode 100644 index 0000000000..3de5206811 --- /dev/null +++ b/view/templates/moderation/report/create/summary.tpl @@ -0,0 +1,28 @@ +
+

{{$l10n.title}} - {{$l10n.page}}

+

{{$l10n.description}}

+ +
+ {{$summary nofilter}} +
+ +

{{$l10n.contact_action_title}}

+

{{$l10n.contact_action_desc}}

+
+ + + + + + {{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}} + +

+
+
diff --git a/view/templates/wall_thread.tpl b/view/templates/wall_thread.tpl index 5e9dd96d61..74e6097b37 100644 --- a/view/templates/wall_thread.tpl +++ b/view/templates/wall_thread.tpl @@ -161,13 +161,15 @@
- {{if $item.drop && $item.drop.pagedrop}} {{/if}} {{if $item.drop && $item.drop.dropping}} {{$item.drop.delete}} {{/if}} + {{if $item.report}} + {{$item.report.label}} + {{/if}} {{if $item.edpost}} {{$item.edpost.1}} {{/if}} diff --git a/view/theme/frio/templates/wall_thread.tpl b/view/theme/frio/templates/wall_thread.tpl index 3e9d19e02d..21aac60e8a 100644 --- a/view/theme/frio/templates/wall_thread.tpl +++ b/view/theme/frio/templates/wall_thread.tpl @@ -409,6 +409,11 @@ as the value of $top_child_total (this is done at the end of this file) {{$item.collapse.collapse}} {{/if}} + {{if $item.report}} +
  • + {{$item.report.label}} +
  • + {{/if}}