]> git.mxchange.org Git - friendica.git/commitdiff
Use "author_handle" for the author handle extracted from Diaspora XML messages
authorMichael <heluecht@pirati.ca>
Sat, 28 Aug 2021 08:42:20 +0000 (08:42 +0000)
committerHypolite Petovan <hypolite@mrpetovan.com>
Mon, 5 Dec 2022 05:00:49 +0000 (00:00 -0500)
- We have structured data under the variable name "author"
- Remove unused $uid parameter from Diaspora::storeByGuid
- Convert $person parameter to just its URL in Diaspora::authorContactByUrl
- Flip parameters in getUriFromGuid to get rid of $onlyfound

src/Protocol/Diaspora.php
src/Protocol/WebFingerUri.php [new file with mode: 0644]
tests/src/Protocol/WebFingerUriTest.php [new file with mode: 0644]

index 54f09e9d970c8ce209249a39a609b17d2c989b90..7fd899f8dd8cdc059dd24d6ea95f1fea2028caf3 100644 (file)
@@ -42,6 +42,7 @@ use Friendica\Model\Post;
 use Friendica\Model\Tag;
 use Friendica\Model\User;
 use Friendica\Network\HTTPClient\Client\HttpClientAccept;
+use Friendica\Network\HTTPException\InternalServerErrorException;
 use Friendica\Network\Probe;
 use Friendica\Util\Crypto;
 use Friendica\Util\DateTimeFormat;
@@ -161,8 +162,12 @@ class Diaspora
                        return false;
                }
 
-               $key = self::key($handle);
-               if ($key == '') {
+               try {
+                       $key = self::key(WebFingerUri::fromString($handle));
+                       if ($key == '') {
+                               throw new \InvalidArgumentException();
+                       }
+               } catch (\InvalidArgumentException $e) {
                        Logger::notice("Couldn't get a key for handle " . $handle . ". Discarding.");
                        return false;
                }
@@ -300,8 +305,13 @@ class Diaspora
                        }
                }
 
