diff --git a/src/Model/APContact.php b/src/Model/APContact.php index 1ae34a40a8..7bdde60c65 100644 --- a/src/Model/APContact.php +++ b/src/Model/APContact.php @@ -31,11 +31,13 @@ use Friendica\DI; use Friendica\Network\Probe; use Friendica\Protocol\ActivityNamespace; use Friendica\Protocol\ActivityPub; +use Friendica\Protocol\ActivityPub\Transmitter; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; use Friendica\Util\JsonLD; use Friendica\Util\Network; +use Friendica\Util\Strings; class APContact { @@ -52,6 +54,20 @@ class APContact return []; } + if (Contact::isLocal($addr) && ($local_uid = User::getIdForURL($addr)) && ($local_owner = User::getOwnerDataById($local_uid))) { + $data = [ + 'addr' => $local_owner['addr'], + 'baseurl' => $local_owner['baseurl'], + 'url' => $local_owner['url'], + 'subscribe' => $local_owner['baseurl'] . '/follow?url={uri}']; + + if (!empty($local_owner['alias']) && ($local_owner['url'] != $local_owner['alias'])) { + $data['alias'] = $local_owner['alias']; + } + + return $data; + } + $data = ['addr' => $addr]; $template = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr); $webfinger = Probe::webfinger(str_replace('{uri}', urlencode($addr), $template), 'application/jrd+json'); @@ -148,39 +164,51 @@ class APContact $url = $apcontact['url']; } - $curlResult = HTTPSignature::fetchRaw($url); - $failed = empty($curlResult) || empty($curlResult->getBody()) || - (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); - - if (!$failed) { - $data = json_decode($curlResult->getBody(), true); - $failed = empty($data) || !is_array($data); - } - - if (!$failed && ($curlResult->getReturnCode() == 410)) { - $data = ['@context' => ActivityPub::CONTEXT, 'id' => $url, 'type' => 'Tombstone']; - } - - if ($failed) { - self::markForArchival($fetched_contact ?: []); - return $fetched_contact; - } - - $compacted = JsonLD::compact($data); - if (empty($compacted['@id'])) { - return $fetched_contact; - } - // Detect multiple fast repeating request to the same address // See https://github.com/friendica/friendica/issues/9303 $cachekey = 'apcontact:getByURL:' . $url; $result = DI::cache()->get($cachekey); if (!is_null($result)) { Logger::notice('Multiple requests for the address', ['url' => $url, 'update' => $update, 'callstack' => System::callstack(20), 'result' => $result]); + if (!empty($fetched_contact)) { + return $fetched_contact; + } } else { DI::cache()->set($cachekey, System::callstack(20), Duration::FIVE_MINUTES); } + if (Network::isLocalLink($url) && ($local_uid = User::getIdForURL($url))) { + $data = Transmitter::getProfile($local_uid); + $local_owner = User::getOwnerDataById($local_uid); + } + + if (empty($data)) { + $local_owner = []; + + $curlResult = HTTPSignature::fetchRaw($url); + $failed = empty($curlResult) || empty($curlResult->getBody()) || + (!$curlResult->isSuccess() && ($curlResult->getReturnCode() != 410)); + + if (!$failed) { + $data = json_decode($curlResult->getBody(), true); + $failed = empty($data) || !is_array($data); + } + + if (!$failed && ($curlResult->getReturnCode() == 410)) { + $data = ['@context' => ActivityPub::CONTEXT, 'id' => $url, 'type' => 'Tombstone']; + } + + if ($failed) { + self::markForArchival($fetched_contact ?: []); + return $fetched_contact; + } + } + + $compacted = JsonLD::compact($data); + if (empty($compacted['@id'])) { + return $fetched_contact; + } + $apcontact['url'] = $compacted['@id']; $apcontact['uuid'] = JsonLD::fetchElement($compacted, 'diaspora:guid', '@value'); $apcontact['type'] = str_replace('as:', '', JsonLD::fetchElement($compacted, '@type')); @@ -264,9 +292,13 @@ class APContact } if (!empty($apcontact['following'])) { - $following = ActivityPub::fetchContent($apcontact['following']); + if (!empty($local_owner)) { + $following = ActivityPub\Transmitter::getContacts($local_owner, [Contact::SHARING, Contact::FRIEND], 'following'); + } else { + $following = ActivityPub::fetchContent($apcontact['following']); + } if (!empty($following['totalItems'])) { - // Mastodon seriously allows for this condition? + // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.social/@BLUW for a negative following count if ($following['totalItems'] < 0) { $following['totalItems'] = 0; @@ -276,9 +308,13 @@ class APContact } if (!empty($apcontact['followers'])) { - $followers = ActivityPub::fetchContent($apcontact['followers']); + if (!empty($local_owner)) { + $followers = ActivityPub\Transmitter::getContacts($local_owner, [Contact::FOLLOWER, Contact::FRIEND], 'followers'); + } else { + $followers = ActivityPub::fetchContent($apcontact['followers']); + } if (!empty($followers['totalItems'])) { - // Mastodon seriously allows for this condition? + // Mastodon seriously allows for this condition? // Jul 14 2021 - See https://mastodon.online/@goes11 for a negative followers count if ($followers['totalItems'] < 0) { $followers['totalItems'] = 0; @@ -288,7 +324,11 @@ class APContact } if (!empty($apcontact['outbox'])) { - $outbox = ActivityPub::fetchContent($apcontact['outbox']); + if (!empty($local_owner)) { + $outbox = ActivityPub\Transmitter::getOutbox($local_owner); + } else { + $outbox = ActivityPub::fetchContent($apcontact['outbox']); + } if (!empty($outbox['totalItems'])) { $apcontact['statuses_count'] = $outbox['totalItems']; } diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 12cba0c080..20135a49e5 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -453,6 +453,11 @@ class Contact */ public static function isLocal($url) { + if (!parse_url($url, PHP_URL_SCHEME)) { + $addr_parts = explode('@', $url); + return (count($addr_parts) == 2) && ($addr_parts[1] == DI::baseUrl()->getHostname()); + } + return Strings::compareLink(self::getBasepath($url, true), DI::baseUrl()); } @@ -1808,7 +1813,7 @@ class Contact // User contacts use are updated through the public contacts if (($uid != 0) && !in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { - $pcid = self::getIdForURL($contact['url'], false); + $pcid = self::getIdForURL($contact['url'], 0, false); if (!empty($pcid)) { Logger::debug('Update the private contact via the public contact', ['id' => $cid, 'uid' => $uid, 'public' => $pcid]); self::updateAvatar($pcid, $avatar, $force, true); @@ -2117,7 +2122,7 @@ class Contact } if (Strings::normaliseLink($ret['url']) != Strings::normaliseLink($contact['url'])) { - $cid = self::getIdForURL($ret['url']); + $cid = self::getIdForURL($ret['url'], 0, false); if (!empty($cid) && ($cid != $id)) { Logger::notice('URL of contact changed.', ['id' => $id, 'new_id' => $cid, 'old' => $contact['url'], 'new' => $ret['url']]); return self::updateFromProbeArray($cid, $ret); diff --git a/src/Model/Photo.php b/src/Model/Photo.php index 61fc2df4ea..30e6668987 100644 --- a/src/Model/Photo.php +++ b/src/Model/Photo.php @@ -804,30 +804,33 @@ class Photo } /** - * Returns the GUID from picture links + * Fetch the guid and scale from picture links * * @param string $name Picture link - * @return string GUID - * @throws \Exception + * @return array */ - public static function getGUID($name) + public static function getResourceData(string $name):array { $base = DI::baseUrl()->get(); $guid = str_replace([Strings::normaliseLink($base), '/photo/'], '', Strings::normaliseLink($name)); + if (parse_url($guid, PHP_URL_SCHEME)) { + return []; + } + $guid = self::stripExtension($guid); if (substr($guid, -2, 1) != "-") { - return ''; + return []; } $scale = intval(substr($guid, -1, 1)); if (!is_numeric($scale)) { - return ''; + return []; } $guid = substr($guid, 0, -2); - return $guid; + return ['guid' => $guid, 'scale' => $scale]; } /** @@ -839,13 +842,12 @@ class Photo */ public static function isLocal($name) { - $guid = self::getGUID($name); - - if (empty($guid)) { + $data = self::getResourceData($name); + if (empty($data)) { return false; } - return DBA::exists('photo', ['resource-id' => $guid]); + return DBA::exists('photo', ['resource-id' => $data['guid'], 'scale' => $data['scale']]); } /** diff --git a/src/Network/HTTPRequest.php b/src/Network/HTTPRequest.php index bd31ac1e14..622828b434 100644 --- a/src/Network/HTTPRequest.php +++ b/src/Network/HTTPRequest.php @@ -74,6 +74,10 @@ class HTTPRequest implements IHTTPRequest { $stamp1 = microtime(true); + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (strlen($url) > 1000) { $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); $this->profiler->saveTimestamp($stamp1, 'network'); @@ -226,6 +230,10 @@ class HTTPRequest implements IHTTPRequest { $stamp1 = microtime(true); + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (Network::isUrlBlocked($url)) { $this->logger->info('Domain is blocked.' . ['url' => $url]); $this->profiler->saveTimestamp($stamp1, 'network'); @@ -328,6 +336,10 @@ class HTTPRequest implements IHTTPRequest */ public function finalUrl(string $url, int $depth = 1, bool $fetchbody = false) { + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + if (Network::isUrlBlocked($url)) { $this->logger->info('Domain is blocked.', ['url' => $url]); return $url; diff --git a/src/Network/Probe.php b/src/Network/Probe.php index e44a8d326e..9ee338e8fd 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -333,7 +333,7 @@ class Probe public static function uri($uri, $network = '', $uid = -1) { // Local profiles aren't probed via network - if (empty($network) && strpos($uri, DI::baseUrl()->getHostname())) { + if (empty($network) && Contact::isLocal($uri)) { $data = self::localProbe($uri); if (!empty($data)) { return $data; @@ -2201,39 +2201,33 @@ class Probe */ private static function localProbe(string $url) { - $uid = User::getIdForURL($url); - if (empty($uid)) { - return []; - } + if ($uid = User::getIdForURL($url)) { + $profile = User::getOwnerDataById($uid); + $approfile = ActivityPub\Transmitter::getProfile($uid); - $profile = User::getOwnerDataById($uid); - if (empty($profile)) { - return []; - } + if (empty($profile['gsid'])) { + $profile['gsid'] = GServer::getID($approfile['generator']['url']); + } - $approfile = ActivityPub\Transmitter::getProfile($uid); - if (empty($approfile)) { - return []; + $data = ['name' => $profile['name'], 'nick' => $profile['nick'], 'guid' => $approfile['diaspora:guid'] ?? '', + 'url' => $profile['url'], 'addr' => $profile['addr'], 'alias' => $profile['alias'], + 'photo' => Contact::getAvatarUrlForId($profile['id'], $profile['updated']), + 'header' => $profile['header'] ? Contact::getHeaderUrlForId($profile['id'], $profile['updated']) : '', + 'account-type' => $profile['contact-type'], 'community' => ($profile['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY), + 'keywords' => $profile['keywords'], 'location' => $profile['location'], 'about' => $profile['about'], + 'hide' => !$profile['net-publish'], 'batch' => '', 'notify' => $profile['notify'], + 'poll' => $profile['poll'], 'request' => $profile['request'], 'confirm' => $profile['confirm'], + 'subscribe' => $approfile['generator']['url'] . '/follow?url={uri}', 'poco' => $profile['poco'], + 'following' => $approfile['following'], 'followers' => $approfile['followers'], + 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], + 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, + 'pubkey' => $profile['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $profile['gsid'], + 'manually-approve' => in_array($profile['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP])]; + } else { + // Default values for non existing targets + $data = ['name' => $url, 'nick' => $url, 'url' => $url, 'network' => Protocol::PHANTOM, + 'photo' => DI::baseUrl() . Contact::DEFAULT_AVATAR_PHOTO]; } - - if (empty($profile['gsid'])) { - $profile['gsid'] = GServer::getID($approfile['generator']['url']); - } - - $data = ['name' => $profile['name'], 'nick' => $profile['nick'], 'guid' => $approfile['diaspora:guid'] ?? '', - 'url' => $profile['url'], 'addr' => $profile['addr'], 'alias' => $profile['alias'], - 'photo' => Contact::getAvatarUrlForId($profile['id'], $profile['updated']), - 'header' => $profile['header'] ? Contact::getHeaderUrlForId($profile['id'], $profile['updated']) : '', - 'account-type' => $profile['contact-type'], 'community' => ($profile['contact-type'] == User::ACCOUNT_TYPE_COMMUNITY), - 'keywords' => $profile['keywords'], 'location' => $profile['location'], 'about' => $profile['about'], - 'hide' => !$profile['net-publish'], 'batch' => '', 'notify' => $profile['notify'], - 'poll' => $profile['poll'], 'request' => $profile['request'], 'confirm' => $profile['confirm'], - 'subscribe' => $approfile['generator']['url'] . '/follow?url={uri}', 'poco' => $profile['poco'], - 'following' => $approfile['following'], 'followers' => $approfile['followers'], - 'inbox' => $approfile['inbox'], 'outbox' => $approfile['outbox'], - 'sharedinbox' => $approfile['endpoints']['sharedInbox'], 'network' => Protocol::DFRN, - 'pubkey' => $profile['upubkey'], 'baseurl' => $approfile['generator']['url'], 'gsid' => $profile['gsid'], - 'manually-approve' => in_array($profile['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP])]; return self::rearrangeData($data); } } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 134f69f4a6..0f62ab5376 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -22,8 +22,6 @@ namespace Friendica\Protocol; use Friendica\Core\Protocol; -use Friendica\Database\DBA; -use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\User; use Friendica\Util\HTTPSignature; diff --git a/src/Util/Images.php b/src/Util/Images.php index 7b11ea3f6b..bf84ee6c22 100644 --- a/src/Util/Images.php +++ b/src/Util/Images.php @@ -22,8 +22,8 @@ namespace Friendica\Util; use Friendica\Core\Logger; -use Friendica\Core\System; use Friendica\DI; +use Friendica\Model\Photo; /** * Image utilities @@ -184,7 +184,17 @@ class Images return $data; } - $img_str = DI::httpRequest()->fetch($url, 4); + if (Network::isLocalLink($url) && ($data = Photo::getResourceData($url))) { + $photo = Photo::selectFirst([], ['resource-id' => $data['guid'], 'scale' => $data['scale']]); + if (!empty($photo)) { + $img_str = Photo::getImageDataForPhoto($photo); + } + // @todo Possibly add a check for locally stored files + } + + if (empty($img_str)) { + $img_str = DI::httpRequest()->fetch($url, 4); + } if (!$img_str) { return [];