Merge pull request #10508 from annando/loop-prevention
Prevent endless loop when updating contact by probe
This commit is contained in:
commit
7cbe1e3ca2
7 changed files with 137 additions and 76 deletions
|
@ -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'];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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']]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 [];
|
||||
|
|
Loading…
Reference in a new issue