]> git.mxchange.org Git - friendica.git/blobdiff - src/Protocol/ActivityPub/Receiver.php
Summary has to be converted to HTML / Don't use summary when we have the source
[friendica.git] / src / Protocol / ActivityPub / Receiver.php
index 2cc165b233d7e230a44f7479b59a5f11633e8fc2..5ee81302f2cf0d85a23a3601410635c646d2fa40 100644 (file)
@@ -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');