diff --git a/components/FreeNetwork/FreeNetwork.php b/components/FreeNetwork/FreeNetwork.php index e5b429e00a..aca11f4b24 100644 --- a/components/FreeNetwork/FreeNetwork.php +++ b/components/FreeNetwork/FreeNetwork.php @@ -23,6 +23,7 @@ namespace Component\FreeNetwork; use App\Core\Event; use App\Entity\Activity; +use Plugin\ActivityPub\Entity\ActivitypubActor; use function App\Core\I18n\_m; use App\Core\Log; use App\Core\Modules\Component; @@ -114,7 +115,7 @@ class FreeNetwork extends Component $profile = null; if (Discovery::isAcct($resource)) { $parts = explode('@', mb_substr(urldecode($resource), 5)); // 5 is strlen of 'acct:' - if (count($parts) == 2) { + if (count($parts) === 2) { [$nick, $domain] = $parts; if ($domain !== $_ENV['SOCIAL_DOMAIN']) { throw new ServerException(_m('Remote profiles not supported via WebFinger yet.')); @@ -265,6 +266,161 @@ class FreeNetwork extends Component return Event::stop; } + /** + * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz + * + * @param string $text The text from which to extract webfinger IDs + * @param string $preMention Character(s) that signals a mention ('@', '!'...) + * @return array The matching IDs (without $preMention) and each respective position in the given string. + */ + public static function extractWebfingerIds(string $text, string $preMention = '@'): array + { + $wmatches = []; + $result = preg_match_all( + '/' . Nickname::BEFORE_MENTIONS . preg_quote($preMention, '/') . '(' . Nickname::WEBFINGER_FMT . ')/', + $text, + $wmatches, + PREG_OFFSET_CAPTURE + ); + if ($result === false) { + Log::error(__METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error==' . preg_last_error() . ').'); + return []; + } elseif (($n_matches = count($wmatches)) != 0) { + Log::debug((sprintf('Found %d matches for WebFinger IDs: %s', $n_matches, print_r($wmatches, true)))); + } + return $wmatches[1]; + } + + /** + * Profile URL matches: @param string $text The text from which to extract URL mentions + * @param string $preMention Character(s) that signals a mention ('@', '!'...) + * @return array The matching URLs (without @ or acct:) and each respective position in the given string. + * @example.com/mublog/user + * + */ + public static function extractUrlMentions(string $text, string $preMention = '@'): array + { + $wmatches = []; + // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged + // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important) + $result = preg_match_all( + '/(?:^|\s+)' . preg_quote($preMention, '/') . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/', + $text, + $wmatches, + PREG_OFFSET_CAPTURE + ); + if ($result === false) { + Log::error(__METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error==' . preg_last_error() . ').'); + return []; + } elseif (count($wmatches)) { + Log::debug((sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), print_r($wmatches, true)))); + } + return $wmatches[1]; + } + + /** + * Find any explicit remote mentions. Accepted forms: + * Webfinger: @user@example.com + * Profile link: @param Actor $sender + * @param string $text input markup text + * @param $mentions + * @return bool hook return value + * @example.com/mublog/user + */ + public function onEndFindMentions(Actor $sender, string $text, array &$mentions): bool + { + $matches = []; + + foreach (self::extractWebfingerIds($text, $preMention = '@') as $wmatch) { + [$target, $pos] = $wmatch; + Log::info("Checking webfinger person '$target'"); + + $actor = null; + + $resource_parts = explode($preMention, $target); + if ($resource_parts[1] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) { + $actor = LocalUser::getWithPK(['nickname' => $resource_parts[0]])->getActor(); + } else { + Event::handle('FreeNetworkFindMentions', [$target, &$actor]); + if (is_null($actor)) { + continue; + } + } + assert($actor instanceof Actor); + + $displayName = $actor->getFullname() ?? $actor->getNickname() ?? $target; // TODO: we could do getBestName() or getFullname() here + + $matches[$pos] = [ + 'mentioned' => [$actor], + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen($target), + 'url' => $actor->getUri() + ]; + } + + foreach (self::extractUrlMentions($text) as $wmatch) { + [$target, $pos] = $wmatch; + $schemes = ['https', 'http']; + foreach ($schemes as $scheme) { + $url = "$scheme://$target"; + if (Common::isValidHttpUrl($url)) { + // This means $resource is a valid url + Log::info("Checking actor address '$url'"); + $actor = null; + $resource_parts = parse_url($url); + // TODO: Use URLMatcher + if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) { + $str = $resource_parts['path']; + // actor_view_nickname + $renick = '/\/@(' . Nickname::DISPLAY_FMT . ')\/?/m'; + // actor_view_id + $reuri = '/\/actor\/(\d+)\/?/m'; + if (preg_match_all($renick, $str, $matches, PREG_SET_ORDER, 0) === 1) { + $actor = LocalUser::getWithPK(['nickname' => $matches[0][1]])->getActor(); + } elseif (preg_match_all($reuri, $str, $matches, PREG_SET_ORDER, 0) === 1) { + $actor = Actor::getById((int) $matches[0][1]); + } else { + Log::error('Unexpected behaviour onEndFindMentions at FreeNetwork'); + throw new ServerException('Unexpected behaviour onEndFindMentions at FreeNetwork'); + } + } else { + Event::handle('FreeNetworkFindUrlMentions', [$url, &$actor]); + if (is_null($actor)) { + continue; + } + } + $displayName = $actor->getFullname() ?? $actor->getNickname() ?? $target; // TODO: we could do getBestName() or getFullname() here + $matches[$pos] = ['mentioned' => [$actor], + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen($target), + 'url' => $actor->getUri() + ]; + } else { + break; + } + } + } + + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + $pos = $other['position']; + if (isset($matches[$pos])) { + $mentions[$i] = $matches[$pos]; + unset($matches[$pos]); + } + } + foreach ($matches as $mention) { + $mentions[] = $mention; + } + + return Event::next; + } + + public static function notify(Actor $sender, Activity $activity, array $targets, ?string $reason = null): bool { $protocols = []; diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index a061aed3e5..1bbc4eb6d7 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -256,133 +256,28 @@ class ActivityPub extends Plugin return Event::next; } - /** - * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz - * - * @param string $text The text from which to extract webfinger IDs - * @param string $preMention Character(s) that signals a mention ('@', '!'...) - * @return array The matching IDs (without $preMention) and each respective position in the given string. - */ - public static function extractWebfingerIds(string $text, string $preMention = '@'): array + public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool { - $wmatches = []; - $result = preg_match_all( - '/' . Nickname::BEFORE_MENTIONS . preg_quote($preMention, '/') . '(' . Nickname::WEBFINGER_FMT . ')/', - $text, - $wmatches, - PREG_OFFSET_CAPTURE - ); - if ($result === false) { - Log::error(__METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error==' . preg_last_error() . ').'); - return []; - } elseif (($n_matches = count($wmatches)) != 0) { - Log::debug((sprintf('Found %d matches for WebFinger IDs: %s', $n_matches, print_r($wmatches, true)))); + try { + $ap_actor = ActivitypubActor::getByAddr($target); + $actor = Actor::getById($ap_actor->getActorId()); + return Event::stop; + } catch (Exception $e) { + Log::error("ActivityPub Webfinger Mention check failed: " . $e->getMessage()); + return Event::next; } - return $wmatches[1]; } - /** - * Profile URL matches: @param string $text The text from which to extract URL mentions - * @param string $preMention Character(s) that signals a mention ('@', '!'...) - * @return array The matching URLs (without @ or acct:) and each respective position in the given string. - * @example.com/mublog/user - * - */ - public static function extractUrlMentions(string $text, string $preMention = '@'): array + public function onFreeNetworkFindUrlMentions(string $url, ?Actor &$actor = null): bool { - $wmatches = []; - // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged - // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important) - $result = preg_match_all( - '/(?:^|\s+)' . preg_quote($preMention, '/') . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/', - $text, - $wmatches, - PREG_OFFSET_CAPTURE - ); - if ($result === false) { - Log::error(__METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error==' . preg_last_error() . ').'); - return []; - } elseif (count($wmatches)) { - Log::debug((sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), print_r($wmatches, true)))); + try { + $ap_actor = ActivitypubActor::fromUri($url); + $actor = Actor::getById($ap_actor->getActorId()); + return Event::stop; + } catch (Exception $e) { + Log::error("ActivityPub Actor from URL Mention check failed: " . $e->getMessage()); + return Event::next; } - return $wmatches[1]; - } - - /** - * Find any explicit remote mentions. Accepted forms: - * Webfinger: @user@example.com - * Profile link: @param Actor $sender - * @param string $text input markup text - * @param $mentions - * @return bool hook return value - * @example.com/mublog/user - */ - public function onEndFindMentions(Actor $sender, string $text, array &$mentions): bool - { - $matches = []; - - foreach (self::extractWebfingerIds($text, '@') as $wmatch) { - list($target, $pos) = $wmatch; - Log::info("Checking webfinger person '$target'"); - $profile = null; - try { - $aprofile = ActivitypubActor::getByAddr($target); - $profile = Actor::getById($aprofile->getActorId()); - } catch (Exception $e) { - Log::error("Webfinger check failed: " . $e->getMessage()); - continue; - } - assert($profile instanceof Actor); - - $displayName = $profile->getFullname() ?? $profile->getNickname() ?? $target; // TODO: we could do getBestName() or getFullname() here - - $matches[$pos] = [ - 'mentioned' => [$profile], - 'type' => 'mention', - 'text' => $displayName, - 'position' => $pos, - 'length' => mb_strlen($target), - 'url' => $aprofile->getUri() - ]; - } - - foreach (self::extractUrlMentions($text) as $wmatch) { - list($target, $pos) = $wmatch; - $schemes = array('https', 'http'); - foreach ($schemes as $scheme) { - $url = "$scheme://$target"; - Log::info("Checking profile address '$url'"); - try { - $aprofile = ActivitypubActor::fromUri($url); - $profile = Actor::getById($aprofile->getActorId()); - $displayName = $profile->getFullname() ?? $profile->getNickname() ?? $target; // TODO: we could do getBestName() or getFullname() here - $matches[$pos] = ['mentioned' => [$profile], - 'type' => 'mention', - 'text' => $displayName, - 'position' => $pos, - 'length' => mb_strlen($target), - 'url' => $aprofile->getUri() - ]; - break; - } catch (Exception $e) { - Log::error("Profile check failed: " . $e->getMessage()); - } - } - } - - foreach ($mentions as $i => $other) { - // If we share a common prefix with a local user, override it! - $pos = $other['position']; - if (isset($matches[$pos])) { - $mentions[$i] = $matches[$pos]; - unset($matches[$pos]); - } - } - foreach ($matches as $mention) { - $mentions[] = $mention; - } - - return Event::next; } /**