X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FReceiver.php;h=5ee81302f2cf0d85a23a3601410635c646d2fa40;hb=0104da3c9392725c7bc524b60708c1e6c4dae7d9;hp=2cc165b233d7e230a44f7479b59a5f11633e8fc2;hpb=8821d33f73785884cfce83e7b23d3ef19cc1bc11;p=friendica.git diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 2cc165b233..5ee81302f2 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,24 +53,25 @@ 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) { $http_signer = HTTPSignature::getSigner($body, $header); if (empty($http_signer)) { - Logger::log('Invalid HTTP signature, message will be discarded.', Logger::DEBUG); + Logger::warning('Invalid HTTP signature, message will be discarded.'); return; } else { - Logger::log('HTTP signature is signed by ' . $http_signer, Logger::DEBUG); + Logger::info('Valid HTTP signature', ['signer' => $http_signer]); } $activity = json_decode($body, true); if (empty($activity)) { - Logger::log('Invalid body.', Logger::DEBUG); + Logger::warning('Invalid body.'); return; } @@ -77,7 +79,7 @@ class Receiver $actor = JsonLD::fetchElement($ldactivity, 'as:actor'); - Logger::log('Message for user ' . $uid . ' is from actor ' . $actor, Logger::DEBUG); + Logger::info('Message for user ' . $uid . ' is from actor ' . $actor); if (LDSignature::isSigned($activity)) { $ld_signer = LDSignature::getSigner($activity); @@ -113,9 +115,11 @@ class Receiver * * @param array $activity * @param string $object_id Object ID of the the provided object - * @param integer $uid User ID + * @param integer $uid User ID * * @return string with object type + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ private static function fetchObjectType($activity, $object_id, $uid = 0) { @@ -151,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) { @@ -202,7 +208,8 @@ class Receiver } // 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); @@ -232,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); @@ -262,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) { @@ -288,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) { @@ -308,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)) { @@ -320,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'])) { @@ -357,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; @@ -413,11 +439,12 @@ class Receiver /** * Fetch the receiver list from an activity array * - * @param array $activity + * @param array $activity * @param string $actor - * @param array $tags + * @param array $tags * * @return array with receivers (user id) + * @throws \Exception */ private static function getReceivers($activity, $actor, $tags = []) { @@ -455,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)) { @@ -472,7 +499,7 @@ class Receiver } // Fetching all directly addressed receivers - $condition = ['self' => true, 'nurl' => normalise_link($receiver)]; + $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)]; $contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition); if (!DBA::isResult($contact)) { continue; @@ -480,13 +507,13 @@ class Receiver // 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::ACCOUNT_TYPE_COMMUNITY)) { + if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; - $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], + $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::ACCOUNT_TYPE_COMMUNITY) { + if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) { $condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER]; } @@ -508,15 +535,16 @@ class Receiver * Fetch the receiver list of a given actor * * @param string $actor - * @param array $tags + * @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' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER], + $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)) { @@ -531,11 +559,12 @@ class Receiver /** * Tests if the contact is a valid receiver for this actor * - * @param array $contact + * @param array $contact * @param string $actor - * @param array $tags + * @param array $tags * - * @return array with receivers (user id) + * @return bool with receivers (user id) + * @throws \Exception */ private static function isValidReceiverForActor($contact, $actor, $tags) { @@ -551,7 +580,7 @@ class Receiver // 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::ACCOUNT_TYPE_COMMUNITY)) { + if (empty($owner) || ($owner['contact-type'] != Contact::TYPE_COMMUNITY)) { return false; } @@ -574,7 +603,9 @@ class Receiver * * @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) { @@ -588,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); @@ -606,6 +638,8 @@ class Receiver * * @param $receivers * @param $actor + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException */ private static function switchContacts($receivers, $actor) { @@ -614,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); } @@ -629,10 +663,10 @@ class Receiver /** * * - * @param $object_data + * @param $object_data * @param array $activity * - * @return + * @return mixed */ private static function addActivityFields($object_data, $activity) { @@ -658,6 +692,8 @@ class Receiver * @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, $uid = 0) { @@ -727,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 * @@ -768,6 +838,7 @@ class Receiver * @param array $object * * @return array + * @throws \Exception */ private static function processObject($object) { @@ -778,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']; } @@ -821,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');