diff --git a/images/default/corgidon.png b/images/default/corgidon.png index 389719b66..0e06380b3 100644 Binary files a/images/default/corgidon.png and b/images/default/corgidon.png differ diff --git a/images/default/gotosocial.svg b/images/default/gotosocial.svg index 9b4e7665e..d4af811fd 100644 --- a/images/default/gotosocial.svg +++ b/images/default/gotosocial.svg @@ -1,160 +1 @@ - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/images/default/hometown.png b/images/default/hometown.png index 29b27183b..314901653 100644 Binary files a/images/default/hometown.png and b/images/default/hometown.png differ diff --git a/images/default/koyuspace.png b/images/default/koyuspace.png index 231e48b20..c622797d6 100644 Binary files a/images/default/koyuspace.png and b/images/default/koyuspace.png differ diff --git a/images/default/plume.png b/images/default/plume.png index 126558136..d6a2f1a77 100644 Binary files a/images/default/plume.png and b/images/default/plume.png differ diff --git a/images/diaspora-banner.jpg b/images/diaspora-banner.jpg index b0df6a2fa..3d423ccf2 100644 Binary files a/images/diaspora-banner.jpg and b/images/diaspora-banner.jpg differ diff --git a/images/friendica-banner.jpg b/images/friendica-banner.jpg index 8b1d0fbf6..dc333f0a4 100644 Binary files a/images/friendica-banner.jpg and b/images/friendica-banner.jpg differ diff --git a/images/friendica-maskable-512.png b/images/friendica-maskable-512.png index b454089f8..da2e0df8b 100644 Binary files a/images/friendica-maskable-512.png and b/images/friendica-maskable-512.png differ diff --git a/images/friendica-maskable.svg b/images/friendica-maskable.svg index 77864d1a5..b2cf4cf65 100644 --- a/images/friendica-maskable.svg +++ b/images/friendica-maskable.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/images/friendica.svg b/images/friendica.svg index 180fe2a90..efbb051cd 100644 --- a/images/friendica.svg +++ b/images/friendica.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/mods/fpostit/friendica.svg b/mods/fpostit/friendica.svg index 180fe2a90..efbb051cd 100644 --- a/mods/fpostit/friendica.svg +++ b/mods/fpostit/friendica.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/src/App/Page.php b/src/App/Page.php index 37141426c..6317214b4 100644 --- a/src/App/Page.php +++ b/src/App/Page.php @@ -73,6 +73,8 @@ class Page implements ArrayAccess 'right_aside' => '', 'template' => '', 'title' => '', + 'section' => '', + 'module' => '', ]; /** * @var string The basepath of the page @@ -509,6 +511,11 @@ class Page implements ArrayAccess $page = $this->page; + // add and escape some common but crucial content for direct "echo" in HTML (security) + $page['title'] = htmlspecialchars($page['title'] ?? ''); + $page['section'] = htmlspecialchars($args->get(0) ?? 'generic'); + $page['module'] = htmlspecialchars($args->getModuleName() ?? ''); + header("X-Friendica-Version: " . App::VERSION); header("Content-type: text/html; charset=utf-8"); diff --git a/src/Core/Protocol.php b/src/Core/Protocol.php index 25955abd5..e4490da73 100644 --- a/src/Core/Protocol.php +++ b/src/Core/Protocol.php @@ -52,7 +52,6 @@ class Protocol // Supported through a connector const DIASPORA2 = 'dspc'; // Diaspora connector - const LINKEDIN = 'lnkd'; // LinkedIn const PUMPIO = 'pump'; // pump.io const STATUSNET = 'stac'; // Statusnet connector const TWITTER = 'twit'; // Twitter @@ -66,6 +65,7 @@ class Protocol // Currently unsupported const ICALENDAR = 'ical'; // iCalendar const MYSPACE = 'mysp'; // MySpace + const LINKEDIN = 'lnkd'; // LinkedIn const NEWS = 'nntp'; // Network News Transfer Protocol const PNUT = 'pnut'; // pnut.io const XMPP = 'xmpp'; // XMPP diff --git a/src/Model/GServer.php b/src/Model/GServer.php index 2fc37f35c..10c1a25e5 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -44,7 +44,9 @@ use Friendica\Util\Network; use Friendica\Util\Strings; use Friendica\Util\XML; use Friendica\Network\HTTPException; +use Friendica\Worker\UpdateGServer; use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; /** * This class handles GServer related functions @@ -99,11 +101,11 @@ class GServer */ public static function add(string $url, bool $only_nodeinfo = false) { - if (self::getID($url, false)) { + if (self::getID($url)) { return; } - Worker::add(Worker::PRIORITY_LOW, 'UpdateGServer', $url, $only_nodeinfo); + UpdateGServer::add(Worker::PRIORITY_LOW, $url, $only_nodeinfo); } /** @@ -164,6 +166,60 @@ class GServer return DI::dba()->toArray($stmt); } + /** + * Checks if the given server array is unreachable for a long time now + * + * @param integer $gsid + * @return boolean + */ + private static function isDefunct(array $gserver): bool + { + return ($gserver['failed'] || in_array($gserver['network'], Protocol::FEDERATED)) && + ($gserver['last_contact'] >= $gserver['created']) && + ($gserver['last_contact'] < $gserver['last_failure']) && + ($gserver['last_contact'] < DateTimeFormat::utc('now - 90 days')); + } + + /** + * Checks if the given server id is unreachable for a long time now + * + * @param integer $gsid + * @return boolean + */ + public static function isDefunctById(int $gsid): bool + { + $gserver = DBA::selectFirst('gserver', ['url', 'next_contact', 'last_contact', 'last_failure', 'created', 'failed', 'network'], ['id' => $gsid]); + if (empty($gserver)) { + return false; + } else { + if (strtotime($gserver['next_contact']) < time()) { + UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['url']); + } + + return self::isDefunct($gserver); + } + } + + /** + * Checks if the given server id is reachable + * + * @param integer $gsid + * @return boolean + */ + public static function isReachableById(int $gsid): bool + { + $gserver = DBA::selectFirst('gserver', ['url', 'next_contact', 'failed', 'network'], ['id' => $gsid]); + if (empty($gserver)) { + return true; + } else { + if (strtotime($gserver['next_contact']) < time()) { + UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['url']); + } + + return !$gserver['failed'] && in_array($gserver['network'], Protocol::FEDERATED); + } + } + /** * Checks if the given server is reachable * @@ -200,7 +256,7 @@ class GServer } if (!empty($server) && (empty($gserver) || strtotime($gserver['next_contact']) < time())) { - Worker::add(Worker::PRIORITY_LOW, 'UpdateGServer', $server, false); + UpdateGServer::add(Worker::PRIORITY_LOW, $server); } return $reachable; @@ -305,6 +361,47 @@ class GServer return self::detect($server_url, $network, $only_nodeinfo); } + /** + * Reset failed server status by gserver id + * + * @param int $gsid + * @param string $network + */ + public static function setReachableById(int $gsid, string $network) + { + $gserver = DBA::selectFirst('gserver', ['url', 'failed', 'next_contact', 'network'], ['id' => $gsid]); + if (DBA::isResult($gserver) && $gserver['failed']) { + $fields = ['failed' => false, 'last_contact' => DateTimeFormat::utcNow()]; + if (!empty($network) && !in_array($gserver['network'], Protocol::FEDERATED)) { + $fields['network'] = $network; + } + self::update($fields, ['id' => $gsid]); + Logger::info('Reset failed status for server', ['url' => $gserver['url']]); + + if (strtotime($gserver['next_contact']) < time()) { + UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['url']); + } + } + } + + /** + * Set failed server status by gserver id + * + * @param int $gsid + */ + public static function setFailureById(int $gsid) + { + $gserver = DBA::selectFirst('gserver', ['url', 'failed', 'next_contact'], ['id' => $gsid]); + if (DBA::isResult($gserver) && !$gserver['failed']) { + self::update(['failed' => true, 'last_failure' => DateTimeFormat::utcNow()], ['id' => $gsid]); + Logger::info('Set failed status for server', ['url' => $gserver['url']]); + + if (strtotime($gserver['next_contact']) < time()) { + UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['url']); + } + } + } + /** * Set failed server status * @@ -334,18 +431,41 @@ class GServer * * @return string cleaned URL * @throws Exception + * @deprecated since 2023.03 Use cleanUri instead */ public static function cleanURL(string $dirtyUrl): string { try { - $url = str_replace('/index.php', '', trim($dirtyUrl, '/')); - return (string)(new Uri($url))->withUserInfo('')->withQuery('')->withFragment(''); + return (string)self::cleanUri(new Uri($dirtyUrl)); } catch (\Throwable $e) { - Logger::warning('Invalid URL', ['dirtyUrl' => $dirtyUrl, 'url' => $url]); + Logger::warning('Invalid URL', ['dirtyUrl' => $dirtyUrl]); return ''; } } + /** + * Remove unwanted content from the given URI + * + * @param UriInterface $dirtyUri + * + * @return UriInterface cleaned URI + * @throws Exception + */ + public static function cleanUri(UriInterface $dirtyUri): string + { + return $dirtyUri + ->withUserInfo('') + ->withQuery('') + ->withFragment('') + ->withPath( + preg_replace( + '#(?:^|/)index\.php#', + '', + rtrim($dirtyUri->getPath(), '/') + ) + ); + } + /** * Detect server data (type, protocol, version number, ...) * The detected data is then updated or inserted in the gserver table. diff --git a/src/Module/ActivityPub/Objects.php b/src/Module/ActivityPub/Objects.php index 8c8109d66..528aa9405 100644 --- a/src/Module/ActivityPub/Objects.php +++ b/src/Module/ActivityPub/Objects.php @@ -75,9 +75,7 @@ class Objects extends BaseModule throw new HTTPException\NotFoundException(); } - $owner = User::getById($item['uid'], ['hidewall']); - - $validated = empty($owner['hidewall']) && in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]); + $validated = in_array($item['private'], [Item::PUBLIC, Item::UNLISTED]); if (!$validated) { $requester = HTTPSignature::getSigner('', $_SERVER); diff --git a/src/Module/DFRN/Poll.php b/src/Module/DFRN/Poll.php index 1562ce0de..e841e9b26 100644 --- a/src/Module/DFRN/Poll.php +++ b/src/Module/DFRN/Poll.php @@ -37,13 +37,13 @@ class Poll extends BaseModule { $owner = User::getByNickname( $this->parameters['nickname'] ?? '', - ['nickname', 'blocked', 'account_expired', 'account_removed', 'hidewall'] + ['nickname', 'blocked', 'account_expired', 'account_removed'] ); if (!$owner || $owner['account_expired'] || $owner['account_removed']) { throw new HTTPException\NotFoundException($this->t('User not found.')); } - if ($owner['blocked'] || $owner['hidewall']) { + if ($owner['blocked']) { throw new HTTPException\UnauthorizedException($this->t('Access to this profile has been restricted.')); } diff --git a/src/Module/Feed.php b/src/Module/Feed.php index 59714af38..92d65fb4b 100644 --- a/src/Module/Feed.php +++ b/src/Module/Feed.php @@ -65,7 +65,7 @@ class Feed extends BaseModule throw new HTTPException\NotFoundException($this->t('User not found.')); } - if ($owner['blocked'] || $owner['hidewall']) { + if ($owner['blocked']) { throw new HTTPException\UnauthorizedException($this->t('Access to this profile has been restricted.')); } diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 0ea11c6ad..66ee9e4b5 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -674,6 +674,20 @@ class Transmitter } $exclusive = false; + $mention = false; + + if ($is_forum_thread) { + foreach (Tag::getByURIId($item['parent-uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION]) as $term) { + $profile = APContact::getByURL($term['url'], false); + if (!empty($profile) && ($profile['type'] == 'Group')) { + if ($term['type'] == Tag::EXCLUSIVE_MENTION) { + $exclusive = true; + } elseif ($term['type'] == Tag::MENTION) { + $mention = true; + } + } + } + } $terms = Tag::getByURIId($item['uri-id'], [Tag::MENTION, Tag::IMPLICIT_MENTION, Tag::EXCLUSIVE_MENTION]); @@ -704,6 +718,8 @@ class Transmitter if (!empty($profile['followers']) && ($profile['type'] == 'Group')) { $data['cc'][] = $profile['followers']; } + } elseif (($term['type'] == Tag::MENTION) && ($profile['type'] == 'Group')) { + $mention = true; } $data['to'][] = $profile['url']; } @@ -726,12 +742,18 @@ class Transmitter if (!empty($profile['followers']) && ($profile['type'] == 'Group')) { $data['cc'][] = $profile['followers']; } + } elseif (($term['type'] == Tag::MENTION) && ($profile['type'] == 'Group')) { + $mention = true; } $data['to'][] = $profile['url']; } } } + if ($mention) { + $exclusive = false; + } + if ($is_forum && !$exclusive && !empty($follower)) { $data['cc'][] = $follower; } elseif (!$exclusive) { diff --git a/src/Util/Network.php b/src/Util/Network.php index 5a06a0056..e9d3fb110 100644 --- a/src/Util/Network.php +++ b/src/Util/Network.php @@ -29,6 +29,7 @@ use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\HTTPException\NotModifiedException; use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; class Network { @@ -177,11 +178,28 @@ class Network * @param string $url The url to check the domain from * * @return boolean + * + * @deprecated since 2023.03 Use isUriBlocked instead */ public static function isUrlBlocked(string $url): bool { - $host = @parse_url($url, PHP_URL_HOST); - if (!$host) { + try { + return self::isUriBlocked(new Uri($url)); + } catch (\Throwable $e) { + Logger::warning('Invalid URL', ['url' => $url]); + return false; + } + } + + /** + * Checks if the provided URI domain is on the domain blocklist. + * + * @param UriInterface $uri + * @return boolean + */ + public static function isUriBlocked(UriInterface $uri): bool + { + if (!$uri->getHost()) { return false; } @@ -191,7 +209,7 @@ class Network } foreach ($domain_blocklist as $domain_block) { - if (fnmatch(strtolower($domain_block['domain']), strtolower($host))) { + if (fnmatch(strtolower($domain_block['domain']), strtolower($uri->getHost()))) { return true; } } diff --git a/src/Worker/UpdateContact.php b/src/Worker/UpdateContact.php index 8de3629ca..f83f47524 100644 --- a/src/Worker/UpdateContact.php +++ b/src/Worker/UpdateContact.php @@ -23,6 +23,7 @@ namespace Friendica\Worker; use Friendica\Core\Logger; use Friendica\Model\Contact; +use Friendica\Network\HTTPException\InternalServerErrorException; class UpdateContact { @@ -34,8 +35,33 @@ class UpdateContact */ public static function execute(int $contact_id) { + // Silently dropping the task if the contact is blocked + if (Contact::isBlocked($contact_id)) { + return; + } + $success = Contact::updateFromProbe($contact_id); Logger::info('Updated from probe', ['id' => $contact_id, 'success' => $success]); } + + /** + * @param array|int $run_parameters Priority constant or array of options described in Worker::add + * @param int $contact_id + * @return int + * @throws InternalServerErrorException + */ + public static function add($run_parameters, int $contact_id): int + { + if (!$contact_id) { + throw new \InvalidArgumentException('Invalid value provided for contact_id'); + } + + // Dropping the task if the contact is blocked + if (Contact::isBlocked($contact_id)) { + return 0; + } + + return Worker::add($run_parameters, 'UpdateContact', $contact_id); + } } diff --git a/src/Worker/UpdateGServer.php b/src/Worker/UpdateGServer.php index d180f34c4..a17fca81c 100644 --- a/src/Worker/UpdateGServer.php +++ b/src/Worker/UpdateGServer.php @@ -22,9 +22,14 @@ namespace Friendica\Worker; use Friendica\Core\Logger; +use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\Model\GServer; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\Network; use Friendica\Util\Strings; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; class UpdateGServer { @@ -34,8 +39,9 @@ class UpdateGServer * @param string $server_url Server URL * @param boolean $only_nodeinfo Only use nodeinfo for server detection * @return void + * @throws \Exception */ - public static function execute(string $server_url, bool $only_nodeinfo = false) + public static function execute(string $server_url, bool $only_nodeinfo) { if (empty($server_url)) { return; @@ -47,6 +53,11 @@ class UpdateGServer return; } + // Silently dropping the worker task if the server domain is blocked + if (Network::isUrlBlocked($filtered)) { + return; + } + if (($filtered != $server_url) && DBA::exists('gserver', ['nurl' => Strings::normaliseLink($server_url)])) { GServer::setFailure($server_url); return; @@ -61,4 +72,23 @@ class UpdateGServer $ret = GServer::check($filtered, '', true, $only_nodeinfo); Logger::info('Updated gserver', ['url' => $filtered, 'result' => $ret]); } + + /** + * @param array|int $run_parameters Priority constant or array of options described in Worker::add + * @param string $serverUrl + * @param bool $onlyNodeInfo Only use NodeInfo for server detection + * @return int + * @throws InternalServerErrorException + */ + public static function add($run_parameters, string $serverUrl, bool $onlyNodeInfo = false): int + { + // Dropping the worker task if the server domain is blocked + if (Network::isUrlBlocked($serverUrl)) { + return 0; + } + + // We have to convert the Uri back to string because worker parameters are saved in JSON format which + // doesn't allow for structured objects. + return Worker::add($run_parameters, 'UpdateGServer', $serverUrl, $onlyNodeInfo); + } } diff --git a/src/Worker/UpdateGServers.php b/src/Worker/UpdateGServers.php index 25a2db16e..fca3bccb7 100644 --- a/src/Worker/UpdateGServers.php +++ b/src/Worker/UpdateGServers.php @@ -27,6 +27,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; +use GuzzleHttp\Psr7\Uri; class UpdateGServers { @@ -63,12 +64,12 @@ class UpdateGServers // There are duplicated "url" but not "nurl". So we check both addresses instead of just overwriting them, // since that would mean loosing data. if (!empty($gserver['url'])) { - if (Worker::add(Worker::PRIORITY_LOW, 'UpdateGServer', $gserver['url'])) { + if (UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['url'])) { $count++; } } if (!empty($gserver['nurl']) && ($gserver['nurl'] != Strings::normaliseLink($gserver['url']))) { - if (Worker::add(Worker::PRIORITY_LOW, 'UpdateGServer', $gserver['nurl'])) { + if (UpdateGServer::add(Worker::PRIORITY_LOW, $gserver['nurl'])) { $count++; } } diff --git a/tests/src/Model/GServerTest.php b/tests/src/Model/GServerTest.php new file mode 100644 index 000000000..a56f4ed6f --- /dev/null +++ b/tests/src/Model/GServerTest.php @@ -0,0 +1,76 @@ +. + * + */ + +namespace Friendica\Test\src\Model; + +use Friendica\Model\GServer; +use GuzzleHttp\Psr7\Uri; +use Psr\Http\Message\UriInterface; + +class GServerTest extends \PHPUnit\Framework\TestCase +{ + public function dataCleanUri(): array + { + return [ + 'full-monty' => [ + 'expected' => new Uri('https://example.com/path'), + 'dirtyUri' => new Uri('https://user:password@example.com/path?query=string#fragment'), + ], + 'index.php' => [ + 'expected' => new Uri('https://example.com'), + 'dirtyUri' => new Uri('https://example.com/index.php'), + ], + 'index.php-2' => [ + 'expected' => new Uri('https://example.com/path/to/resource'), + 'dirtyUri' => new Uri('https://example.com/index.php/path/to/resource'), + ], + 'index.php-path' => [ + 'expected' => new Uri('https://example.com/path/to'), + 'dirtyUri' => new Uri('https://example.com/path/to/index.php'), + ], + 'index.php-path-2' => [ + 'expected' => new Uri('https://example.com/path/to/path/to/resource'), + 'dirtyUri' => new Uri('https://example.com/path/to/index.php/path/to/resource'), + ], + 'index.php-slash' => [ + 'expected' => new Uri('https://example.com'), + 'dirtyUri' => new Uri('https://example.com/index.php/'), + ], + 'index.php-slash-2' => [ + 'expected' => new Uri('https://example.com/path/to/resource'), + 'dirtyUri' => new Uri('https://example.com/index.php/path/to/resource/'), + ], + ]; + } + + /** + * @dataProvider dataCleanUri + * + * @param UriInterface $expected + * @param UriInterface $dirtyUri + * @return void + * @throws \Exception + */ + public function testCleanUri(UriInterface $expected, UriInterface $dirtyUri) + { + $this->assertEquals($expected, GServer::cleanUri($dirtyUri)); + } +} diff --git a/view/theme/frio/php/default.php b/view/theme/frio/php/default.php index d0580373e..0c6dd3934 100644 --- a/view/theme/frio/php/default.php +++ b/view/theme/frio/php/default.php @@ -77,7 +77,7 @@ $is_singleuser_class = $is_singleuser ? "is-singleuser" : "is-not-singleuser"; ?> - "> + "> t('Skip to main content'); ?>
'; if (!empty($page['content'])) { echo $page['content'];