-               $key = self::key($author_addr);
-               if ($key == '') {
+               try {
+                       $author = WebFingerUri::fromString($author_addr);
+                       $key = self::key($author);
+                       if ($key == '') {
+                               throw new \InvalidArgumentException();
+                       }
+               } catch (\InvalidArgumentException $e) {
                        Logger::notice("Couldn't get a key for handle " . $author_addr . ". Discarding.");
                        if ($no_exit) {
                                return false;
@@ -322,8 +332,8 @@ class Diaspora
 
                return [
                        'message' => (string)Strings::base64UrlDecode($base->data),
-                       'author' => XML::unescape($author_addr),
-                       'key' => (string)$key
+                       'author'  => $author->getAddr(),
+                       'key'     => (string)$key
                ];
        }
 
@@ -356,7 +366,7 @@ class Diaspora
 
                if ($children->header) {
                        $public = true;
-                       $author_link = str_replace('acct:', '', $children->header->author_id);
+                       $idom = $children->header;
                } else {
                        // This happens with posts from a relais
                        if (empty($privKey)) {
@@ -384,8 +394,13 @@ class Diaspora
 
                        $inner_iv = base64_decode($idom->iv);
                        $inner_aes_key = base64_decode($idom->aes_key);
+               }
 
-                       $author_link = str_replace('acct:', '', $idom->author_id);
+               try {
+                       $author = WebFingerUri::fromString($idom->author_id);
+               } catch (\Throwable $e) {
+                       Logger::notice('Could not retrieve author URI.', ['idom' => $idom]);
+                       throw new \Friendica\Network\HTTPException\BadRequestException();
                }
 
                $dom = $basedom->children(ActivityNamespace::SALMON_ME);
@@ -439,17 +454,12 @@ class Diaspora
                        $inner_decrypted = self::aesDecrypt($inner_aes_key, $inner_iv, $inner_encrypted);
                }
 
-               if (!$author_link) {
-                       Logger::notice('Could not retrieve author URI.');
-                       throw new \Friendica\Network\HTTPException\BadRequestException();
-               }
                // Once we have the author URI, go to the web and try to find their public key
                // (first this will look it up locally if it is in the fcontact cache)
                // This will also convert diaspora public key from pkcs#1 to pkcs#8
 
-               Logger::notice('Fetching key for '.$author_link);
-               $key = self::key($author_link);
-
+               Logger::notice('Fetching key for ' . $author);
+               $key = self::key($author);
                if (!$key) {
                        Logger::notice('Could not retrieve author key.');
                        throw new \Friendica\Network\HTTPException\BadRequestException();
@@ -465,9 +475,9 @@ class Diaspora
                Logger::notice('Message verified.');
 
                return [
-                       'message' => (string)$inner_decrypted,
-                       'author' => XML::unescape($author_link),
-                       'key' => (string)$key
+                       'message' => $inner_decrypted,
+                       'author'  => $author->getAddr(),
+                       'key'     => $key
                ];
        }
 
@@ -520,7 +530,7 @@ class Diaspora
        {
                // The sender is the handle of the contact that sent the message.
                // This will often be different with relayed messages (for example "like" and "comment")
-               $sender = $msg['author'];
+               $sender = WebFingerUri::fromString($msg['author']);
 
                // This is only needed for private postings since this is already done for public ones before
                if (is_null($fields)) {
@@ -535,7 +545,7 @@ class Diaspora
 
                $type = $fields->getName();
 
-               Logger::info('Received message', ['type' => $type, 'sender' => $sender, 'user' => $importer['uid']]);
+               Logger::info('Received message', ['type' => $type, 'sender' => $sender->getAddr(), 'user' => $importer['uid']]);
 
                switch ($type) {
                        case 'account_migration':
@@ -743,7 +753,7 @@ class Diaspora
                }
 
                if (isset($parent_author_signature)) {
-                       $key = self::key($msg['author']);
+                       $key = self::key(WebFingerUri::fromString($msg['author']));
                        if (empty($key)) {
                                Logger::info('No key found for parent', ['author' => $msg['author']]);
                                return false;
@@ -755,8 +765,12 @@ class Diaspora
                        }
                }
 
-               $key = self::key($fields->author);
-               if (empty($key)) {
+               try {
+                       $key = self::key(WebFingerUri::fromString($fields->author));
+                       if (empty($key)) {
+                               throw new \InvalidArgumentException();
+                       }
+               } catch (\Throwable $e) {
                        Logger::info('No key found', ['author' => $fields->author]);
                        return false;
                }
@@ -772,19 +786,17 @@ class Diaspora
        /**
         * Fetches the public key for a given handle
         *
-        * @param string $handle The handle
+        * @param WebFingerUri $uri The handle
         *
         * @return string The public key
-        * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+        * @throws InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function key(string $handle = null): string
+       private static function key(WebFingerUri $uri): string
        {
-               $handle = strval($handle);
-
-               Logger::notice('Fetching diaspora key', ['handle' => $handle, 'callstack' => System::callstack(20)]);
+               Logger::notice('Fetching diaspora key', ['handle' => $uri->getAddr(), 'callstack' => System::callstack(20)]);
 
-               $fcontact = FContact::getByURL($handle);
+               $fcontact = FContact::getByURL($uri);
                if (!empty($fcontact['pubkey'])) {
                        return $fcontact['pubkey'];
                }
@@ -795,18 +807,16 @@ class Diaspora
        /**
         * Get a contact id for a given handle
         *
-        * @todo  Move to Friendica\Model\Contact
-        *
-        * @param int    $uid    The user id
-        * @param string $handle The handle in the format user@domain.tld
+        * @param int          $uid The user id
+        * @param WebFingerUri $uri
         *
         * @return array Contact data
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function contactByHandle(int $uid, string $handle): array
+       private static function contactByHandle(int $uid, WebFingerUri $uri): array
        {
-               return Contact::getByURL($handle, null, [], $uid);
+               return Contact::getByURL($uri->getAddr(), null, [], $uid);
        }
 
        /**
@@ -874,21 +884,22 @@ class Diaspora
        /**
         * Fetches the contact id for a handle and checks if posting is allowed
         *
-        * @param array  $importer   Array of the importer user
-        * @param string $handle     The checked handle in the format user@domain.tld
-        * @param bool   $is_comment Is the check for a comment?
+        * @param array        $importer    Array of the importer user
+        * @param WebFingerUri $contact_uri The checked contact
+        * @param bool         $is_comment  Is the check for a comment?
         *
         * @return array|bool The contact data or false on error
-        * @throws \Exception
+        * @throws InternalServerErrorException
+        * @throws \ImagickException
         */
-       private static function allowedContactByHandle(array $importer, string $handle, bool $is_comment = false)
+       private static function allowedContactByHandle(array $importer, WebFingerUri $contact_uri, bool $is_comment = false)
        {
-               $contact = self::contactByHandle($importer['uid'], $handle);
+               $contact = self::contactByHandle($importer['uid'], $contact_uri);
                if (!$contact) {
-                       Logger::notice('A Contact for handle ' . $handle . ' and user ' . $importer['uid'] . ' was not found');
+                       Logger::notice('A Contact for handle ' . $contact_uri . ' and user ' . $importer['uid'] . ' was not found');
                        // If a contact isn't found, we accept it anyway if it is a comment
                        if ($is_comment && ($importer['uid'] != 0)) {
-                               return self::contactByHandle(0, $handle);
+                               return self::contactByHandle(0, $contact_uri);
                        } elseif ($is_comment) {
                                return $importer;
                        } else {
@@ -897,7 +908,7 @@ class Diaspora
                }
 
                if (!self::postAllow($importer, $contact, $is_comment)) {
-                       Logger::notice('The handle: ' . $handle . ' is not allowed to post to user ' . $importer['uid']);
+                       Logger::notice('The handle: ' . $contact_uri . ' is not allowed to post to user ' . $importer['uid']);
                        return false;
                }
                return $contact;
@@ -1011,7 +1022,7 @@ class Diaspora
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function storeByGuid(string $guid, string $server, bool $force)
+       private static function storeByGuid(string $guid, string $server, bool $force)
        {
                $serverparts = parse_url($server);
 
@@ -1092,25 +1103,27 @@ class Diaspora
                        return self::message($source_xml->root_guid, $server, ++$level);
                }
 
-               $author = '';
+               $author_handle = '';
 
                // Fetch the author - for the old and the new Diaspora version
                if ($source_xml->post->status_message && $source_xml->post->status_message->diaspora_handle) {
-                       $author = (string)$source_xml->post->status_message->diaspora_handle;
+                       $author_handle = (string)$source_xml->post->status_message->diaspora_handle;
                } elseif ($source_xml->author && ($source_xml->getName() == 'status_message')) {
-                       $author = (string)$source_xml->author;
+                       $author_handle = (string)$source_xml->author;
                }
 
-               // If this isn't a "status_message" then quit
-               if (!$author) {
+               try {
+                       $author = WebFingerUri::fromString($author_handle);
+               } catch (\InvalidArgumentException $e) {
+                       // If this isn't a "status_message" then quit
                        Logger::info("Message doesn't seem to be a status message");
                        return false;
                }
 
                return [
                        'message' => $x,
-                       'author' => $author,
-                       'key' => self::key($author)
+                       'author'  => $author->getAddr(),
+                       'key'     => self::key($author)
                ];
        }
 
@@ -1157,15 +1170,15 @@ class Diaspora
        /**
         * Fetches the item record of a given guid
         *
-        * @param int    $uid     The user id
-        * @param string $guid    message guid
-        * @param string $author  The handle of the item
-        * @param array  $contact The contact of the item owner
+        * @param int          $uid     The user id
+        * @param string       $guid    message guid
+        * @param WebFingerUri $author
+        * @param array        $contact The contact of the item owner
         *
         * @return array|bool the item record or false on failure
         * @throws \Exception
         */
-       private static function parentItem(int $uid, string $guid, string $author, array $contact)
+       private static function parentItem(int $uid, string $guid, WebFingerUri $author, array $contact)
        {
                $fields = ['id', 'parent', 'body', 'wall', 'uri', 'guid', 'private', 'origin',
                        'author-name', 'author-link', 'author-avatar', 'gravity',
@@ -1200,20 +1213,20 @@ class Diaspora
        }
 
        /**
-        * returns contact details
+        * returns contact details for the given user
         *
-        * @param array $def_contact The default contact if the person isn't found
-        * @param array $person      The record of the person
-        * @param int   $uid         The user id
+        * @param array  $def_contact The default details if the contact isn't found
+        * @param string $contact_url The url of the contact
+        * @param int    $uid         The user id
         *
         * @return array
         *      'cid' => contact id
         *      'network' => network type
         * @throws \Exception
         */
-       private static function authorContactByUrl(array $def_contact, array $person, int $uid): array
+       private static function authorContactByUrl(array $def_contact, string $contact_url, int $uid): array
        {
-               $condition = ['nurl' => Strings::normaliseLink($person['url']), 'uid' => $uid];
+               $condition = ['nurl' => Strings::normaliseLink($contact_url), 'uid' => $uid];
                $contact = DBA::selectFirst('contact', ['id', 'network'], $condition);
                if (DBA::isResult($contact)) {
                        $cid = $contact['id'];
@@ -1318,21 +1331,27 @@ class Diaspora
         */
        private static function receiveAccountMigration(array $importer, SimpleXMLElement $data): bool
        {
-               $old_handle = XML::unescape($data->author);
-               $new_handle = XML::unescape($data->profile->author);
+               try {
+                       $old_author = WebFingerUri::fromString(XML::unescape($data->author));
+                       $new_author = WebFingerUri::fromString(XML::unescape($data->profile->author));
+               } catch (\Throwable $e) {
+                       Logger::notice('Cannot find handles for sender and user', ['data' => $data]);
+                       return false;
+               }
+
                $signature = XML::unescape($data->signature);
 
-               $contact = self::contactByHandle($importer['uid'], $old_handle);
+               $contact = self::contactByHandle($importer['uid'], $old_author);
                if (!$contact) {
-                       Logger::notice('Cannot find contact for sender: ' . $old_handle . ' and user ' . $importer['uid']);
+                       Logger::notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']);
                        return false;
                }
 
-               Logger::notice('Got migration for ' . $old_handle . ', to ' . $new_handle . ' with user ' . $importer['uid']);
+               Logger::notice('Got migration for ' . $old_author . ', to ' . $new_author . ' with user ' . $importer['uid']);
 
                // Check signature
-               $signed_text = 'AccountMigration:' . $old_handle . ':' . $new_handle;
-               $key = self::key($old_handle);
+               $signed_text = 'AccountMigration:' . $old_author . ':' . $new_author;
+               $key = self::key($old_author);
                if (!Crypto::rsaVerify($signed_text, $signature, $key, 'sha256')) {
                        Logger::notice('No valid signature for migration.');
                        return false;
@@ -1342,9 +1361,9 @@ class Diaspora
                self::receiveProfile($importer, $data->profile);
 
                // change the technical stuff in contact
-               $data = Probe::uri($new_handle);
+               $data = Probe::uri($new_author);
                if ($data['network'] == Protocol::PHANTOM) {
-                       Logger::notice("Account for " . $new_handle . " couldn't be probed.");
+                       Logger::notice("Account for " . $new_author . " couldn't be probed.");
                        return false;
                }
 
@@ -1360,7 +1379,7 @@ class Diaspora
                        'network' => $data['network'],
                ];
 
-               Contact::update($fields, ['addr' => $old_handle]);
+               Contact::update($fields, ['addr' => $old_author->getAddr()]);
 
                Logger::notice('Contacts are updated.');
 
@@ -1377,15 +1396,15 @@ class Diaspora
         */
        private static function receiveAccountDeletion(SimpleXMLElement $data): bool
        {
-               $author = XML::unescape($data->author);
+               $author_handle = XML::unescape($data->author);
 
-               $contacts = DBA::select('contact', ['id'], ['addr' => $author]);
+               $contacts = DBA::select('contact', ['id'], ['addr' => $author_handle]);
                while ($contact = DBA::fetch($contacts)) {
                        Contact::remove($contact['id']);
                }
                DBA::close($contacts);
 
-               Logger::notice('Removed contacts for ' . $author);
+               Logger::notice('Removed contacts for ' . $author_handle);
 
                return true;
        }
@@ -1393,21 +1412,20 @@ class Diaspora
        /**
         * Fetch the uri from our database if we already have this item (maybe from ourselves)
         *
-        * @param string  $author    Author handle
-        * @param string  $guid      Message guid
-        * @param boolean $onlyfound Only return uri when found in the database
+        * @param string            $guid       Message guid
+        * @param WebFingerUri|null $person_uri Optional person to derive the base URL from
         *
-        * @return string The constructed uri or the one from our database or empty string on if $onlyfound is true
+        * @return string The constructed uri or the one from our database or empty string
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function getUriFromGuid(string $author, string $guid, bool $onlyfound = false): string
+       private static function getUriFromGuid(string $guid, WebFingerUri $person_uri = null): string
        {
                $item = Post::selectFirst(['uri'], ['guid' => $guid]);
                if (DBA::isResult($item)) {
                        return $item['uri'];
-               } elseif (!$onlyfound) {
-                       $person = FContact::getByURL($author);
+               } elseif ($person_uri) {
+                       $person = FContact::getByURL($person_uri);
 
                        $parts = parse_url($person['url']);
                        unset($parts['path']);
@@ -1456,19 +1474,19 @@ class Diaspora
        /**
         * Processes an incoming comment
         *
-        * @param array  $importer  Array of the importer user
-        * @param string $sender    The sender of the message
+        * @param array            $importer  Array of the importer user
+        * @param WebFingerUri     $sender    The sender of the message
         * @param SimpleXMLElement $data      The message object
-        * @param string $xml       The original XML of the message
-        * @param int    $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
+        * @param string           $xml       The original XML of the message
+        * @param int              $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
         *
-        * @return int The message id of the generated comment or "false" if there was an error
+        * @return bool The message id of the generated comment or "false" if there was an error
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function receiveComment(array $importer, string $sender, SimpleXMLElement $data, string $xml, int $direction): bool
+       private static function receiveComment(array $importer, WebFingerUri $sender, SimpleXMLElement $data, string $xml, int $direction): bool
        {
-               $author = XML::unescape($data->author);
+               $author = WebFingerUri::fromString(XML::unescape($data->author));
                $guid = XML::unescape($data->guid);
                $parent_guid = XML::unescape($data->parent_guid);
                $text = XML::unescape($data->text);
@@ -1481,7 +1499,7 @@ class Diaspora
 
                if (isset($data->thread_parent_guid)) {
                        $thread_parent_guid = XML::unescape($data->thread_parent_guid);
-                       $thr_parent = self::getUriFromGuid('', $thread_parent_guid, true);
+                       $thr_parent = self::getUriFromGuid($thread_parent_guid);
                } else {
                        $thr_parent = '';
                }
@@ -1512,7 +1530,7 @@ class Diaspora
                }
 
                // Fetch the contact id - if we know this contact
-               $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']);
+               $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
 
                $datarray = [];
 
@@ -1530,7 +1548,7 @@ class Diaspora
                $datarray = self::setDirection($datarray, $direction);
 
                $datarray['guid'] = $guid;
-               $datarray['uri'] = self::getUriFromGuid($author, $guid);
+               $datarray['uri'] = self::getUriFromGuid($guid, $author);
                $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
 
                $datarray['verb'] = Activity::POST;
@@ -1601,16 +1619,16 @@ class Diaspora
         */
        private static function receiveConversationMessage(array $importer, array $contact, SimpleXMLElement $data, array $msg, $mesg, array $conversation): bool
        {
-               $author = XML::unescape($data->author);
+               $author_handle = XML::unescape($data->author);
                $guid = XML::unescape($data->guid);
                $subject = XML::unescape($data->subject);
 
                // "diaspora_handle" is the element name from the old version
                // "author" is the element name from the new version
                if ($mesg->author) {
-                       $msg_author = XML::unescape($mesg->author);
+                       $msg_author_handle = XML::unescape($mesg->author);
                } elseif ($mesg->diaspora_handle) {
-                       $msg_author = XML::unescape($mesg->diaspora_handle);
+                       $msg_author_handle = XML::unescape($mesg->diaspora_handle);
                } else {
                        return false;
                }
@@ -1626,9 +1644,8 @@ class Diaspora
                }
 
                $body = Markdown::toBBCode($msg_text);
-               $message_uri = $msg_author . ':' . $msg_guid;
 
-               $person = FContact::getByURL($msg_author);
+               $person = FContact::getByURL($msg_author_handle);
 
                return Mail::insert([
                        'uid'        => $importer['uid'],
@@ -1640,8 +1657,8 @@ class Diaspora
                        'contact-id' => $contact['id'],
                        'title'      => $subject,
                        'body'       => $body,
-                       'uri'        => $message_uri,
-                       'parent-uri' => $author . ':' . $guid,
+                       'uri'        => $msg_author_handle . ':' . $msg_guid,
+                       'parent-uri' => $author_handle . ':' . $guid,
                        'created'    => $msg_created_at
                ]);
        }
@@ -1658,7 +1675,7 @@ class Diaspora
         */
        private static function receiveConversation(array $importer, array $msg, SimpleXMLElement $data)
        {
-               $author = XML::unescape($data->author);
+               $author_handle = XML::unescape($data->author);
                $guid = XML::unescape($data->guid);
                $subject = XML::unescape($data->subject);
                $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
@@ -1671,7 +1688,7 @@ class Diaspora
                        return false;
                }
 
-               $contact = self::allowedContactByHandle($importer, $msg['author'], true);
+               $contact = self::allowedContactByHandle($importer, WebFingerUri::fromString($msg['author']), true);
                if (!$contact) {
                        return false;
                }
@@ -1685,7 +1702,7 @@ class Diaspora
                        $r = DBA::insert('conv', [
                                'uid'     => $importer['uid'],
                                'guid'    => $guid,
-                               'creator' => $author,
+                               'creator' => $author_handle,
                                'created' => $created_at,
                                'updated' => DateTimeFormat::utcNow(),
                                'subject' => $subject,
@@ -1711,18 +1728,18 @@ class Diaspora
        /**
         * Processes "like" messages
         *
-        * @param array  $importer  Array of the importer user
-        * @param string $sender    The sender of the message
+        * @param array            $importer  Array of the importer user
+        * @param WebFingerUri     $sender    The sender of the message
         * @param SimpleXMLElement $data      The message object
-        * @param int    $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
+        * @param int              $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
         *
         * @return bool Success or failure
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function receiveLike(array $importer, string $sender, SimpleXMLElement $data, int $direction): bool
+       private static function receiveLike(array $importer, WebFingerUri $sender, SimpleXMLElement $data, int $direction): bool
        {
-               $author = XML::unescape($data->author);
+               $author = WebFingerUri::fromString(XML::unescape($data->author));
                $guid = XML::unescape($data->guid);
                $parent_guid = XML::unescape($data->parent_guid);
                $parent_type = XML::unescape($data->parent_type);
@@ -1760,7 +1777,7 @@ class Diaspora
                }
 
                // Fetch the contact id - if we know this contact
-               $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']);
+               $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
 
                // "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora
                // We would accept this anyhow.
@@ -1784,7 +1801,7 @@ class Diaspora
                $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0);
 
                $datarray['guid'] = $guid;
-               $datarray['uri'] = self::getUriFromGuid($author, $guid);
+               $datarray['uri'] = self::getUriFromGuid($guid, $author);
 
                $datarray['verb'] = $verb;
                $datarray['gravity'] = Item::GRAVITY_ACTIVITY;
@@ -1843,7 +1860,7 @@ class Diaspora
         */
        private static function receiveMessage(array $importer, SimpleXMLElement $data): bool
        {
-               $author = XML::unescape($data->author);
+               $author = WebFingerUri::fromString(XML::unescape($data->author));
                $guid = XML::unescape($data->guid);
                $conversation_guid = XML::unescape($data->conversation_guid);
                $text = XML::unescape($data->text);
@@ -1858,18 +1875,13 @@ class Diaspora
                        GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA);
                }
 
-               $conversation = null;
-
                $condition = ['uid' => $importer['uid'], 'guid' => $conversation_guid];
                $conversation = DBA::selectFirst('conv', [], $condition);
-
                if (!DBA::isResult($conversation)) {
                        Logger::notice('Conversation not available.');
                        return false;
                }
 
-               $message_uri = $author . ':' . $guid;
-
                $person = FContact::getByURL($author);
                if (!$person) {
                        Logger::notice('Unable to find author details');
@@ -1891,7 +1903,7 @@ class Diaspora
                        'title'      => $conversation['subject'],
                        'body'       => $body,
                        'reply'      => 1,
-                       'uri'        => $message_uri,
+                       'uri'        => $author . ':' . $guid,
                        'parent-uri' => $author . ':' . $conversation['guid'],
                        'created'    => $created_at
                ]);
@@ -1910,7 +1922,7 @@ class Diaspora
         */
        private static function receiveParticipation(array $importer, SimpleXMLElement $data, int $direction): bool
        {
-               $author = strtolower(XML::unescape($data->author));
+               $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author)));
                $guid = XML::unescape($data->guid);
                $parent_guid = XML::unescape($data->parent_guid);
 
@@ -1947,7 +1959,7 @@ class Diaspora
                        return false;
                }
 
-               $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']);
+               $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
 
                // Store participation
                $datarray = [];
@@ -1964,7 +1976,7 @@ class Diaspora
                $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0);
 
                $datarray['guid'] = $guid;
-               $datarray['uri'] = self::getUriFromGuid($author, $guid);
+               $datarray['uri'] = self::getUriFromGuid($guid, $author);
 
                $datarray['verb'] = Activity::FOLLOW;
                $datarray['gravity'] = Item::GRAVITY_ACTIVITY;
@@ -2056,7 +2068,7 @@ class Diaspora
         */
        private static function receiveProfile(array $importer, SimpleXMLElement $data): bool
        {
-               $author = strtolower(XML::unescape($data->author));
+               $author = WebFingerUri::fromString(strtolower(XML::unescape($data->author)));
 
                $contact = self::contactByHandle($importer['uid'], $author);
                if (!$contact) {
@@ -2084,16 +2096,13 @@ class Diaspora
 
                $keywords = implode(', ', $keywords);
 
-               $handle_parts = explode('@', $author);
-               $nick = $handle_parts[0];
-
                if ($name === '') {
-                       $name = $handle_parts[0];
+                       $name = $author->getUser();
                }
 
                if (preg_match('|^https?://|', $image_url) === 0) {
                        // @TODO No HTTPS here?
-                       $image_url = 'http://' . $handle_parts[1] . $image_url;
+                       $image_url = 'http://' . $author->getFullHost() . $image_url;
                }
 
                Contact::updateAvatar($contact['id'], $image_url);
@@ -2115,7 +2124,7 @@ class Diaspora
 
                $fields = ['name' => $name, 'location' => $location,
                        'name-date' => DateTimeFormat::utcNow(), 'about' => $about,
-                       'addr' => $author, 'nick' => $nick, 'keywords' => $keywords,
+                       'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords,
                        'unsearchable' => !$searchable, 'sensitive' => $nsfw];
 
                if (!empty($birthday)) {
@@ -2158,13 +2167,15 @@ class Diaspora
         */
        private static function receiveContactRequest(array $importer, SimpleXMLElement $data): bool
        {
-               $author = XML::unescape($data->author);
+               $author_handle = XML::unescape($data->author);
                $recipient = XML::unescape($data->recipient);
 
-               if (!$author || !$recipient) {
+               if (!$author_handle || !$recipient) {
                        return false;
                }
 
+               $author = WebFingerUri::fromString($author_handle);
+
                // the current protocol version doesn't know these fields
                // That means that we will assume their existance
                if (isset($data->following)) {
@@ -2262,12 +2273,15 @@ class Diaspora
        /**
         * Stores a reshare activity
         *
-        * @param array   $item              Array of reshare post
-        * @param integer $parent_message_id Id of the parent post
-        * @param string  $guid              GUID string of reshare action
-        * @param string  $author            Author handle
+        * @param array        $item              Array of reshare post
+        * @param integer      $parent_message_id Id of the parent post
+        * @param string       $guid              GUID string of reshare action
+        * @param WebFingerUri $author            Author handle
+        * @return false|void
+        * @throws InternalServerErrorException
+        * @throws \ImagickException
         */
-       private static function addReshareActivity(array $item, int $parent_message_id, string $guid, string $author)
+       private static function addReshareActivity(array $item, int $parent_message_id, string $guid, WebFingerUri $author)
        {
                $parent = Post::selectFirst(['uri', 'guid'], ['id' => $parent_message_id]);
 
@@ -2284,7 +2298,7 @@ class Diaspora
                $datarray['owner-id'] = $datarray['author-id'];
 
                $datarray['guid'] = $parent['guid'] . '-' . $guid;
-               $datarray['uri'] = self::getUriFromGuid($author, $datarray['guid']);
+               $datarray['uri'] = self::getUriFromGuid($datarray['guid'], $author);
                $datarray['thr-parent'] = $parent['uri'];
 
                $datarray['verb'] = $datarray['body'] = Activity::ANNOUNCE;
@@ -2329,7 +2343,7 @@ class Diaspora
         */
        private static function receiveReshare(array $importer, SimpleXMLElement $data, string $xml, int $direction): bool
        {
-               $author = XML::unescape($data->author);
+               $author = WebFingerUri::fromString(XML::unescape($data->author));
                $guid = XML::unescape($data->guid);
                $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
                $root_author = XML::unescape($data->root_author);
@@ -2337,7 +2351,7 @@ class Diaspora
                /// @todo handle unprocessed property "provider_display_name"
                $public = XML::unescape($data->public);
 
-               $contact = self::allowedContactByHandle($importer, $author, false);
+               $contact = self::allowedContactByHandle($importer, $author);
                if (!$contact) {
                        return false;
                }
@@ -2369,7 +2383,7 @@ class Diaspora
                $datarray['owner-id'] = $datarray['author-id'];
 
                $datarray['guid'] = $guid;
-               $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid);
+               $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author);
                $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
 
                $datarray['verb'] = Activity::POST;
@@ -2448,13 +2462,13 @@ class Diaspora
         */
        private static function itemRetraction(array $importer, array $contact, SimpleXMLElement $data): bool
        {
-               $author = XML::unescape($data->author);
+               $author_handle = XML::unescape($data->author);
                $target_guid = XML::unescape($data->target_guid);
                $target_type = XML::unescape($data->target_type);
 
-               $person = FContact::getByURL($author);
+               $person = FContact::getByURL($author_handle);
                if (!is_array($person)) {
-                       Logger::notice('Unable to find author detail for ' . $author);
+                       Logger::notice('Unable to find author detail for ' . $author_handle);
                        return false;
                }
 
@@ -2505,14 +2519,14 @@ class Diaspora
        /**
         * Receives retraction messages
         *
-        * @param array  $importer Array of the importer user
-        * @param string $sender   The sender of the message
+        * @param array            $importer Array of the importer user
+        * @param WebFingerUri     $sender   The sender of the message
         * @param SimpleXMLElement $data     The message object
         *
         * @return bool Success
         * @throws \Exception
         */
-       private static function receiveRetraction(array $importer, string $sender, SimpleXMLElement $data)
+       private static function receiveRetraction(array $importer, WebFingerUri $sender, SimpleXMLElement $data)
        {
                $target_type = XML::unescape($data->target_type);
 
@@ -2639,14 +2653,14 @@ class Diaspora
         */
        private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, string $xml, int $direction)
        {
-               $author = XML::unescape($data->author);
+               $author = WebFingerUri::fromString(XML::unescape($data->author));
                $guid = XML::unescape($data->guid);
                $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
                $public = XML::unescape($data->public);
                $text = XML::unescape($data->text);
                $provider_display_name = XML::unescape($data->provider_display_name);
 
-               $contact = self::allowedContactByHandle($importer, $author, false);
+               $contact = self::allowedContactByHandle($importer, $author);
                if (!$contact) {
                        return false;
                }
@@ -2672,7 +2686,7 @@ class Diaspora
                $datarray = [];
 
                $datarray['guid'] = $guid;
-               $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid);
+               $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author);
                $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
 
                // Attach embedded pictures to the body
@@ -3089,16 +3103,16 @@ class Diaspora
                        $owner = User::getOwnerDataById($item['uid']);
                }
 
-               $author = self::myHandle($owner);
+               $author_handle = self::myHandle($owner);
 
                $message = [
-                       'author' => $author,
+                       'author' => $author_handle,
                        'guid' => System::createUUID(),
                        'parent_type' => 'Post',
                        'parent_guid' => $item['guid']
                ];
 
-               Logger::info('Send participation for ' . $item['guid'] . ' by ' . $author);
+               Logger::info('Send participation for ' . $item['guid'] . ' by ' . $author_handle);
 
                // It doesn't matter what we store, we only want to avoid sending repeated notifications for the same item
                DI::cache()->set($cachekey, $item['guid'], Duration::QUARTER_HOUR);
diff --git a/src/Protocol/WebFingerUri.php b/src/Protocol/WebFingerUri.php
new file mode 100644 (file)
index 0000000..b9959f2
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Friendica\Protocol;
+
+use GuzzleHttp\Psr7\Uri;
+
+class WebFingerUri
+{
+       /**
+        * @var string
+        */
+       private $user;
+       /**
+        * @var string
+        */
+       private $host;
+       /**
+        * @var int|null
+        */
+       private $port;
+       /**
+        * @var string|null
+        */
+       private $path;
+
+       private function __construct(string $user, string $host, int $port = null, string $path = null)
+       {
+               $this->user = $user;
+               $this->host = $host;
+               $this->port = $port;
+               $this->path = $path;
+
+               $this->validate();
+       }
+
+       /**
+        * @param string $addr
+        * @return WebFingerUri
+        */
+       public static function fromString(string $addr): WebFingerUri
+       {
+               $uri = new Uri('acct://' . preg_replace('/^acct:/', '', $addr));
+
+               return new self($uri->getUserInfo(), $uri->getHost(), $uri->getPort(), $uri->getPath());
+       }
+
+       private function validate()
+       {
+               if (!$this->user) {
+                       throw new \InvalidArgumentException('WebFinger URI User part is required');
+               }
+
+               if (!$this->host) {
+                       throw new \InvalidArgumentException('WebFinger URI Host part is required');
+               }
+       }
+
+       public function getUser(): string
+       {
+               return $this->user;
+       }
+
+       public function getHost(): string
+       {
+               return $this->host;
+       }
+
+       public function getFullHost(): string
+       {
+               return $this->host
+                       . ($this->port ? ':' . $this->port : '') .
+                       ($this->path ?: '');
+       }
+
+       public function getLongForm(): string
+       {
+               return 'acct:' . $this->getShortForm();
+       }
+
+       public function getShortForm(): string
+       {
+               return $this->user . '@' . $this->getFullHost();
+       }
+
+       public function getAddr(): string
+       {
+               return $this->getShortForm();
+       }
+
+       public function __toString(): string
+       {
+               return $this->getShortForm();
+       }
+}
diff --git a/tests/src/Protocol/WebFingerUriTest.php b/tests/src/Protocol/WebFingerUriTest.php
new file mode 100644 (file)
index 0000000..7378040
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2022, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * Main database structure configuration file.
+ *
+ * Here are described all the tables, fields and indexes Friendica needs to work.
+ * The entry order is mostly alphabetic - with the exception of tables that are used in foreign keys.
+ *
+ * Syntax (braces indicate optionale values):
+ * "<table name>" => [
+ *    "comment" => "Description of the table",
+ *    "fields" => [
+ *        "<field name>" => [
+ *            "type" => "<field type>{(<field size>)} <unsigned>",
+ *            "not null" => 0|1,
+ *            {"extra" => "auto_increment",}
+ *            {"default" => "<default value>",}
+ *            {"default" => NULL_DATE,} (for datetime fields)
+ *            {"primary" => "1",}
+ *            {"foreign|relation" => ["<foreign key table name>" => "<foreign key field name>"],}
+ *            "comment" => "Description of the fields"
+ *        ],
+ *        ...
+ *    ],
+ *    "indexes" => [
+ *        "PRIMARY" => ["<primary key field name>", ...],
+ *        "<index name>" => [{"UNIQUE",} "<field name>{(<key size>)}", ...]
+ *        ...
+ *    ],
+ * ],
+ *
+ * Whenever possible prefer "foreign" before "relation" with the foreign keys.
+ * "foreign" adds true foreign keys on the database level, while "relation" is just an indicator of a table relation without any consequences
+ *
+ * If you need to make any change, make sure to increment the DB_UPDATE_VERSION constant value below.
+ *
+ */
+
+namespace Friendica\Test\src\Protocol;
+
+use Friendica\Protocol\WebFingerUri;
+use PHPUnit\Framework\TestCase;
+
+class WebFingerUriTest extends TestCase
+{
+       public function dataFromString(): array
+       {
+               return [
+                       'long' => [
+                               'expectedLong'  => 'acct:selma@www.example.com:8080/friend',
+                               'expectedShort' => 'selma@www.example.com:8080/friend',
+                               'input'         => 'acct:selma@www.example.com:8080/friend',
+                       ],
+                       'short' => [
+                               'expectedLong'  => 'acct:selma@www.example.com:8080/friend',
+                               'expectedShort' => 'selma@www.example.com:8080/friend',
+                               'input'         => 'selma@www.example.com:8080/friend',
+                       ],
+                       'minimal' => [
+                               'expectedLong'  => 'acct:bob@example.com',
+                               'expectedShort' => 'bob@example.com',
+                               'input'         => 'bob@example.com',
+                       ],
+                       'acct:' => [
+                               'expectedLong'  => 'acct:alice@example.acct:90',
+                               'expectedShort' => 'alice@example.acct:90',
+                               'input'         => 'alice@example.acct:90',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider dataFromString
+        * @param string $expectedLong
+        * @param string $expectedShort
+        * @param string $input
+        * @return void
+        */
+       public function testFromString(string $expectedLong, string $expectedShort, string $input)
+       {
+               $uri = WebFingerUri::fromString($input);
+
+               $this->assertEquals($expectedLong, $uri->getLongForm());
+               $this->assertEquals($expectedShort, $uri->getShortForm());
+       }
+
+       public function dataFromStringFailure()
+       {
+               return [
+                       'missing user' => [
+                               'input' => 'example.com',
+                       ],
+                       'missing user @' => [
+                               'input' => '@example.com',
+                       ],
+                       'missing host' => [
+                               'input' => 'alice',
+                       ],
+                       'missing host @' => [
+                               'input' => 'alice@',
+                       ],
+                       'missing everything' => [
+                               'input' => '',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider dataFromStringFailure
+        * @param string $input
+        * @return void
+        */
+       public function testFromStringFailure(string $input)
+       {
+               $this->expectException(\InvalidArgumentException::class);
+
+               WebFingerUri::fromString($input);
+       }
+}