X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FReceiver.php;h=e0ee1f0f90e71ac6a200580dcddb1e0d13aefd66;hb=b2c29715519b1712abce6acb592caedbc8ac8710;hp=80c2a5f08492659f674f4ddef48b844fae444c5a;hpb=50da89d861dce3b648c8f9e5c1e4c480ee320a43;p=friendica.git diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 80c2a5f084..e0ee1f0f90 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -5,18 +5,19 @@ namespace Friendica\Protocol\ActivityPub; use Friendica\Database\DBA; -use Friendica\Util\HTTPSignature; use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Model\Contact; use Friendica\Model\APContact; +use Friendica\Model\Conversation; use Friendica\Model\Item; use Friendica\Model\User; -use Friendica\Util\JsonLD; -use Friendica\Util\LDSignature; use Friendica\Protocol\ActivityPub; -use Friendica\Model\Conversation; use Friendica\Util\DateTimeFormat; +use Friendica\Util\HTTPSignature; +use Friendica\Util\JsonLD; +use Friendica\Util\LDSignature; +use Friendica\Util\Strings; /** * @brief ActivityPub Receiver Protocol class @@ -41,7 +42,7 @@ class Receiver /** * Checks if the web request is done for the AP protocol * - * @return is it AP? + * @return bool is it AP? */ public static function isRequest() { @@ -52,9 +53,10 @@ class Receiver /** * Checks incoming message from the inbox * - * @param $body - * @param $header + * @param $body + * @param $header * @param integer $uid User ID + * @throws \Exception */ public static function processInbox($body, $header, $uid) { @@ -111,12 +113,15 @@ class Receiver /** * Fetches the object type for a given object id * - * @param array $activity - * @param string $object_id Object ID of the the provided object + * @param array $activity + * @param string $object_id Object ID of the the provided object + * @param integer $uid User ID * * @return string with object type + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ - private static function fetchObjectType($activity, $object_id) + private static function fetchObjectType($activity, $object_id, $uid = 0) { if (!empty($activity['as:object'])) { $object_type = JsonLD::fetchElement($activity['as:object'], '@type'); @@ -135,7 +140,7 @@ class Receiver return 'as:' . $profile['type']; } - $data = ActivityPub::fetchContent($object_id); + $data = ActivityPub::fetchContent($object_id, $uid); if (!empty($data)) { $object = JsonLD::compact($data); $type = JsonLD::fetchElement($object, '@type'); @@ -150,11 +155,13 @@ class Receiver /** * Prepare the object array * - * @param array $activity + * @param array $activity * @param integer $uid User ID - * @param $trust_source + * @param $trust_source * * @return array with object data + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ private static function prepareObjectData($activity, $uid, &$trust_source) { @@ -171,12 +178,15 @@ class Receiver // When it is a delivery to a personal inbox we add that user to the receivers if (!empty($uid)) { - $owner = User::getOwnerDataById($uid); $additional = ['uid:' . $uid => $uid]; $receivers = array_merge($receivers, $additional); + } else { + // We possibly need some user to fetch private content, + // so we fetch the first out ot the list. + $uid = self::getFirstUserFromReceivers($receivers); } - Logger::log('Receivers: ' . json_encode($receivers), Logger::DEBUG); + Logger::log('Receivers: ' . $uid . ' - ' . json_encode($receivers), Logger::DEBUG); $object_id = JsonLD::fetchElement($activity, 'as:object'); if (empty($object_id)) { @@ -184,21 +194,22 @@ class Receiver return []; } - $object_type = self::fetchObjectType($activity, $object_id); + $object_type = self::fetchObjectType($activity, $object_id, $uid); // Fetch the content only on activities where this matters if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) { if ($type == 'as:Announce') { $trust_source = false; } - $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source); + $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $uid); if (empty($object_data)) { Logger::log("Object data couldn't be processed", Logger::DEBUG); return []; } // We had been able to retrieve the object data - so we can trust the source $trust_source = true; - } elseif (in_array($type, ['as:Like', 'as:Dislike'])) { + } elseif (in_array($type, ['as:Like', 'as:Dislike']) || + (($type == 'as:Follow') && in_array($object_type, self::CONTENT_TYPES))) { // Create a mostly empty array out of the activity data (instead of the object). // This way we later don't have to check for the existence of ech individual array element. $object_data = self::processObject($activity); @@ -216,7 +227,7 @@ class Receiver // An Undo is done on the object of an object, so we need that type as well if ($type == 'as:Undo') { - $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object']); + $object_data['object_object_type'] = self::fetchObjectType([], $object_data['object_object'], $uid); } } @@ -228,6 +239,7 @@ class Receiver $object_data['type'] = $type; $object_data['actor'] = $actor; + $object_data['item_receiver'] = $receivers; $object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers); Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG); @@ -235,6 +247,22 @@ class Receiver return $object_data; } + /** + * Fetches the first user id from the receiver array + * + * @param array $receivers Array with receivers + * @return integer user id; + */ + public static function getFirstUserFromReceivers($receivers) + { + foreach ($receivers as $receiver) { + if (!empty($receiver)) { + return $receiver; + } + } + return 0; + } + /** * Store the unprocessed data into the conversation table * This has to be done outside the regular function, @@ -242,6 +270,7 @@ class Receiver * * @param array $activity Array with activity data * @param string $body The raw message + * @throws \Exception */ private static function storeConversation($activity, $body) { @@ -268,6 +297,7 @@ class Receiver * @param string $body * @param integer $uid User ID * @param boolean $trust_source Do we trust the source? + * @throws \Exception */ public static function processActivity($activity, $body = '', $uid = null, $trust_source = false) { @@ -288,6 +318,16 @@ class Receiver } + // Don't trust the source if "actor" differs from "attributedTo". The content could be forged. + if ($trust_source && ($type == 'as:Create') && is_array($activity['as:object'])) { + $actor = JsonLD::fetchElement($activity, 'as:actor'); + $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo'); + $trust_source = ($actor == $attributed_to); + if (!$trust_source) { + Logger::log('Not trusting actor: ' . $actor . '. It differs from attributedTo: ' . $attributed_to, Logger::DEBUG); + } + } + // $trust_source is called by reference and is set to true if the content was retrieved successfully $object_data = self::prepareObjectData($activity, $uid, $trust_source); if (empty($object_data)) { @@ -300,7 +340,10 @@ class Receiver return; } - self::storeConversation($object_data, $body); + // Only store content related stuff - and no announces, since they possibly overwrite the original content + if (in_array($object_data['object_type'], self::CONTENT_TYPES) && ($type != 'as:Announce')) { + self::storeConversation($object_data, $body); + } // Internal flag for thread completion. See Processor.php if (!empty($activity['thread-completion'])) { @@ -337,21 +380,24 @@ class Receiver if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::updateItem($object_data); } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) { - ActivityPub\Processor::updatePerson($object_data, $body); + ActivityPub\Processor::updatePerson($object_data); } break; case 'as:Delete': if ($object_data['object_type'] == 'as:Tombstone') { - ActivityPub\Processor::deleteItem($object_data, $body); + ActivityPub\Processor::deleteItem($object_data); } elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) { - ActivityPub\Processor::deletePerson($object_data, $body); + ActivityPub\Processor::deletePerson($object_data); } break; case 'as:Follow': if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) { ActivityPub\Processor::followUser($object_data); + } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { + $object_data['reply-to-id'] = $object_data['object_id']; + ActivityPub\Processor::createActivity($object_data, ACTIVITY_FOLLOW); } break; @@ -393,12 +439,14 @@ class Receiver /** * Fetch the receiver list from an activity array * - * @param array $activity + * @param array $activity * @param string $actor + * @param array $tags * * @return array with receivers (user id) + * @throws \Exception */ - private static function getReceivers($activity, $actor) + private static function getReceivers($activity, $actor, $tags = []) { $receivers = []; @@ -434,7 +482,7 @@ class Receiver if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) { // This will most likely catch all OStatus connections to Mastodon - $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND] + $condition = ['alias' => [$actor, Strings::normaliseLink($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND] , 'archive' => false, 'pending' => false]; $contacts = DBA::select('contact', ['uid'], $condition); while ($contact = DBA::fetch($contacts)) { @@ -446,24 +494,34 @@ class Receiver } if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) { - $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; - $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], - 'network' => $networks, 'archive' => false, 'pending' => false]; - $contacts = DBA::select('contact', ['uid'], $condition); - while ($contact = DBA::fetch($contacts)) { - if ($contact['uid'] != 0) { - $receivers['uid:' . $contact['uid']] = $contact['uid']; - } - } - DBA::close($contacts); + $receivers = array_merge($receivers, self::getReceiverForActor($actor, $tags)); continue; } - $condition = ['self' => true, 'nurl' => normalise_link($receiver)]; - $contact = DBA::selectFirst('contact', ['uid'], $condition); + // Fetching all directly addressed receivers + $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)]; + $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition); if (!DBA::isResult($contact)) { continue; } + + // Check if the potential receiver is following the actor + // Exception: The receiver is targetted via "to" or this is a comment + if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { + $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], + 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']]; + + // Forum posts are only accepted from forum contacts + if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) { + $condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER]; + } + + if (!DBA::exists('contact', $condition)) { + continue; + } + } + $receivers['uid:' . $contact['uid']] = $contact['uid']; } } @@ -473,12 +531,81 @@ class Receiver return $receivers; } + /** + * Fetch the receiver list of a given actor + * + * @param string $actor + * @param array $tags + * + * @return array with receivers (user id) + * @throws \Exception + */ + public static function getReceiverForActor($actor, $tags) + { + $receivers = []; + $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER], + 'network' => $networks, 'archive' => false, 'pending' => false]; + $contacts = DBA::select('contact', ['uid', 'rel'], $condition); + while ($contact = DBA::fetch($contacts)) { + if (self::isValidReceiverForActor($contact, $actor, $tags)) { + $receivers['uid:' . $contact['uid']] = $contact['uid']; + } + } + DBA::close($contacts); + return $receivers; + } + + /** + * Tests if the contact is a valid receiver for this actor + * + * @param array $contact + * @param string $actor + * @param array $tags + * + * @return bool with receivers (user id) + * @throws \Exception + */ + private static function isValidReceiverForActor($contact, $actor, $tags) + { + // Public contacts are no valid receiver + if ($contact['uid'] == 0) { + return false; + } + + // Are we following the contact? Then this is a valid receiver + if (in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])) { + return true; + } + + // When the possible receiver isn't a community, then it is no valid receiver + $owner = User::getOwnerDataById($contact['uid']); + if (empty($owner) || ($owner['contact-type'] != Contact::TYPE_COMMUNITY)) { + return false; + } + + // Is the community account tagged? + foreach ($tags as $tag) { + if ($tag['type'] != 'Mention') { + continue; + } + + if ($tag['href'] == $owner['url']) { + return true; + } + } + + return false; + } + /** * Switches existing contacts to ActivityPub * * @param integer $cid Contact ID * @param integer $uid User ID - * @param string $url Profile URL + * @param string $url Profile URL + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ public static function switchContact($cid, $uid, $url) { @@ -492,8 +619,9 @@ class Receiver $photo = defaults($profile, 'photo', null); unset($profile['photo']); unset($profile['baseurl']); + unset($profile['guid']); - $profile['nurl'] = normalise_link($profile['url']); + $profile['nurl'] = Strings::normaliseLink($profile['url']); DBA::update('contact', $profile, ['id' => $cid]); Contact::updateAvatar($photo, $uid, $cid); @@ -510,6 +638,8 @@ class Receiver * * @param $receivers * @param $actor + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ private static function switchContacts($receivers, $actor) { @@ -518,12 +648,12 @@ class Receiver } foreach ($receivers as $receiver) { - $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => normalise_link($actor)]); + $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]); if (DBA::isResult($contact)) { self::switchContact($contact['id'], $receiver, $actor); } - $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [normalise_link($actor), $actor]]); + $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]); if (DBA::isResult($contact)) { self::switchContact($contact['id'], $receiver, $actor); } @@ -533,10 +663,10 @@ class Receiver /** * * - * @param $object_data + * @param $object_data * @param array $activity * - * @return + * @return mixed */ private static function addActivityFields($object_data, $activity) { @@ -559,16 +689,19 @@ class Receiver * @param string $object_id Object ID of the the provided object * @param array $object The provided object array * @param boolean $trust_source Do we trust the provided object? + * @param integer $uid User ID for the signature that we use to fetch data * * @return array with trusted and valid object data + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ - private static function fetchObject($object_id, $object = [], $trust_source = false) + private static function fetchObject($object_id, $object = [], $trust_source = false, $uid = 0) { // By fetching the type we check if the object is complete. $type = JsonLD::fetchElement($object, '@type'); if (!$trust_source || empty($type)) { - $data = ActivityPub::fetchContent($object_id); + $data = ActivityPub::fetchContent($object_id, $uid); if (!empty($data)) { $object = JsonLD::compact($data); Logger::log('Fetched content for ' . $object_id, Logger::DEBUG); @@ -604,7 +737,7 @@ class Receiver if (empty($object_id)) { return false; } - return self::fetchObject($object_id); + return self::fetchObject($object_id, [], false, $uid); } Logger::log('Unhandled object type: ' . $type, Logger::DEBUG); @@ -630,13 +763,47 @@ class Receiver continue; } - $taglist[] = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')), + $element = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')), 'href' => JsonLD::fetchElement($tag, 'as:href'), 'name' => JsonLD::fetchElement($tag, 'as:name')]; + + if (empty($element['type'])) { + continue; + } + + $taglist[] = $element; } return $taglist; } + /** + * Convert emojis from JSON-LD format into a simplified format + * + * @param $emojis + * @return array with emojis in a simplified format + */ + private static function processEmojis($emojis) + { + $emojilist = []; + + if (empty($emojis)) { + return []; + } + + foreach ($emojis as $emoji) { + if (empty($emoji) || (JsonLD::fetchElement($emoji, '@type') != 'toot:Emoji') || empty($emoji['as:icon'])) { + continue; + } + + $url = JsonLD::fetchElement($emoji['as:icon'], 'as:url'); + $element = ['name' => JsonLD::fetchElement($emoji, 'as:name'), + 'href' => $url]; + + $emojilist[] = $element; + } + return $emojilist; + } + /** * Convert attachments from JSON-LD format into a simplified format * @@ -671,6 +838,7 @@ class Receiver * @param array $object * * @return array + * @throws \Exception */ private static function processObject($object) { @@ -681,10 +849,10 @@ class Receiver $object_data = []; $object_data['object_type'] = JsonLD::fetchElement($object, '@type'); $object_data['id'] = JsonLD::fetchElement($object, '@id'); - $object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo'); - if (empty($object_data['reply-to-id'])) { + // An empty "id" field is translated to "./" by the compactor, so we have to check for this content + if (empty($object_data['reply-to-id']) || ($object_data['reply-to-id'] == './')) { $object_data['reply-to-id'] = $object_data['id']; } @@ -724,6 +892,7 @@ class Receiver $object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value'); $object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment')); $object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag')); + $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', 'toot:Emoji')); $object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application'); $object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url'); @@ -736,7 +905,7 @@ class Receiver } } - $object_data['receiver'] = self::getReceivers($object, $object_data['actor']); + $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags']); // Common object data: