From ace80ca1b45e88d2120633914502f597ed8468ef Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 28 Aug 2021 08:42:20 +0000 Subject: [PATCH] Use "author_handle" for the author handle extracted from Diaspora XML messages - 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 | 322 ++++++++++++------------ src/Protocol/WebFingerUri.php | 113 +++++++++ tests/src/Protocol/WebFingerUriTest.php | 135 ++++++++++ 3 files changed, 416 insertions(+), 154 deletions(-) create mode 100644 src/Protocol/WebFingerUri.php create mode 100644 tests/src/Protocol/WebFingerUriTest.php diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 54f09e9d9..7fd899f8d 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -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' => $uri->getAddr(), 'callstack' => System::callstack(20)]); - Logger::notice('Fetching diaspora key', ['handle' => $handle, '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); - $signature = XML::unescape($data->signature); - - $contact = self::contactByHandle($importer['uid'], $old_handle); - if (!$contact) { - Logger::notice('Cannot find contact for sender: ' . $old_handle . ' and user ' . $importer['uid']); + 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; } - Logger::notice('Got migration for ' . $old_handle . ', to ' . $new_handle . ' with user ' . $importer['uid']); + $signature = XML::unescape($data->signature); + + $contact = self::contactByHandle($importer['uid'], $old_author); + if (!$contact) { + Logger::notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']); + return false; + } + + 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 index 000000000..b9959f2db --- /dev/null +++ b/src/Protocol/WebFingerUri.php @@ -0,0 +1,113 @@ +. + * + */ + +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 index 000000000..7378040d2 --- /dev/null +++ b/tests/src/Protocol/WebFingerUriTest.php @@ -0,0 +1,135 @@ +. + * + * 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): + * "" => [ + * "comment" => "Description of the table", + * "fields" => [ + * "" => [ + * "type" => "{()} ", + * "not null" => 0|1, + * {"extra" => "auto_increment",} + * {"default" => "",} + * {"default" => NULL_DATE,} (for datetime fields) + * {"primary" => "1",} + * {"foreign|relation" => ["" => ""],} + * "comment" => "Description of the fields" + * ], + * ... + * ], + * "indexes" => [ + * "PRIMARY" => ["", ...], + * "" => [{"UNIQUE",} "{()}", ...] + * ... + * ], + * ], + * + * 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); + } +}