From e445975c2033c339fb9d1c0acf3bd11a35a2f7f6 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 27 Jul 2022 19:54:02 -0400 Subject: [PATCH 1/5] Use DBA::quoteIdentifier in Database::escapeFields --- src/Database/DBA.php | 4 ++-- src/Database/Database.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Database/DBA.php b/src/Database/DBA.php index 9ce2b61473..677bf1a27f 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -531,9 +531,9 @@ class DBA } /** - * Escape an identifier (table or field name) optional with a schema like (schema.)table + * Escape an identifier (table or field name) optional with a schema like ((schema.)table.)field * - * @param $identifier Table, field name + * @param string $identifier Table, field name * @return string Quotes table or field name */ public static function quoteIdentifier(string $identifier): string diff --git a/src/Database/Database.php b/src/Database/Database.php index 3276e90e52..cd2803c0fc 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1438,7 +1438,7 @@ class Database array_walk($fields, function(&$value, $key) use ($options) { $field = $value; - $value = '`' . str_replace('`', '``', $value) . '`'; + $value = DBA::quoteIdentifier($field); if (!empty($options['group_by']) && !in_array($field, $options['group_by'])) { $value = 'ANY_VALUE(' . $value . ') AS ' . $value; From 1810b32c26683bb94fee10de2ffe18de1a7e85bf Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 27 Jul 2022 15:36:20 -0400 Subject: [PATCH 2/5] Move server domain pattern blocklist features to its own class - Update tests --- src/Console/ServerBlock.php | 237 +++++++------------ src/Moderation/DomainPatternBlocklist.php | 180 ++++++++++++++ src/Module/Admin/Blocklist/Server/Add.php | 94 +++++--- src/Module/Admin/Blocklist/Server/Import.php | 61 ++--- src/Module/Admin/Blocklist/Server/Index.php | 83 ++++--- src/Module/Blocklist/Domain/Download.php | 29 +-- tests/src/Console/ServerBlockConsoleTest.php | 195 +++++---------- 7 files changed, 458 insertions(+), 421 deletions(-) create mode 100644 src/Moderation/DomainPatternBlocklist.php diff --git a/src/Console/ServerBlock.php b/src/Console/ServerBlock.php index c25b57ae79..9ef6777db7 100644 --- a/src/Console/ServerBlock.php +++ b/src/Console/ServerBlock.php @@ -24,7 +24,7 @@ namespace Friendica\Console; use Asika\SimpleConsole\CommandArgsException; use Asika\SimpleConsole\Console; use Console_Table; -use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Moderation\DomainPatternBlocklist; /** * Manage blocked servers @@ -34,18 +34,14 @@ use Friendica\Core\Config\Capability\IManageConfigValues; */ class ServerBlock extends Console { - const DEFAULT_REASON = 'blocked'; - protected $helpOptions = ['h', 'help', '?']; - /** - * @var IManageConfigValues - */ - private $config; + /** @var DomainPatternBlocklist */ + private $blocklist; - protected function getHelp() + protected function getHelp(): string { - $help = <<config = $config; + $this->blocklist = $blocklist; } protected function doExecute(): int { if (count($this->args) == 0) { - $this->printBlockedServers($this->config); + $this->printBlockedServers(); return 0; } switch ($this->getArgument(0)) { case 'add': - return $this->addBlockedServer($this->config); + return $this->addBlockedServer(); case 'remove': - return $this->removeBlockedServer($this->config); + return $this->removeBlockedServer(); case 'export': - return $this->exportBlockedServers($this->config); + return $this->exportBlockedServers(); case 'import': - return $this->importBlockedServers($this->config); + return $this->importBlockedServers(); default: throw new CommandArgsException('Unknown command.'); - break; } } /** - * Exports the list of blocked domains including the reason for the + * Exports the list of blocked domain patterns including the reason for the * block to a CSV file. * - * @param IManageConfigValues $config + * @return int + * @throws \Exception */ - private function exportBlockedServers(IManageConfigValues $config) + private function exportBlockedServers(): int { $filename = $this->getArgument(1); - $blocklist = $config->get('system', 'blocklist', []); - $fp = fopen($filename, 'w'); - if (!$fp) { - throw new Exception(sprintf('The file "%s" could not be created.', $filename)); - } - foreach ($blocklist as $domain) { - fputcsv($fp, $domain); - } + + $this->blocklist->exportToFile($filename); // Success return 0; } + /** - * Imports a list of domains and a reason for the block from a CSV + * Imports a list of domain patterns and a reason for the block from a CSV * file, e.g. created with the export function. * - * @param IManageConfigValues $config + * @return int + * @throws \Exception */ - private function importBlockedServers(IManageConfigValues $config) + private function importBlockedServers(): int { $filename = $this->getArgument(1); - $currBlockList = $config->get('system', 'blocklist', []); - $newBlockList = []; - if (($fp = fopen($filename, 'r')) !== false) { - while (($data = fgetcsv($fp, 1000, ',')) !== false) { - $domain = $data[0]; - if (count($data) == 0) { - $reason = self::DEFAULT_REASON; - } else { - $reason = $data[1]; - } - $data = [ - 'domain' => $domain, - 'reason' => $reason - ]; - if (!in_array($data, $newBlockList)) { - $newBlockList[] = $data; - } - } + $newBlockList = $this->blocklist::extractFromCSVFile($filename); - foreach ($currBlockList as $blocked) { - if (!in_array($blocked, $newBlockList)) { - $newBlockList[] = $blocked; - } - } - - if ($config->set('system', 'blocklist', $newBlockList)) { - $this->out(sprintf("Entries from %s that were not blocked before are now blocked", $filename)); - return 0; - } else { - $this->out(sprintf("Couldn't save '%s' as blocked server", $domain)); - return 1; - } - } else { - throw new Exception(sprintf('The file "%s" could not be opened for importing', $filename)); - } - } - - /** - * Prints the whole list of blocked domains including the reason - * - * @param IManageConfigValues $config - */ - private function printBlockedServers(IManageConfigValues $config) - { - $table = new Console_Table(); - $table->setHeaders(['Domain', 'Reason']); - $blocklist = $config->get('system', 'blocklist', []); - foreach ($blocklist as $domain) { - $table->addRow($domain); - } - $this->out($table->getTable()); - - // Success - return 0; - } - - /** - * Adds a server to the blocked list - * - * @param IManageConfigValues $config - * - * @return int The return code (0 = success, 1 = failed) - */ - private function addBlockedServer(IManageConfigValues $config) - { - if (count($this->args) < 2 || count($this->args) > 3) { - throw new CommandArgsException('Add needs a domain and optional a reason.'); - } - - $domain = $this->getArgument(1); - $reason = (count($this->args) === 3) ? $this->getArgument(2) : self::DEFAULT_REASON; - - $update = false; - - $currBlockList = $config->get('system', 'blocklist', []); - $newBlockList = []; - foreach ($currBlockList as $blocked) { - if ($blocked['domain'] === $domain) { - $update = true; - $newBlockList[] = [ - 'domain' => $domain, - 'reason' => $reason, - ]; - } else { - $newBlockList[] = $blocked; - } - } - - if (!$update) { - $newBlockList[] = [ - 'domain' => $domain, - 'reason' => $reason, - ]; - } - - if ($config->set('system', 'blocklist', $newBlockList)) { - if ($update) { - $this->out(sprintf("The domain '%s' is now updated. (Reason: '%s')", $domain, $reason)); - } else { - $this->out(sprintf("The domain '%s' is now blocked. (Reason: '%s')", $domain, $reason)); - } + if ($this->blocklist->append($newBlockList)) { + $this->out(sprintf("Entries from %s that were not blocked before are now blocked", $filename)); return 0; } else { - $this->out(sprintf("Couldn't save '%s' as blocked server", $domain)); + $this->out("Couldn't save the block list"); return 1; } } /** - * Removes a server from the blocked list - * - * @param IManageConfigValues $config + * Prints the whole list of blocked domain patterns including the reason + */ + private function printBlockedServers(): void + { + $table = new Console_Table(); + $table->setHeaders(['Pattern', 'Reason']); + foreach ($this->blocklist->get() as $pattern) { + $table->addRow($pattern); + } + + $this->out($table->getTable()); + } + + /** + * Adds a domain pattern to the block list * * @return int The return code (0 = success, 1 = failed) */ - private function removeBlockedServer(IManageConfigValues $config) + private function addBlockedServer(): int + { + if (count($this->args) < 2 || count($this->args) > 3) { + throw new CommandArgsException('Add needs a domain pattern and optionally a reason.'); + } + + $pattern = $this->getArgument(1); + $reason = (count($this->args) === 3) ? $this->getArgument(2) : DomainPatternBlocklist::DEFAULT_REASON; + + $result = $this->blocklist->addPattern($pattern, $reason); + if ($result) { + if ($result == 2) { + $this->out(sprintf("The domain pattern '%s' is now updated. (Reason: '%s')", $pattern, $reason)); + } else { + $this->out(sprintf("The domain pattern '%s' is now blocked. (Reason: '%s')", $pattern, $reason)); + } + return 0; + } else { + $this->out(sprintf("Couldn't save '%s' as blocked domain pattern", $pattern)); + return 1; + } + } + + /** + * Removes a domain pattern from the block list + * + * @return int The return code (0 = success, 1 = failed) + */ + private function removeBlockedServer(): int { if (count($this->args) !== 2) { throw new CommandArgsException('Remove needs a second parameter.'); } - $domain = $this->getArgument(1); + $pattern = $this->getArgument(1); - $found = false; - - $currBlockList = $config->get('system', 'blocklist', []); - $newBlockList = []; - foreach ($currBlockList as $blocked) { - if ($blocked['domain'] === $domain) { - $found = true; + $result = $this->blocklist->removePattern($pattern); + if ($result) { + if ($result == 2) { + $this->out(sprintf("The domain pattern '%s' isn't blocked anymore", $pattern)); + return 0; } else { - $newBlockList[] = $blocked; + $this->out(sprintf("The domain pattern '%s' wasn't blocked.", $pattern)); + return 1; } - } - - if (!$found) { - $this->out(sprintf("The domain '%s' is not blocked.", $domain)); - return 1; - } - - if ($config->set('system', 'blocklist', $newBlockList)) { - $this->out(sprintf("The domain '%s' is not more blocked", $domain)); - return 0; } else { - $this->out(sprintf("Couldn't remove '%s' from blocked servers", $domain)); + $this->out(sprintf("Couldn't remove '%s' from blocked domain patterns", $pattern)); return 1; } } diff --git a/src/Moderation/DomainPatternBlocklist.php b/src/Moderation/DomainPatternBlocklist.php new file mode 100644 index 0000000000..33bd189d6d --- /dev/null +++ b/src/Moderation/DomainPatternBlocklist.php @@ -0,0 +1,180 @@ +. + * + */ + +namespace Friendica\Moderation; + +use Exception; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; +use Friendica\Database\Database; +use Friendica\Network\HTTPException; +use Friendica\Util\Emailer; + +class DomainPatternBlocklist +{ + const DEFAULT_REASON = 'blocked'; + + /** + * @var IManageConfigValues + */ + private $config; + + public function __construct(IManageConfigValues $config) + { + $this->config = $config; + } + + public function get(): array + { + return $this->config->get('system', 'blocklist', []); + } + + public function set(array $blocklist): bool + { + return $this->config->set('system', 'blocklist', $blocklist); + } + + /** + * @param string $pattern + * @param string|null $reason + * @return int 0 if the block list couldn't be saved, 1 if the pattern was added, 2 if it was updated in place + */ + public function addPattern(string $pattern, string $reason = null): int + { + $update = false; + + $blocklist = []; + foreach ($this->get() as $blocked) { + if ($blocked['domain'] === $pattern) { + $blocklist[] = [ + 'domain' => $pattern, + 'reason' => $reason ?? self::DEFAULT_REASON, + ]; + + $update = true; + } else { + $blocklist[] = $blocked; + } + } + + if (!$update) { + $blocklist[] = [ + 'domain' => $pattern, + 'reason' => $reason ?? self::DEFAULT_REASON, + ]; + } + + return $this->set($blocklist) ? ($update ? 2 : 1) : 0; + } + + /** + * @param string $pattern + * @return int 0 if the block list couldn't be saved, 1 if the pattern wasn't found, 2 if it was removed + */ + public function removePattern(string $pattern): int + { + $found = false; + + $blocklist = []; + foreach ($this->get() as $blocked) { + if ($blocked['domain'] === $pattern) { + $found = true; + } else { + $blocklist[] = $blocked; + } + } + + return $found ? ($this->set($blocklist) ? 2 : 0) : 1; + } + + public function exportToFile(string $filename) + { + $fp = fopen($filename, 'w'); + if (!$fp) { + throw new Exception(sprintf('The file "%s" could not be created.', $filename)); + } + + foreach ($this->get() as $domain) { + fputcsv($fp, $domain); + } + } + + /** + * Appends to the local block list all the patterns from the provided list that weren't already present. + * + * @param array $blocklist + * @return int The number of patterns actually added to the block list + */ + public function append(array $blocklist): int + { + $localBlocklist = $this->get(); + $localPatterns = array_column($localBlocklist, 'domain'); + + $importedPatterns = array_column($blocklist, 'domain'); + + $patternsToAppend = array_diff($importedPatterns, $localPatterns); + + if (count($patternsToAppend)) { + foreach (array_keys($patternsToAppend) as $key) { + $localBlocklist[] = $blocklist[$key]; + } + + $this->set($localBlocklist); + } + + return count($patternsToAppend); + } + + /** + * Extracts a server domain pattern block list from the provided CSV file name. Deduplicates the list based on patterns. + * + * @param string $filename + * @return array + * @throws Exception + */ + public static function extractFromCSVFile(string $filename): array + { + $fp = fopen($filename, 'r'); + if ($fp === false) { + throw new Exception(sprintf('The file "%s" could not be opened for importing', $filename)); + } + + $blocklist = []; + while (($data = fgetcsv($fp, 1000)) !== false) { + $domain = $data[0]; + if (count($data) == 0) { + $reason = self::DEFAULT_REASON; + } else { + $reason = $data[1]; + } + + $data = [ + 'domain' => $domain, + 'reason' => $reason + ]; + if (!in_array($data, $blocklist)) { + $blocklist[] = $data; + } + } + + return $blocklist; + } +} diff --git a/src/Module/Admin/Blocklist/Server/Add.php b/src/Module/Admin/Blocklist/Server/Add.php index a11a8ab21f..a4aaa4b65c 100644 --- a/src/Module/Admin/Blocklist/Server/Add.php +++ b/src/Module/Admin/Blocklist/Server/Add.php @@ -21,58 +21,90 @@ namespace Friendica\Module\Admin\Blocklist\Server; +use Friendica\App; use Friendica\Content\ContactSelector; +use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Core\Worker; -use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\GServer; +use Friendica\Moderation\DomainPatternBlocklist; use Friendica\Module\BaseAdmin; +use Friendica\Module\Response; +use Friendica\Navigation\SystemMessages; +use Friendica\Util\Profiler; use GuzzleHttp\Psr7\Uri; +use Psr\Log\LoggerInterface; class Add extends BaseAdmin { + /** @var SystemMessages */ + private $sysmsg; + + /** @var DomainPatternBlocklist */ + private $blocklist; + + public function __construct(SystemMessages $sysmsg, DomainPatternBlocklist $blocklist, 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->sysmsg = $sysmsg; + $this->blocklist = $blocklist; + } + + /** + * @param array $request + * @return void + * @throws \Friendica\Network\HTTPException\ForbiddenException + * @throws \Friendica\Network\HTTPException\FoundException + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Friendica\Network\HTTPException\MovedPermanentlyException + * @throws \Friendica\Network\HTTPException\TemporaryRedirectException + * @throws \Exception + */ protected function post(array $request = []) { self::checkAdminAccess(); - if (empty($_POST['page_blocklist_add'])) { + if (empty($request['page_blocklist_add'])) { return; } self::checkFormSecurityTokenRedirectOnError('/admin/blocklist/server/add', 'admin_blocklist_add'); + $pattern = trim($request['pattern']); + // Add new item to blocklist - $domain = trim($_POST['pattern']); + $this->blocklist->addPattern($pattern, trim($request['reason'])); - $blocklist = DI::config()->get('system', 'blocklist'); - $blocklist[] = [ - 'domain' => $domain, - 'reason' => trim($_POST['reason']), - ]; - DI::config()->set('system', 'blocklist', $blocklist); + $this->sysmsg->addInfo($this->l10n->t('Server domain pattern added to the blocklist.')); - info(DI::l10n()->t('Server domain pattern added to the blocklist.')); - - if (!empty($_POST['purge'])) { - $gservers = GServer::listByDomainPattern($domain); + if (!empty($request['purge'])) { + $gservers = GServer::listByDomainPattern($pattern); foreach (Contact::selectToArray(['id'], ['gsid' => array_column($gservers, 'id')]) as $contact) { Worker::add(PRIORITY_LOW, 'Contact\RemoveContent', $contact['id']); } - info(DI::l10n()->tt('%s server scheduled to be purged.', '%s servers scheduled to be purged.', count($gservers))); + $this->sysmsg->addInfo($this->l10n->tt('%s server scheduled to be purged.', '%s servers scheduled to be purged.', count($gservers))); } - DI::baseUrl()->redirect('admin/blocklist/server'); + $this->baseUrl->redirect('admin/blocklist/server'); } + /** + * @param array $request + * @return string + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \Friendica\Network\HTTPException\ServiceUnavailableException + * @throws \Exception + */ protected function content(array $request = []): string { parent::content(); $gservers = []; - if ($pattern = trim($_REQUEST['pattern'] ?? '')) { + if ($pattern = trim($request['pattern'] ?? '')) { $gservers = GServer::listByDomainPattern($pattern); } @@ -85,28 +117,28 @@ class Add extends BaseAdmin $t = Renderer::getMarkupTemplate('admin/blocklist/server/add.tpl'); return Renderer::replaceMacros($t, [ '$l10n' => [ - 'return_list' => DI::l10n()->t('← Return to the list'), - 'title' => DI::l10n()->t('Administration'), - 'page' => DI::l10n()->t('Block A New Server Domain Pattern'), - 'syntax' => DI::l10n()->t('

The server domain pattern syntax is case-insensitive shell wildcard, comprising the following special characters:

+ 'return_list' => $this->l10n->t('← Return to the list'), + 'title' => $this->l10n->t('Administration'), + 'page' => $this->l10n->t('Block A New Server Domain Pattern'), + 'syntax' => $this->l10n->t('

The server domain pattern syntax is case-insensitive shell wildcard, comprising the following special characters:

  • *: Any number of characters
  • ?: Any single character
'), - 'submit' => DI::l10n()->t('Check pattern'), - 'matching_servers' => DI::l10n()->t('Matching known servers'), - 'server_name' => DI::l10n()->t('Server Name'), - 'server_domain' => DI::l10n()->t('Server Domain'), - 'known_contacts' => DI::l10n()->t('Known Contacts'), - 'server_count' => DI::l10n()->tt('%d known server', '%d known servers', count($gservers)), - 'add_pattern' => DI::l10n()->t('Add pattern to the blocklist'), + 'submit' => $this->l10n->t('Check pattern'), + 'matching_servers' => $this->l10n->t('Matching known servers'), + 'server_name' => $this->l10n->t('Server Name'), + 'server_domain' => $this->l10n->t('Server Domain'), + 'known_contacts' => $this->l10n->t('Known Contacts'), + 'server_count' => $this->l10n->tt('%d known server', '%d known servers', count($gservers)), + 'add_pattern' => $this->l10n->t('Add pattern to the blocklist'), ], - '$newdomain' => ['pattern', DI::l10n()->t('Server Domain Pattern'), $pattern, DI::l10n()->t('The domain pattern of the new server to add to the blocklist. Do not include the protocol.'), DI::l10n()->t('Required'), '', ''], - '$newpurge' => ['purge', DI::l10n()->t('Purge server'), $_REQUEST['purge'] ?? false, DI::l10n()->tt('Also purges all the locally stored content authored by the known contacts registered on that server. Keeps the contacts and the server records. This action cannot be undone.', 'Also purges all the locally stored content authored by the known contacts registered on these servers. Keeps the contacts and the servers records. This action cannot be undone.', count($gservers))], - '$newreason' => ['reason', DI::l10n()->t('Block reason'), $_REQUEST['reason'] ?? '', DI::l10n()->t('The reason why you blocked this server domain pattern. This reason will be shown publicly in the server information page.'), DI::l10n()->t('Required'), '', ''], + '$newdomain' => ['pattern', $this->l10n->t('Server Domain Pattern'), $pattern, $this->l10n->t('The domain pattern of the new server to add to the blocklist. Do not include the protocol.'), $this->l10n->t('Required'), '', ''], + '$newpurge' => ['purge', $this->l10n->t('Purge server'), $request['purge'] ?? false, $this->l10n->tt('Also purges all the locally stored content authored by the known contacts registered on that server. Keeps the contacts and the server records. This action cannot be undone.', 'Also purges all the locally stored content authored by the known contacts registered on these servers. Keeps the contacts and the servers records. This action cannot be undone.', count($gservers))], + '$newreason' => ['reason', $this->l10n->t('Block reason'), $request['reason'] ?? '', $this->l10n->t('The reason why you blocked this server domain pattern. This reason will be shown publicly in the server information page.'), $this->l10n->t('Required'), '', ''], '$pattern' => $pattern, '$gservers' => $gservers, - '$baseurl' => DI::baseUrl()->get(true), + '$baseurl' => $this->baseUrl->get(true), '$form_security_token' => self::getFormSecurityToken('admin_blocklist_add') ]); } diff --git a/src/Module/Admin/Blocklist/Server/Import.php b/src/Module/Admin/Blocklist/Server/Import.php index 0714be5be9..166f248e99 100644 --- a/src/Module/Admin/Blocklist/Server/Import.php +++ b/src/Module/Admin/Blocklist/Server/Import.php @@ -22,9 +22,9 @@ namespace Friendica\Module\Admin\Blocklist\Server; use Friendica\App; -use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\Renderer; +use Friendica\Moderation\DomainPatternBlocklist; use Friendica\Module\Response; use Friendica\Navigation\SystemMessages; use Friendica\Util\Profiler; @@ -32,8 +32,8 @@ use Psr\Log\LoggerInterface; class Import extends \Friendica\Module\BaseAdmin { - /** @var IManageConfigValues */ - private $config; + /** @var DomainPatternBlocklist */ + private $localBlocklist; /** @var SystemMessages */ private $sysmsg; @@ -41,11 +41,11 @@ class Import extends \Friendica\Module\BaseAdmin /** @var array of blocked server domain patterns */ private $blocklist = []; - public function __construct(IManageConfigValues $config, SystemMessages $sysmsg, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(DomainPatternBlocklist $localBlocklist, SystemMessages $sysmsg, 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->config = $config; + $this->localBlocklist = $localBlocklist; $this->sysmsg = $sysmsg; } @@ -62,63 +62,36 @@ class Import extends \Friendica\Module\BaseAdmin { self::checkAdminAccess(); - if (!isset($_POST['page_blocklist_upload']) && !isset($_POST['page_blocklist_import'])) { + if (!isset($request['page_blocklist_upload']) && !isset($request['page_blocklist_import'])) { return; } self::checkFormSecurityTokenRedirectOnError('/admin/blocklist/server/import', 'admin_blocklist_import'); - if (isset($_POST['page_blocklist_upload'])) { - if (($fp = fopen($_FILES['listfile']['tmp_name'], 'r')) !== false) { - $blocklist = []; - while (($data = fgetcsv($fp, 1000, ',')) !== false) { - $domain = $data[0]; - if (count($data) == 0) { - $reason = 'blocked'; - } else { - $reason = $data[1]; - } - - $blocklist[] = [ - 'domain' => $domain, - 'reason' => $reason - ]; - } - } else { + if (isset($request['page_blocklist_upload'])) { + try { + $this->blocklist = $this->localBlocklist::extractFromCSVFile($_FILES['listfile']['tmp_name']); + } catch (\Throwable $e) { $this->sysmsg->addNotice($this->l10n->t('Error importing pattern file')); - return; } - $this->blocklist = $blocklist; - return; } - if (isset($_POST['page_blocklist_import'])) { - $blocklist = json_decode($_POST['blocklist'], true); + if (isset($request['page_blocklist_import'])) { + $blocklist = json_decode($request['blocklist'], true); if ($blocklist === null) { $this->sysmsg->addNotice($this->l10n->t('Error importing pattern file')); return; } - if (($_POST['mode'] ?? 'append') == 'replace') { - $this->config->set('system', 'blocklist', $blocklist); + if (($request['mode'] ?? 'append') == 'replace') { + $this->localBlocklist->set($blocklist); $this->sysmsg->addNotice($this->l10n->t('Local blocklist replaced with the provided file.')); } else { - $localBlocklist = $this->config->get('system', 'blocklist', []); - $localPatterns = array_column($localBlocklist, 'domain'); - - $importedPatterns = array_column($blocklist, 'domain'); - - $patternsToAppend = array_diff($importedPatterns, $localPatterns); - - if (count($patternsToAppend)) { - foreach (array_keys($patternsToAppend) as $key) { - $localBlocklist[] = $blocklist[$key]; - } - - $this->config->set('system', 'blocklist', $localBlocklist); - $this->sysmsg->addNotice($this->l10n->tt('%d pattern was added to the local blocklist.', '%d patterns were added to the local blocklist.', count($patternsToAppend))); + $count = $this->localBlocklist->append($blocklist); + if ($count) { + $this->sysmsg->addNotice($this->l10n->tt('%d pattern was added to the local blocklist.', '%d patterns were added to the local blocklist.', $count)); } else { $this->sysmsg->addNotice($this->l10n->t('No pattern was added to the local blocklist.')); } diff --git a/src/Module/Admin/Blocklist/Server/Index.php b/src/Module/Admin/Blocklist/Server/Index.php index ee6f1d4e88..9913cb38e0 100644 --- a/src/Module/Admin/Blocklist/Server/Index.php +++ b/src/Module/Admin/Blocklist/Server/Index.php @@ -21,17 +21,33 @@ namespace Friendica\Module\Admin\Blocklist\Server; +use Friendica\App; +use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Core\L10n; use Friendica\Core\Renderer; -use Friendica\DI; +use Friendica\Moderation\DomainPatternBlocklist; use Friendica\Module\BaseAdmin; +use Friendica\Module\Response; +use Friendica\Util\Profiler; +use Psr\Log\LoggerInterface; class Index extends BaseAdmin { + /** @var DomainPatternBlocklist */ + private $blocklist; + + public function __construct(DomainPatternBlocklist $blocklist, 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->blocklist = $blocklist; + } + protected function post(array $request = []) { self::checkAdminAccess(); - if (empty($_POST['page_blocklist_edit'])) { + if (empty($request['page_blocklist_edit'])) { return; } @@ -39,11 +55,11 @@ class Index extends BaseAdmin // Edit the entries from blocklist $blocklist = []; - foreach ($_POST['domain'] as $id => $domain) { + foreach ($request['domain'] as $id => $domain) { // Trimming whitespaces as well as any lingering slashes $domain = trim($domain); - $reason = trim($_POST['reason'][$id]); - if (empty($_POST['delete'][$id])) { + $reason = trim($request['reason'][$id]); + if (empty($request['delete'][$id])) { $blocklist[] = [ 'domain' => $domain, 'reason' => $reason @@ -51,54 +67,53 @@ class Index extends BaseAdmin } } - DI::config()->set('system', 'blocklist', $blocklist); + $this->blocklist->set($blocklist); - DI::baseUrl()->redirect('admin/blocklist/server'); + $ + + $this->baseUrl->redirect('admin/blocklist/server'); } protected function content(array $request = []): string { parent::content(); - $blocklist = DI::config()->get('system', 'blocklist'); $blocklistform = []; - if (is_array($blocklist)) { - foreach ($blocklist as $id => $b) { - $blocklistform[] = [ - 'domain' => ["domain[$id]", DI::l10n()->t('Blocked server domain pattern'), $b['domain'], '', DI::l10n()->t('Required'), '', ''], - 'reason' => ["reason[$id]", DI::l10n()->t("Reason for the block"), $b['reason'], '', DI::l10n()->t('Required'), '', ''], - 'delete' => ["delete[$id]", DI::l10n()->t("Delete server domain pattern") . ' (' . $b['domain'] . ')', false, DI::l10n()->t("Check to delete this entry from the blocklist")] - ]; - } + foreach ($this->blocklist->get() as $id => $b) { + $blocklistform[] = [ + 'domain' => ["domain[$id]", $this->l10n->t('Blocked server domain pattern'), $b['domain'], '', $this->l10n->t('Required'), '', ''], + 'reason' => ["reason[$id]", $this->l10n->t("Reason for the block"), $b['reason'], '', $this->l10n->t('Required'), '', ''], + 'delete' => ["delete[$id]", $this->l10n->t("Delete server domain pattern") . ' (' . $b['domain'] . ')', false, $this->l10n->t("Check to delete this entry from the blocklist")] + ]; } $t = Renderer::getMarkupTemplate('admin/blocklist/server/index.tpl'); return Renderer::replaceMacros($t, [ '$l10n' => [ - 'title' => DI::l10n()->t('Administration'), - 'page' => DI::l10n()->t('Server Domain Pattern Blocklist'), - 'intro' => DI::l10n()->t('This page can be used to define a blocklist of server domain patterns from the federated network that are not allowed to interact with your node. For each domain pattern you should also provide the reason why you block it.'), - 'public' => DI::l10n()->t('The list of blocked server domain patterns will be made publically available on the /friendica page so that your users and people investigating communication problems can find the reason easily.'), - 'syntax' => DI::l10n()->t('

The server domain pattern syntax is case-insensitive shell wildcard, comprising the following special characters:

+ 'title' => $this->l10n->t('Administration'), + 'page' => $this->l10n->t('Server Domain Pattern Blocklist'), + 'intro' => $this->l10n->t('This page can be used to define a blocklist of server domain patterns from the federated network that are not allowed to interact with your node. For each domain pattern you should also provide the reason why you block it.'), + 'public' => $this->l10n->t('The list of blocked server domain patterns will be made publically available on the /friendica page so that your users and people investigating communication problems can find the reason easily.'), + 'syntax' => $this->l10n->t('

The server domain pattern syntax is case-insensitive shell wildcard, comprising the following special characters:

  • *: Any number of characters
  • ?: Any single character
'), - 'importtitle' => DI::l10n()->t('Import server domain pattern blocklist'), - 'addtitle' => DI::l10n()->t('Add new entry to the blocklist'), - 'importsubmit' => DI::l10n()->t('Upload file'), - 'addsubmit' => DI::l10n()->t('Check pattern'), - 'savechanges' => DI::l10n()->t('Save changes to the blocklist'), - 'currenttitle' => DI::l10n()->t('Current Entries in the Blocklist'), - 'thurl' => DI::l10n()->t('Blocked server domain pattern'), - 'threason' => DI::l10n()->t('Reason for the block'), - 'delentry' => DI::l10n()->t('Delete entry from the blocklist'), - 'confirm_delete' => DI::l10n()->t('Delete entry from the blocklist?'), + 'importtitle' => $this->l10n->t('Import server domain pattern blocklist'), + 'addtitle' => $this->l10n->t('Add new entry to the blocklist'), + 'importsubmit' => $this->l10n->t('Upload file'), + 'addsubmit' => $this->l10n->t('Check pattern'), + 'savechanges' => $this->l10n->t('Save changes to the blocklist'), + 'currenttitle' => $this->l10n->t('Current Entries in the Blocklist'), + 'thurl' => $this->l10n->t('Blocked server domain pattern'), + 'threason' => $this->l10n->t('Reason for the block'), + 'delentry' => $this->l10n->t('Delete entry from the blocklist'), + 'confirm_delete' => $this->l10n->t('Delete entry from the blocklist?'), ], - '$listfile' => ['listfile', DI::l10n()->t('Server domain pattern blocklist CSV file'), '', '', DI::l10n()->t('Required'), '', 'file'], - '$newdomain' => ['pattern', DI::l10n()->t('Server Domain Pattern'), '', DI::l10n()->t('The domain pattern of the new server to add to the blocklist. Do not include the protocol.'), DI::l10n()->t('Required'), '', ''], + '$listfile' => ['listfile', $this->l10n->t('Server domain pattern blocklist CSV file'), '', '', $this->l10n->t('Required'), '', 'file'], + '$newdomain' => ['pattern', $this->l10n->t('Server Domain Pattern'), '', $this->l10n->t('The domain pattern of the new server to add to the blocklist. Do not include the protocol.'), $this->l10n->t('Required'), '', ''], '$entries' => $blocklistform, - '$baseurl' => DI::baseUrl()->get(true), + '$baseurl' => $this->baseUrl->get(true), '$form_security_token' => self::getFormSecurityToken('admin_blocklist'), '$form_security_token_import' => self::getFormSecurityToken('admin_blocklist_import'), ]); diff --git a/src/Module/Blocklist/Domain/Download.php b/src/Module/Blocklist/Domain/Download.php index db7437dc79..bc3c80a255 100644 --- a/src/Module/Blocklist/Domain/Download.php +++ b/src/Module/Blocklist/Domain/Download.php @@ -22,37 +22,38 @@ namespace Friendica\Module\Blocklist\Domain; use Friendica\App; -use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Core\System; +use Friendica\Moderation\DomainPatternBlocklist; use Friendica\Module\Response; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; class Download extends \Friendica\BaseModule { - /** @var IManageConfigValues */ - private $config; + /** @var DomainPatternBlocklist */ + private $blocklist; - public function __construct(IManageConfigValues $config, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = []) + public function __construct(DomainPatternBlocklist $blocklist, 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->config = $config; + $this->blocklist = $blocklist; } + /** + * @param array $request + * @return void + * @throws \Exception + */ protected function rawContent(array $request = []) { - $blocklist = $this->config->get('system', 'blocklist'); - - $blocklistJson = json_encode($blocklist, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - - $hash = md5($blocklistJson); + $hash = md5(json_encode($this->blocklist->get(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); $etag = 'W/"' . $hash . '"'; - if (trim($_SERVER['HTTP_IF_NONE_MATCH'] ?? '') == $etag) { header("HTTP/1.1 304 Not Modified"); + System::exit(); } header('Content-Type: text/csv'); @@ -60,11 +61,7 @@ class Download extends \Friendica\BaseModule header('Content-disposition: attachment; filename="' . $this->baseUrl->getHostname() . '_domain_blocklist_' . substr($hash, 0, 6) . '.csv"'); header("Etag: $etag"); - $fp = fopen('php://output', 'w'); - foreach ($blocklist as $domain) { - fputcsv($fp, $domain); - } - fclose($fp); + $this->blocklist->exportToFile('php://output'); System::exit(); } diff --git a/tests/src/Console/ServerBlockConsoleTest.php b/tests/src/Console/ServerBlockConsoleTest.php index 202714fb1f..cdd7efef0e 100644 --- a/tests/src/Console/ServerBlockConsoleTest.php +++ b/tests/src/Console/ServerBlockConsoleTest.php @@ -23,6 +23,7 @@ namespace Friendica\Test\src\Console; use Friendica\Console\ServerBlock; use Friendica\Core\Config\Capability\IManageConfigValues; +use Friendica\Moderation\DomainPatternBlocklist; use Mockery; class ServerBlockConsoleTest extends ConsoleTest @@ -38,15 +39,15 @@ class ServerBlockConsoleTest extends ConsoleTest ] ]; /** - * @var IManageConfigValues|Mockery\LegacyMockInterface|Mockery\MockInterface + * @var DomainPatternBlocklist|Mockery\LegacyMockInterface|Mockery\MockInterface */ - private $configMock; + private $blocklistMock; protected function setUp() : void { parent::setUp(); - $this->configMock = Mockery::mock(IManageConfigValues::class); + $this->blocklistMock = Mockery::mock(DomainPatternBlocklist::class); } /** @@ -54,23 +55,18 @@ class ServerBlockConsoleTest extends ConsoleTest */ public function testBlockedServersList() { - $this->configMock + $this->blocklistMock ->shouldReceive('get') - ->with('system', 'blocklist', []) ->andReturn($this->defaultBlockList) ->once(); - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $txt = $this->dumpExecute($console); - $output = <<configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('addPattern') + ->with('testme.now', 'I like it!') + ->andReturn(1) ->once(); - $newBlockList = $this->defaultBlockList; - $newBlockList[] = [ - 'domain' => 'testme.now', - 'reason' => 'I like it!', - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(true) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $console->setArgument(1, 'testme.now'); $console->setArgument(2, 'I like it!'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain \'testme.now\' is now blocked. (Reason: \'I like it!\')' . PHP_EOL, $txt); + self::assertEquals('The domain pattern \'testme.now\' is now blocked. (Reason: \'I like it!\')' . "\n", $txt); } /** @@ -114,30 +98,18 @@ CONS; */ public function testAddBlockedServerWithDefaultReason() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('addPattern') + ->with('testme.now', DomainPatternBlocklist::DEFAULT_REASON) + ->andReturn(1) ->once(); - $newBlockList = $this->defaultBlockList; - $newBlockList[] = [ - 'domain' => 'testme.now', - 'reason' => ServerBlock::DEFAULT_REASON, - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(true) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $console->setArgument(1, 'testme.now'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain \'testme.now\' is now blocked. (Reason: \'' . ServerBlock::DEFAULT_REASON . '\')' . PHP_EOL, $txt); + self::assertEquals('The domain pattern \'testme.now\' is now blocked. (Reason: \'' . DomainPatternBlocklist::DEFAULT_REASON . '\')' . "\n", $txt); } /** @@ -145,36 +117,19 @@ CONS; */ public function testUpdateBlockedServer() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('addPattern') + ->with('pod.ordoevangelistarum.com', 'Other reason') + ->andReturn(2) ->once(); - $newBlockList = [ - [ - 'domain' => 'social.nobodyhasthe.biz', - 'reason' => 'Illegal content', - ], - [ - 'domain' => 'pod.ordoevangelistarum.com', - 'reason' => 'Other reason', - ] - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(true) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $console->setArgument(1, 'pod.ordoevangelistarum.com'); $console->setArgument(2, 'Other reason'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain \'pod.ordoevangelistarum.com\' is now updated. (Reason: \'Other reason\')' . PHP_EOL, $txt); + self::assertEquals('The domain pattern \'pod.ordoevangelistarum.com\' is now updated. (Reason: \'Other reason\')' . "\n", $txt); } /** @@ -182,31 +137,18 @@ CONS; */ public function testRemoveBlockedServer() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('removePattern') + ->with('pod.ordoevangelistarum.com') + ->andReturn(2) ->once(); - $newBlockList = [ - [ - 'domain' => 'social.nobodyhasthe.biz', - 'reason' => 'Illegal content', - ], - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(true) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'remove'); $console->setArgument(1, 'pod.ordoevangelistarum.com'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain \'pod.ordoevangelistarum.com\' is not more blocked' . PHP_EOL, $txt); + self::assertEquals('The domain pattern \'pod.ordoevangelistarum.com\' isn\'t blocked anymore' . "\n", $txt); } /** @@ -214,7 +156,7 @@ CONS; */ public function testBlockedServersWrongCommand() { - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'wrongcommand'); $txt = $this->dumpExecute($console); @@ -226,18 +168,18 @@ CONS; */ public function testRemoveBlockedServerNotExist() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('removePattern') + ->with('not.exiting') + ->andReturn(1) ->once(); - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'remove'); $console->setArgument(1, 'not.exiting'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain \'not.exiting\' is not blocked.' . PHP_EOL, $txt); + self::assertEquals('The domain pattern \'not.exiting\' wasn\'t blocked.' . "\n", $txt); } /** @@ -245,11 +187,11 @@ CONS; */ public function testAddBlockedServerMissingArgument() { - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $txt = $this->dumpExecute($console); - self::assertStringStartsWith('[Warning] Add needs a domain and optional a reason.', $txt); + self::assertStringStartsWith('[Warning] Add needs a domain pattern and optionally a reason.', $txt); } /** @@ -257,30 +199,18 @@ CONS; */ public function testAddBlockedServerNoSave() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('addPattern') + ->with('testme.now', DomainPatternBlocklist::DEFAULT_REASON) + ->andReturn(0) ->once(); - $newBlockList = $this->defaultBlockList; - $newBlockList[] = [ - 'domain' => 'testme.now', - 'reason' => ServerBlock::DEFAULT_REASON, - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(false) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $console->setArgument(1, 'testme.now'); $txt = $this->dumpExecute($console); - self::assertEquals('Couldn\'t save \'testme.now\' as blocked server' . PHP_EOL, $txt); + self::assertEquals('Couldn\'t save \'testme.now\' as blocked domain pattern' . "\n", $txt); } /** @@ -288,31 +218,18 @@ CONS; */ public function testRemoveBlockedServerNoSave() { - $this->configMock - ->shouldReceive('get') - ->with('system', 'blocklist', []) - ->andReturn($this->defaultBlockList) + $this->blocklistMock + ->shouldReceive('removePattern') + ->with('pod.ordoevangelistarum.com') + ->andReturn(0) ->once(); - $newBlockList = [ - [ - 'domain' => 'social.nobodyhasthe.biz', - 'reason' => 'Illegal content', - ], - ]; - - $this->configMock - ->shouldReceive('set') - ->with('system', 'blocklist', $newBlockList) - ->andReturn(false) - ->once(); - - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'remove'); $console->setArgument(1, 'pod.ordoevangelistarum.com'); $txt = $this->dumpExecute($console); - self::assertEquals('Couldn\'t remove \'pod.ordoevangelistarum.com\' from blocked servers' . PHP_EOL, $txt); + self::assertEquals('Couldn\'t remove \'pod.ordoevangelistarum.com\' from blocked domain patterns' . "\n", $txt); } /** @@ -320,7 +237,7 @@ CONS; */ public function testRemoveBlockedServerMissingArgument() { - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'remove'); $txt = $this->dumpExecute($console); @@ -332,7 +249,7 @@ CONS; */ public function testBlockedServersHelp() { - $console = new ServerBlock($this->configMock, $this->consoleArgv); + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setOption('help', true); $txt = $this->dumpExecute($console); From 5045f9e188ef7a8e6b151b7a492fc095c365985a Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 27 Jul 2022 22:11:58 -0400 Subject: [PATCH 3/5] Add email notification to all users on server domain pattern block list update --- src/Moderation/DomainPatternBlocklist.php | 78 +++++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/src/Moderation/DomainPatternBlocklist.php b/src/Moderation/DomainPatternBlocklist.php index 33bd189d6d..c606346879 100644 --- a/src/Moderation/DomainPatternBlocklist.php +++ b/src/Moderation/DomainPatternBlocklist.php @@ -22,6 +22,7 @@ namespace Friendica\Moderation; use Exception; +use Friendica\App\BaseURL; use Friendica\Core\Config\Capability\IManageConfigValues; use Friendica\Core\L10n; use Friendica\Database\Database; @@ -32,14 +33,28 @@ class DomainPatternBlocklist { const DEFAULT_REASON = 'blocked'; - /** - * @var IManageConfigValues - */ + /** @var IManageConfigValues */ private $config; - public function __construct(IManageConfigValues $config) + /** @var Database */ + private $db; + + /** @var Emailer */ + private $emailer; + + /** @var L10n */ + private $l10n; + + /** @var BaseURL */ + private $baseUrl; + + public function __construct(IManageConfigValues $config, Database $db, Emailer $emailer, L10n $l10n, BaseURL $baseUrl) { - $this->config = $config; + $this->config = $config; + $this->db = $db; + $this->emailer = $emailer; + $this->l10n = $l10n; + $this->baseUrl = $baseUrl; } public function get(): array @@ -49,7 +64,12 @@ class DomainPatternBlocklist public function set(array $blocklist): bool { - return $this->config->set('system', 'blocklist', $blocklist); + $result = $this->config->set('system', 'blocklist', $blocklist); + if ($result) { + $this->notifyAll(); + } + + return $result; } /** @@ -177,4 +197,50 @@ class DomainPatternBlocklist return $blocklist; } + + /** + * Sends a system email to all the node users about a change in the block list. Sends a single email to each unique + * email address among the valid users. + * + * @return int The number of recipients that were sent an email + * @throws HTTPException\InternalServerErrorException + * @throws HTTPException\UnprocessableEntityException + */ + public function notifyAll(): int + { + // Gathering all non-system parent users who verified their email address and aren't blocked or about to be deleted + // We sort on language to minimize the number of actual language switches during the email build loop + $recipients = $this->db->selectToArray( + 'user', + ['username', 'email', 'language'], + ['`uid` > 0 AND `parent-uid` = 0 AND `verified` AND NOT `account_removed` AND NOT `account_expired` AND NOT `blocked`'], + ['group_by' => ['email'], 'order' => ['language']] + ); + if (!$recipients) { + return 0; + } + + foreach ($recipients as $recipient) { + $this->l10n->withLang($recipient['language']); + $email = $this->emailer->newSystemMail() + ->withMessage( + $this->l10n->t('[%s] Notice of remote server domain pattern block list update', $this->emailer->getSiteEmailName()), + $this->l10n->t( + 'Dear %s, + +You are receiving this email because the Friendica node at %s where you are registered as a user updated their remote server domain pattern block list. + +Please review the updated list at %s at your earliest convenience.', + $recipient['username'], + $this->baseUrl->get(), + $this->baseUrl . '/friendica' + ) + ) + ->withRecipient($recipient['email']) + ->build(); + $this->emailer->send($email); + } + + return count($recipients); + } } From e35651c44a8186bd4a7d5c312bea60a433bd12ab Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 27 Jul 2022 23:09:35 -0400 Subject: [PATCH 4/5] Updated main translation file after adding strings --- view/lang/C/messages.po | 156 ++++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 70 deletions(-) diff --git a/view/lang/C/messages.po b/view/lang/C/messages.po index cd6550a05c..be5ecdca5a 100644 --- a/view/lang/C/messages.po +++ b/view/lang/C/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 2022.09-dev\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-07-27 18:59+0000\n" +"POT-Creation-Date: 2022-07-27 22:28-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -355,13 +355,13 @@ msgid "Event Starts:" msgstr "" #: mod/events.php:476 mod/events.php:506 -#: src/Module/Admin/Blocklist/Server/Add.php:104 -#: src/Module/Admin/Blocklist/Server/Add.php:106 -#: src/Module/Admin/Blocklist/Server/Import.php:155 -#: src/Module/Admin/Blocklist/Server/Index.php:68 -#: src/Module/Admin/Blocklist/Server/Index.php:69 -#: src/Module/Admin/Blocklist/Server/Index.php:98 -#: src/Module/Admin/Blocklist/Server/Index.php:99 +#: src/Module/Admin/Blocklist/Server/Add.php:136 +#: src/Module/Admin/Blocklist/Server/Add.php:138 +#: src/Module/Admin/Blocklist/Server/Import.php:128 +#: src/Module/Admin/Blocklist/Server/Index.php:84 +#: src/Module/Admin/Blocklist/Server/Index.php:85 +#: src/Module/Admin/Blocklist/Server/Index.php:113 +#: src/Module/Admin/Blocklist/Server/Index.php:114 #: src/Module/Admin/Item/Delete.php:69 src/Module/Debug/Probe.php:59 #: src/Module/Install.php:207 src/Module/Install.php:240 #: src/Module/Install.php:245 src/Module/Install.php:264 @@ -4265,6 +4265,22 @@ msgid "" "\t\t\tThank you and welcome to %2$s." msgstr "" +#: src/Moderation/DomainPatternBlocklist.php:144 +#, php-format +msgid "[%s] Notice of remote server domain pattern block list update" +msgstr "" + +#: src/Moderation/DomainPatternBlocklist.php:146 +#, php-format +msgid "" +"Dear %s,\n" +"\n" +"You are receiving this email because the Friendica node at %s where you are " +"registered as a user updated their remote server domain pattern block list.\n" +"\n" +"Please review the updated list at %s at your earliest convenience." +msgstr "" + #: src/Module/Admin/Addons/Details.php:65 msgid "Addon not found." msgstr "" @@ -4291,9 +4307,9 @@ msgstr "" #: src/Module/Admin/Addons/Details.php:111 src/Module/Admin/Addons/Index.php:67 #: src/Module/Admin/Blocklist/Contact.php:94 -#: src/Module/Admin/Blocklist/Server/Add.php:89 -#: src/Module/Admin/Blocklist/Server/Import.php:144 -#: src/Module/Admin/Blocklist/Server/Index.php:78 +#: src/Module/Admin/Blocklist/Server/Add.php:121 +#: src/Module/Admin/Blocklist/Server/Import.php:117 +#: src/Module/Admin/Blocklist/Server/Index.php:93 #: src/Module/Admin/Federation.php:200 src/Module/Admin/Item/Delete.php:64 #: src/Module/Admin/Logs/Settings.php:79 src/Module/Admin/Logs/View.php:84 #: src/Module/Admin/Queue.php:72 src/Module/Admin/Site.php:433 @@ -4509,32 +4525,32 @@ msgid "" msgstr "" #: src/Module/Admin/Blocklist/Contact.php:118 -#: src/Module/Admin/Blocklist/Server/Import.php:150 +#: src/Module/Admin/Blocklist/Server/Import.php:123 msgid "Block Reason" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:55 +#: src/Module/Admin/Blocklist/Server/Add.php:80 msgid "Server domain pattern added to the blocklist." msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:63 +#: src/Module/Admin/Blocklist/Server/Add.php:88 #, php-format msgid "%s server scheduled to be purged." msgid_plural "%s servers scheduled to be purged." msgstr[0] "" msgstr[1] "" -#: src/Module/Admin/Blocklist/Server/Add.php:88 -#: src/Module/Admin/Blocklist/Server/Import.php:143 +#: src/Module/Admin/Blocklist/Server/Add.php:120 +#: src/Module/Admin/Blocklist/Server/Import.php:116 msgid "← Return to the list" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:90 +#: src/Module/Admin/Blocklist/Server/Add.php:122 msgid "Block A New Server Domain Pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:91 -#: src/Module/Admin/Blocklist/Server/Index.php:82 +#: src/Module/Admin/Blocklist/Server/Add.php:123 +#: src/Module/Admin/Blocklist/Server/Index.php:97 msgid "" "

The server domain pattern syntax is case-insensitive shell wildcard, " "comprising the following special characters:

\n" @@ -4544,55 +4560,55 @@ msgid "" "" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:96 -#: src/Module/Admin/Blocklist/Server/Index.php:90 +#: src/Module/Admin/Blocklist/Server/Add.php:128 +#: src/Module/Admin/Blocklist/Server/Index.php:105 msgid "Check pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:97 +#: src/Module/Admin/Blocklist/Server/Add.php:129 msgid "Matching known servers" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:98 +#: src/Module/Admin/Blocklist/Server/Add.php:130 msgid "Server Name" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:99 +#: src/Module/Admin/Blocklist/Server/Add.php:131 msgid "Server Domain" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:100 +#: src/Module/Admin/Blocklist/Server/Add.php:132 msgid "Known Contacts" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:101 +#: src/Module/Admin/Blocklist/Server/Add.php:133 #, php-format msgid "%d known server" msgid_plural "%d known servers" msgstr[0] "" msgstr[1] "" -#: src/Module/Admin/Blocklist/Server/Add.php:102 +#: src/Module/Admin/Blocklist/Server/Add.php:134 msgid "Add pattern to the blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:104 -#: src/Module/Admin/Blocklist/Server/Index.php:99 +#: src/Module/Admin/Blocklist/Server/Add.php:136 +#: src/Module/Admin/Blocklist/Server/Index.php:114 msgid "Server Domain Pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:104 -#: src/Module/Admin/Blocklist/Server/Index.php:99 +#: src/Module/Admin/Blocklist/Server/Add.php:136 +#: src/Module/Admin/Blocklist/Server/Index.php:114 msgid "" "The domain pattern of the new server to add to the blocklist. Do not include " "the protocol." msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:105 +#: src/Module/Admin/Blocklist/Server/Add.php:137 msgid "Purge server" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:105 +#: src/Module/Admin/Blocklist/Server/Add.php:137 msgid "" "Also purges all the locally stored content authored by the known contacts " "registered on that server. Keeps the contacts and the server records. This " @@ -4604,154 +4620,154 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: src/Module/Admin/Blocklist/Server/Add.php:106 +#: src/Module/Admin/Blocklist/Server/Add.php:138 msgid "Block reason" msgstr "" -#: src/Module/Admin/Blocklist/Server/Add.php:106 +#: src/Module/Admin/Blocklist/Server/Add.php:138 msgid "" "The reason why you blocked this server domain pattern. This reason will be " "shown publicly in the server information page." msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:88 -#: src/Module/Admin/Blocklist/Server/Import.php:100 +#: src/Module/Admin/Blocklist/Server/Import.php:75 +#: src/Module/Admin/Blocklist/Server/Import.php:84 msgid "Error importing pattern file" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:106 +#: src/Module/Admin/Blocklist/Server/Import.php:90 msgid "Local blocklist replaced with the provided file." msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:121 +#: src/Module/Admin/Blocklist/Server/Import.php:94 #, php-format msgid "%d pattern was added to the local blocklist." msgid_plural "%d patterns were added to the local blocklist." msgstr[0] "" msgstr[1] "" -#: src/Module/Admin/Blocklist/Server/Import.php:123 +#: src/Module/Admin/Blocklist/Server/Import.php:96 msgid "No pattern was added to the local blocklist." msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:145 +#: src/Module/Admin/Blocklist/Server/Import.php:118 msgid "Import a Server Domain Pattern Blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:146 +#: src/Module/Admin/Blocklist/Server/Import.php:119 msgid "" "

This file can be downloaded from the /friendica path of any " "Friendica server.

" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:147 -#: src/Module/Admin/Blocklist/Server/Index.php:89 +#: src/Module/Admin/Blocklist/Server/Import.php:120 +#: src/Module/Admin/Blocklist/Server/Index.php:104 msgid "Upload file" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:148 +#: src/Module/Admin/Blocklist/Server/Import.php:121 msgid "Patterns to import" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:149 +#: src/Module/Admin/Blocklist/Server/Import.php:122 msgid "Domain Pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:151 +#: src/Module/Admin/Blocklist/Server/Import.php:124 msgid "Import Mode" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:152 +#: src/Module/Admin/Blocklist/Server/Import.php:125 msgid "Import Patterns" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:153 +#: src/Module/Admin/Blocklist/Server/Import.php:126 #, php-format msgid "%d total pattern" msgid_plural "%d total patterns" msgstr[0] "" msgstr[1] "" -#: src/Module/Admin/Blocklist/Server/Import.php:155 -#: src/Module/Admin/Blocklist/Server/Index.php:98 +#: src/Module/Admin/Blocklist/Server/Import.php:128 +#: src/Module/Admin/Blocklist/Server/Index.php:113 msgid "Server domain pattern blocklist CSV file" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:156 +#: src/Module/Admin/Blocklist/Server/Import.php:129 msgid "Append" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:156 +#: src/Module/Admin/Blocklist/Server/Import.php:129 msgid "" "Imports patterns from the file that weren't already existing in the current " "blocklist." msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:157 +#: src/Module/Admin/Blocklist/Server/Import.php:130 msgid "Replace" msgstr "" -#: src/Module/Admin/Blocklist/Server/Import.php:157 +#: src/Module/Admin/Blocklist/Server/Import.php:130 msgid "Replaces the current blocklist by the imported patterns." msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:68 -#: src/Module/Admin/Blocklist/Server/Index.php:93 +#: src/Module/Admin/Blocklist/Server/Index.php:84 +#: src/Module/Admin/Blocklist/Server/Index.php:108 msgid "Blocked server domain pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:69 -#: src/Module/Admin/Blocklist/Server/Index.php:94 src/Module/Friendica.php:82 +#: src/Module/Admin/Blocklist/Server/Index.php:85 +#: src/Module/Admin/Blocklist/Server/Index.php:109 src/Module/Friendica.php:82 msgid "Reason for the block" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:70 +#: src/Module/Admin/Blocklist/Server/Index.php:86 msgid "Delete server domain pattern" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:70 +#: src/Module/Admin/Blocklist/Server/Index.php:86 msgid "Check to delete this entry from the blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:79 +#: src/Module/Admin/Blocklist/Server/Index.php:94 msgid "Server Domain Pattern Blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:80 +#: src/Module/Admin/Blocklist/Server/Index.php:95 msgid "" "This page can be used to define a blocklist of server domain patterns from " "the federated network that are not allowed to interact with your node. For " "each domain pattern you should also provide the reason why you block it." msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:81 +#: src/Module/Admin/Blocklist/Server/Index.php:96 msgid "" "The list of blocked server domain patterns will be made publically available " "on the /friendica page so that your users and " "people investigating communication problems can find the reason easily." msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:87 +#: src/Module/Admin/Blocklist/Server/Index.php:102 msgid "Import server domain pattern blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:88 +#: src/Module/Admin/Blocklist/Server/Index.php:103 msgid "Add new entry to the blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:91 +#: src/Module/Admin/Blocklist/Server/Index.php:106 msgid "Save changes to the blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:92 +#: src/Module/Admin/Blocklist/Server/Index.php:107 msgid "Current Entries in the Blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:95 +#: src/Module/Admin/Blocklist/Server/Index.php:110 msgid "Delete entry from the blocklist" msgstr "" -#: src/Module/Admin/Blocklist/Server/Index.php:96 +#: src/Module/Admin/Blocklist/Server/Index.php:111 msgid "Delete entry from the blocklist?" msgstr "" From 97ccb4d2c45ff705d6f776cc9a841f0680d8238f Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 28 Jul 2022 05:25:41 -0400 Subject: [PATCH 5/5] Make server domain pattern block reason mandatory --- src/Console/ServerBlock.php | 6 ++-- src/Moderation/DomainPatternBlocklist.php | 27 +++++--------- tests/src/Console/ServerBlockConsoleTest.php | 37 +++++++------------- 3 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/Console/ServerBlock.php b/src/Console/ServerBlock.php index 9ef6777db7..65b806fc70 100644 --- a/src/Console/ServerBlock.php +++ b/src/Console/ServerBlock.php @@ -155,12 +155,12 @@ HELP; */ private function addBlockedServer(): int { - if (count($this->args) < 2 || count($this->args) > 3) { - throw new CommandArgsException('Add needs a domain pattern and optionally a reason.'); + if (count($this->args) != 3) { + throw new CommandArgsException('Add needs a domain pattern and a reason.'); } $pattern = $this->getArgument(1); - $reason = (count($this->args) === 3) ? $this->getArgument(2) : DomainPatternBlocklist::DEFAULT_REASON; + $reason = $this->getArgument(2); $result = $this->blocklist->addPattern($pattern, $reason); if ($result) { diff --git a/src/Moderation/DomainPatternBlocklist.php b/src/Moderation/DomainPatternBlocklist.php index c606346879..a57b5f04cb 100644 --- a/src/Moderation/DomainPatternBlocklist.php +++ b/src/Moderation/DomainPatternBlocklist.php @@ -31,8 +31,6 @@ use Friendica\Util\Emailer; class DomainPatternBlocklist { - const DEFAULT_REASON = 'blocked'; - /** @var IManageConfigValues */ private $config; @@ -73,11 +71,11 @@ class DomainPatternBlocklist } /** - * @param string $pattern - * @param string|null $reason + * @param string $pattern + * @param string $reason * @return int 0 if the block list couldn't be saved, 1 if the pattern was added, 2 if it was updated in place */ - public function addPattern(string $pattern, string $reason = null): int + public function addPattern(string $pattern, string $reason): int { $update = false; @@ -86,7 +84,7 @@ class DomainPatternBlocklist if ($blocked['domain'] === $pattern) { $blocklist[] = [ 'domain' => $pattern, - 'reason' => $reason ?? self::DEFAULT_REASON, + 'reason' => $reason, ]; $update = true; @@ -98,7 +96,7 @@ class DomainPatternBlocklist if (!$update) { $blocklist[] = [ 'domain' => $pattern, - 'reason' => $reason ?? self::DEFAULT_REASON, + 'reason' => $reason, ]; } @@ -179,18 +177,11 @@ class DomainPatternBlocklist $blocklist = []; while (($data = fgetcsv($fp, 1000)) !== false) { - $domain = $data[0]; - if (count($data) == 0) { - $reason = self::DEFAULT_REASON; - } else { - $reason = $data[1]; - } - - $data = [ - 'domain' => $domain, - 'reason' => $reason + $item = [ + 'domain' => $data[0], + 'reason' => $data[1] ?? '', ]; - if (!in_array($data, $blocklist)) { + if (!in_array($item, $blocklist)) { $blocklist[] = $data; } } diff --git a/tests/src/Console/ServerBlockConsoleTest.php b/tests/src/Console/ServerBlockConsoleTest.php index cdd7efef0e..9837fbb8ee 100644 --- a/tests/src/Console/ServerBlockConsoleTest.php +++ b/tests/src/Console/ServerBlockConsoleTest.php @@ -93,25 +93,6 @@ CONS; self::assertEquals('The domain pattern \'testme.now\' is now blocked. (Reason: \'I like it!\')' . "\n", $txt); } - /** - * Test blockedservers add command with the default reason - */ - public function testAddBlockedServerWithDefaultReason() - { - $this->blocklistMock - ->shouldReceive('addPattern') - ->with('testme.now', DomainPatternBlocklist::DEFAULT_REASON) - ->andReturn(1) - ->once(); - - $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); - $console->setArgument(0, 'add'); - $console->setArgument(1, 'testme.now'); - $txt = $this->dumpExecute($console); - - self::assertEquals('The domain pattern \'testme.now\' is now blocked. (Reason: \'' . DomainPatternBlocklist::DEFAULT_REASON . '\')' . "\n", $txt); - } - /** * Test blockedservers add command on existed domain */ @@ -170,16 +151,16 @@ CONS; { $this->blocklistMock ->shouldReceive('removePattern') - ->with('not.exiting') + ->with('not.existing') ->andReturn(1) ->once(); $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'remove'); - $console->setArgument(1, 'not.exiting'); + $console->setArgument(1, 'not.existing'); $txt = $this->dumpExecute($console); - self::assertEquals('The domain pattern \'not.exiting\' wasn\'t blocked.' . "\n", $txt); + self::assertEquals('The domain pattern \'not.existing\' wasn\'t blocked.' . "\n", $txt); } /** @@ -191,7 +172,14 @@ CONS; $console->setArgument(0, 'add'); $txt = $this->dumpExecute($console); - self::assertStringStartsWith('[Warning] Add needs a domain pattern and optionally a reason.', $txt); + self::assertStringStartsWith('[Warning] Add needs a domain pattern and a reason.', $txt); + + $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); + $console->setArgument(0, 'add'); + $console->setArgument(1, 'testme.now'); + $txt = $this->dumpExecute($console); + + self::assertStringStartsWith('[Warning] Add needs a domain pattern and a reason.', $txt); } /** @@ -201,13 +189,14 @@ CONS; { $this->blocklistMock ->shouldReceive('addPattern') - ->with('testme.now', DomainPatternBlocklist::DEFAULT_REASON) + ->with('testme.now', 'I like it!') ->andReturn(0) ->once(); $console = new ServerBlock($this->blocklistMock, $this->consoleArgv); $console->setArgument(0, 'add'); $console->setArgument(1, 'testme.now'); + $console->setArgument(2, 'I like it!'); $txt = $this->dumpExecute($console); self::assertEquals('Couldn\'t save \'testme.now\' as blocked domain pattern' . "\n", $txt);