X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FDiaspora.php;h=dd6c2115b5e32714a868bae0ee589465ca76bf31;hb=fdaff4303952427f222ee21f6b501d5087e25932;hp=09f4380b0b1eb5d7210f18052d38d4e89751b31a;hpb=eec48057dcc2cfc1725d5a398e449301b5b599ac;p=friendica.git diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 09f4380b0b..dd6c2115b5 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -1,6 +1,6 @@ $item['parent'], 'gravity' => [Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY]]); + $items = Post::select( + ['author-id', 'author-link', 'parent-author-link', 'parent-guid', 'guid'], + ['parent' => $item['parent'], 'gravity' => [Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY]] + ); while ($item = Post::fetch($items)) { - $contact = DBA::selectFirst('contact', ['id', 'url', 'name', 'protocol', 'batch', 'network'], - ['id' => $item['author-id']]); - if (!DBA::isResult($contact) || empty($contact['batch']) || + $contact = DBA::selectFirst( + 'contact', + ['id', 'url', 'name', 'protocol', 'batch', 'network'], + ['id' => $item['author-id']] + ); + if ( + !DBA::isResult($contact) || empty($contact['batch']) || ($contact['network'] != Protocol::DIASPORA) || - Strings::compareLink($item['parent-author-link'], $item['author-link'])) { + Strings::compareLink($item['parent-author-link'], $item['author-link']) + ) { continue; } @@ -161,8 +168,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; } @@ -224,26 +235,41 @@ class Diaspora // Is it a private post? Then decrypt the outer Salmon if (is_object($data)) { - $encrypted_aes_key_bundle = base64_decode($data->aes_key); - $ciphertext = base64_decode($data->encrypted_magic_envelope); + try { + if (!isset($data->aes_key) || !isset($data->encrypted_magic_envelope)) { + Logger::info('Missing keys "aes_key" and/or "encrypted_magic_envelope"', ['data' => $data]); + throw new \RuntimeException('Missing keys "aes_key" and/or "encrypted_magic_envelope"'); + } - $outer_key_bundle = ''; - @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); - $j_outer_key_bundle = json_decode($outer_key_bundle); + $encrypted_aes_key_bundle = base64_decode($data->aes_key); + $ciphertext = base64_decode($data->encrypted_magic_envelope); + + $outer_key_bundle = ''; + @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); + $j_outer_key_bundle = json_decode($outer_key_bundle); + + if (!is_object($j_outer_key_bundle)) { + Logger::info('Unable to decode outer key bundle', ['outer_key_bundle' => $outer_key_bundle]); + throw new \RuntimeException('Unable to decode outer key bundle'); + } + + if (!isset($j_outer_key_bundle->iv) || !isset($j_outer_key_bundle->key)) { + Logger::info('Missing keys "iv" and/or "key" from outer Salmon', ['j_outer_key_bundle' => $j_outer_key_bundle]); + throw new \RuntimeException('Missing keys "iv" and/or "key" from outer Salmon'); + } - if (!is_object($j_outer_key_bundle)) { + $outer_iv = base64_decode($j_outer_key_bundle->iv); + $outer_key = base64_decode($j_outer_key_bundle->key); + + $xml = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); + } catch (\Throwable $e) { Logger::notice('Outer Salmon did not verify. Discarding.'); if ($no_exit) { return false; } else { - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } } - - $outer_iv = base64_decode($j_outer_key_bundle->iv); - $outer_key = base64_decode($j_outer_key_bundle->key); - - $xml = self::aesDecrypt($outer_key, $outer_iv, $ciphertext); } else { $xml = $raw; } @@ -251,11 +277,11 @@ class Diaspora $basedom = XML::parseString($xml, true); if (!is_object($basedom)) { - Logger::notice('Received data does not seem to be an XML. Discarding. '.$xml); + Logger::notice('Received data does not seem to be an XML. Discarding. ' . $xml); if ($no_exit) { return false; } else { - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } } @@ -281,17 +307,22 @@ class Diaspora if ($no_exit) { return false; } else { - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } } - $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; } else { - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } } @@ -301,14 +332,14 @@ class Diaspora if ($no_exit) { return false; } else { - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } } return [ 'message' => (string)Strings::base64UrlDecode($base->data), - 'author' => XML::unescape($author_addr), - 'key' => (string)$key + 'author' => $author->getAddr(), + 'key' => (string)$key ]; } @@ -341,7 +372,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)) { @@ -369,8 +400,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); @@ -388,7 +424,7 @@ class Diaspora if (!$base) { Logger::notice('unable to locate salmon data in xml'); - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } @@ -408,14 +444,11 @@ class Diaspora $encoding = $base->encoding; $alg = $base->alg; - - $signed_data = $data.'.'.Strings::base64UrlEncode($type).'.'.Strings::base64UrlEncode($encoding).'.'.Strings::base64UrlEncode($alg); - + $signed_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); // decode the data $data = Strings::base64UrlDecode($data); - if ($public) { $inner_decrypted = $data; } else { @@ -424,35 +457,29 @@ 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) + // (first this will look it up locally if it is in the diaspora-contact 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::info('Fetching key for ' . $author); + $key = self::key($author); if (!$key) { Logger::notice('Could not retrieve author key.'); - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } $verify = Crypto::rsaVerify($signed_data, $signature, $key); if (!$verify) { Logger::notice('Message did not verify. Discarding.'); - throw new \Friendica\Network\HTTPException\BadRequestException(); + throw new HTTPException\BadRequestException(); } - Logger::notice('Message verified.'); + Logger::info('Message verified.'); return [ - 'message' => (string)$inner_decrypted, - 'author' => XML::unescape($author_link), - 'key' => (string)$key + 'message' => $inner_decrypted, + 'author' => $author->getAddr(), + 'key' => $key ]; } @@ -469,14 +496,13 @@ class Diaspora */ public static function dispatchPublic(array $msg, int $direction) { - $enabled = intval(DI::config()->get('system', 'diaspora_enabled')); - if (!$enabled) { + if (!DI::config()->get('system', 'diaspora_enabled')) { Logger::notice('Diaspora is disabled'); return false; } if (!($fields = self::validPosting($msg))) { - Logger::warning('Invalid posting'); + Logger::notice('Invalid posting', ['msg' => $msg]); return false; } @@ -505,13 +531,13 @@ 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)) { $private = true; if (!($fields = self::validPosting($msg))) { - Logger::warning('Invalid posting'); + Logger::notice('Invalid posting', ['msg' => $msg]); return false; } } else { @@ -520,7 +546,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': @@ -696,7 +722,8 @@ class Diaspora $signed_data .= $entry; } - if (!in_array($fieldname, ['parent_author_signature', 'target_author_signature']) + if ( + !in_array($fieldname, ['parent_author_signature', 'target_author_signature']) || ($orig_type == 'relayable_retraction') ) { XML::copy($entry, $fields, $fieldname); @@ -728,7 +755,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; @@ -740,8 +767,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; } @@ -757,55 +788,58 @@ 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)]); - - $fcontact = FContact::getByURL($handle); - if (!empty($fcontact['pubkey'])) { - return $fcontact['pubkey']; + Logger::info('Fetching diaspora key', ['handle' => $uri->getAddr(), 'callstack' => System::callstack(20)]); + try { + return DI::dsprContact()->getByAddr($uri)->pubKey; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + return ''; } - - return ''; } /** * 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); + Contact::updateByUrlIfNeeded($uri->getAddr()); + return Contact::getByURL($uri->getAddr(), null, [], $uid); } /** * Checks if the given contact url does support ActivityPub * - * @param string $url profile url - * @param boolean $update true = always update, false = never update, null = update when not found or outdated + * @param string $url profile url or WebFinger address + * @param boolean|null $update true = always update, false = never update, null = update when not found or outdated * @return boolean * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isSupportedByContactUrl(string $url, $update = null) + public static function isSupportedByContactUrl(string $url, ?bool $update = null): bool { - return !empty(FContact::getByURL($url, $update)); + $contact = Contact::getByURL($url, $update, ['uri-id', 'network']); + + $supported = DI::dsprContact()->existsByUriId($contact['uri-id'] ?? 0); + + if (!$supported && is_null($update) && ($contact['network'] == Protocol::DFRN)) { + $supported = self::isSupportedByContactUrl($url, true); + } + + return $supported; } /** @@ -859,21 +893,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 { @@ -882,7 +917,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; @@ -901,7 +936,7 @@ class Diaspora { $item = Post::selectFirst(['id'], ['uid' => $uid, 'guid' => $guid]); if (DBA::isResult($item)) { - Logger::notice('Message ' . $guid . ' already exists for user ' . $uid); + Logger::notice('Message already exists.', ['uid' => $uid, 'guid' => $guid, 'id' => $item['id']]); return $item['id']; } @@ -912,6 +947,7 @@ class Diaspora * Checks for links to posts in a message * * @param array $item The item array + * * @return void */ private static function fetchGuid(array $item) @@ -951,7 +987,7 @@ class Diaspora // 0 => '[url=/people/0123456789abcdef]Foo Bar[/url]' // 1 => '0123456789abcdef' // 2 => 'Foo Bar' - $handle = FContact::getUrlByGuid($match[1]); + $handle = DI::dsprContact()->getUrlByGuid($match[1]); if ($handle) { $return = '@[url=' . $handle . ']' . $match[2] . '[/url]'; @@ -996,7 +1032,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); @@ -1077,25 +1113,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) ]; } @@ -1113,7 +1151,7 @@ class Diaspora { // Check for Diaspora (and Friendica) typical paths if (!preg_match('=(https?://.+)/(?:posts|display|objects)/([a-zA-Z0-9-_@.:%]+[a-zA-Z0-9])=i', $url, $matches)) { - Logger::info('Invalid url', ['url' => $url]); + Logger::notice('Invalid url', ['url' => $url]); return false; } @@ -1134,7 +1172,7 @@ class Diaspora Logger::info('Found', ['id' => $item['id']]); return $item['id']; } else { - Logger::info('Not found', ['guid' => $guid, 'uid' => $uid]); + Logger::notice('Not found', ['guid' => $guid, 'uid' => $uid]); return false; } } @@ -1142,36 +1180,41 @@ 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', + $fields = [ + 'id', 'parent', 'body', 'wall', 'uri', 'guid', 'private', 'origin', 'author-name', 'author-link', 'author-avatar', 'gravity', - 'owner-name', 'owner-link', 'owner-avatar']; + 'owner-name', 'owner-link', 'owner-avatar' + ]; $condition = ['uid' => $uid, 'guid' => $guid]; $item = Post::selectFirst($fields, $condition); if (!DBA::isResult($item)) { - $person = FContact::getByURL($author); - $result = self::storeByGuid($guid, $person['url'], false); + try { + $result = self::storeByGuid($guid, DI::dsprContact()->getByAddr($author)->url, false); - // We don't have an url for items that arrived at the public dispatcher - if (!$result && !empty($contact['url'])) { - $result = self::storeByGuid($guid, $contact['url'], false); - } + // We don't have an url for items that arrived at the public dispatcher + if (!$result && !empty($contact['url'])) { + $result = self::storeByGuid($guid, $contact['url'], false); + } - if ($result) { - Logger::info('Fetched missing item ' . $guid . ' - result: ' . $result); + if ($result) { + Logger::info('Fetched missing item ' . $guid . ' - result: ' . $result); - $item = Post::selectFirst($fields, $condition); + $item = Post::selectFirst($fields, $condition); + } + } catch (HTTPException\NotFoundException $e) { + Logger::notice('Unable to retrieve author details', ['author' => $author->getAddr()]); } } @@ -1179,26 +1222,26 @@ class Diaspora Logger::notice('Parent item not found: parent: ' . $guid . ' - user: ' . $uid); return false; } else { - Logger::notice('Parent item found: parent: ' . $guid . ' - user: ' . $uid); + Logger::info('Parent item found: parent: ' . $guid . ' - user: ' . $uid); return $item; } } /** - * 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']; @@ -1303,21 +1346,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::info('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; @@ -1327,9 +1376,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; } @@ -1345,9 +1394,9 @@ class Diaspora 'network' => $data['network'], ]; - Contact::update($fields, ['addr' => $old_handle]); + Contact::update($fields, ['addr' => $old_author->getAddr()]); - Logger::notice('Contacts are updated.'); + Logger::info('Contacts are updated.'); return true; } @@ -1362,15 +1411,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::info('Removed contacts for ' . $author_handle); return true; } @@ -1378,27 +1427,24 @@ 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)) { + if ($item) { return $item['uri']; - } elseif (!$onlyfound) { - $person = FContact::getByURL($author); - - $parts = parse_url($person['url']); - unset($parts['path']); - $host_url = (string)Uri::fromParts($parts); - - return $host_url . '/objects/' . $guid; + } elseif ($person_uri) { + try { + return DI::dsprContact()->selectOneByAddr($person_uri)->baseurl . '/objects/' . $guid; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + return ''; + } } return ''; @@ -1429,31 +1475,30 @@ class Diaspora continue; } - $person = FContact::getByURL($match[3]); - if (empty($person)) { - continue; + try { + $contact = DI::dsprContact()->getByUrl(new Uri($match[3])); + Tag::storeByHash($uriid, $match[1], $contact->name ?: $contact->nick, $contact->url); + } catch (\Throwable $e) { } - - Tag::storeByHash($uriid, $match[1], $person['name'] ?: $person['nick'], $person['url']); } } /** * 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); @@ -1466,7 +1511,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 = ''; } @@ -1490,14 +1535,15 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author details'); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } // Fetch the contact id - if we know this contact - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); $datarray = []; @@ -1505,17 +1551,17 @@ class Diaspora $datarray['contact-id'] = $author_contact['cid']; $datarray['network'] = $author_contact['network']; - $datarray['author-link'] = $person['url']; - $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['author-link'] = $author_url; + $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['owner-link'] = $contact['url']; - $datarray['owner-id'] = Contact::getIdForURL($contact['url'], 0); + $datarray['owner-id'] = Contact::getIdForURL($contact['url']); // Will be overwritten for sharing accounts in Item::insert $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; @@ -1536,7 +1582,7 @@ class Diaspora $datarray['plink'] = self::plink($author, $guid, $toplevel_parent_item['guid']); $body = Markdown::toBBCode($text); - $datarray['body'] = self::replacePeopleGuid($body, $person['url']); + $datarray['body'] = self::replacePeopleGuid($body, $author_url); self::storeMentions($datarray['uri-id'], $text); Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray['body']); @@ -1586,20 +1632,26 @@ 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; } + try { + $msg_author_uri = WebFingerUri::fromString($msg_author_handle); + } catch (\InvalidArgumentException $e) { + return false; + } + $msg_guid = XML::unescape($mesg->guid); $msg_conversation_guid = XML::unescape($mesg->conversation_guid); $msg_text = XML::unescape($mesg->text); @@ -1610,23 +1662,20 @@ class Diaspora return false; } - $body = Markdown::toBBCode($msg_text); - $message_uri = $msg_author . ':' . $msg_guid; - - $person = FContact::getByURL($msg_author); + $msg_author = DI::dsprContact()->getByAddr($msg_author_uri); return Mail::insert([ 'uid' => $importer['uid'], 'guid' => $msg_guid, 'convid' => $conversation['id'], - 'from-name' => $person['name'], - 'from-photo' => $person['photo'], - 'from-url' => $person['url'], + 'from-name' => $msg_author->name, + 'from-photo' => (string)$msg_author->photo, + 'from-url' => (string)$msg_author->url, 'contact-id' => $contact['id'], 'title' => $subject, - 'body' => $body, - 'uri' => $message_uri, - 'parent-uri' => $author . ':' . $guid, + 'body' => Markdown::toBBCode($msg_text), + 'uri' => $msg_author_handle . ':' . $msg_guid, + 'parent-uri' => $author_handle . ':' . $guid, 'created' => $msg_created_at ]); } @@ -1643,7 +1692,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)); @@ -1656,7 +1705,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; } @@ -1670,7 +1719,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, @@ -1696,18 +1745,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); @@ -1738,14 +1787,15 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author details'); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author->getAddr()]); return false; } // Fetch the contact id - if we know this contact - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); // "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora // We would accept this anyhow. @@ -1765,11 +1815,11 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['verb'] = $verb; $datarray['gravity'] = Item::GRAVITY_ACTIVITY; @@ -1828,13 +1878,13 @@ class Diaspora */ private static function receiveMessage(array $importer, SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); $guid = XML::unescape($data->guid); $conversation_guid = XML::unescape($data->conversation_guid); $text = XML::unescape($data->text); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); - $contact = self::allowedContactByHandle($importer, $author, true); + $contact = self::allowedContactByHandle($importer, $author_uri, true); if (!$contact) { return false; } @@ -1843,41 +1893,37 @@ 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'); + try { + $author = DI::dsprContact()->getByAddr($author_uri); + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('Unable to find author details', ['author' => $author_uri->getAddr()]); return false; } $body = Markdown::toBBCode($text); - $body = self::replacePeopleGuid($body, $person['url']); + $body = self::replacePeopleGuid($body, $author->url); return Mail::insert([ 'uid' => $importer['uid'], 'guid' => $guid, 'convid' => $conversation['id'], - 'from-name' => $person['name'], - 'from-photo' => $person['photo'], - 'from-url' => $person['url'], + 'from-name' => $author->name, + 'from-photo' => (string)$author->photo, + 'from-url' => (string)$author->url, 'contact-id' => $contact['id'], 'title' => $conversation['subject'], 'body' => $body, 'reply' => 1, - 'uri' => $message_uri, - 'parent-uri' => $author . ':' . $conversation['guid'], + 'uri' => $author_uri . ':' . $guid, + 'parent-uri' => $author_uri . ':' . $conversation['guid'], 'created' => $created_at ]); } @@ -1895,7 +1941,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); @@ -1926,13 +1972,14 @@ class Diaspora return false; } - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Person not found: ' . $author); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('unable to find author details', ['author' => $author->getAddr()]); return false; } - $author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); + $author_contact = self::authorContactByUrl($contact, $author_url, $importer['uid']); // Store participation $datarray = []; @@ -1945,11 +1992,11 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); $datarray['guid'] = $guid; - $datarray['uri'] = self::getUriFromGuid($author, $guid); + $datarray['uri'] = self::getUriFromGuid($guid, $author); $datarray['verb'] = Activity::FOLLOW; $datarray['gravity'] = Item::GRAVITY_ACTIVITY; @@ -1972,8 +2019,10 @@ class Diaspora Logger::info('Participation stored', ['id' => $message_id, 'guid' => $guid, 'parent_guid' => $parent_guid, 'author' => $author]); // Send all existing comments and likes to the requesting server - $comments = Post::select(['id', 'uri-id', 'parent-author-network', 'author-network', 'verb', 'gravity'], - ['parent' => $toplevel_parent_item['id'], 'gravity' => [Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY]]); + $comments = Post::select( + ['id', 'uri-id', 'parent-author-network', 'author-network', 'verb', 'gravity'], + ['parent' => $toplevel_parent_item['id'], 'gravity' => [Item::GRAVITY_COMMENT, Item::GRAVITY_ACTIVITY]] + ); while ($comment = Post::fetch($comments)) { if (($comment['gravity'] == Item::GRAVITY_ACTIVITY) && !in_array($comment['verb'], [Activity::LIKE, Activity::DISLIKE])) { Logger::info('Unsupported activities are not relayed', ['item' => $comment['id'], 'verb' => $comment['verb']]); @@ -2016,7 +2065,7 @@ class Diaspora } /** - * Processes poll participations - unssupported + * Processes poll participations - unsupported * * @param array $importer Array of the importer user * @param object $data The message object @@ -2041,14 +2090,14 @@ 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) { return false; } - $name = XML::unescape($data->first_name).((strlen($data->last_name)) ? ' ' . XML::unescape($data->last_name) : ''); + $name = XML::unescape($data->first_name) . ((strlen($data->last_name)) ? ' ' . XML::unescape($data->last_name) : ''); $image_url = XML::unescape($data->image_url); $birthday = XML::unescape($data->birthday); $about = Markdown::toBBCode(XML::unescape($data->bio)); @@ -2069,16 +2118,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); @@ -2098,10 +2144,12 @@ class Diaspora $birthday = $contact['bd']; } - $fields = ['name' => $name, 'location' => $location, + $fields = [ + 'name' => $name, 'location' => $location, 'name-date' => DateTimeFormat::utcNow(), 'about' => $about, - 'addr' => $author, 'nick' => $nick, 'keywords' => $keywords, - 'unsearchable' => !$searchable, 'sensitive' => $nsfw]; + 'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords, + 'unsearchable' => !$searchable, 'sensitive' => $nsfw + ]; if (!empty($birthday)) { $fields['bd'] = $birthday; @@ -2143,15 +2191,17 @@ 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 + // That means that we will assume their existence if (isset($data->following)) { $following = (XML::unescape($data->following) == 'true'); } else { @@ -2202,27 +2252,29 @@ class Diaspora } elseif (!$following && $sharing) { Logger::info("Author " . $author . " wants to share with us."); } elseif ($following && $sharing) { - Logger::info("Author " . $author . " wants to have a bidirectional conection."); + Logger::info("Author " . $author . " wants to have a bidirectional connection."); } elseif ($following && !$sharing) { Logger::info("Author " . $author . " wants to listen to us."); } - $ret = FContact::getByURL($author); - - if (!$ret || ($ret['network'] != Protocol::DIASPORA)) { - Logger::notice("Cannot resolve diaspora handle " . $author . " for ".$recipient); + try { + $author_url = (string)DI::dsprContact()->getByAddr($author)->url; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('Cannot resolve diaspora handle for recipient', ['author' => $author->getAddr(), 'recipient' => $recipient]); return false; } - $cid = Contact::getIdForURL($ret['url'], $importer['uid']); + $cid = Contact::getIdForURL($author_url, $importer['uid']); if (!empty($cid)) { $contact = DBA::selectFirst('contact', [], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]); } else { $contact = []; } - $item = ['author-id' => Contact::getIdForURL($ret['url']), - 'author-link' => $ret['url']]; + $item = [ + 'author-id' => Contact::getIdForURL($author_url), + 'author-link' => $author_url + ]; $result = Contact::addRelationship($importer, $contact, $item, false); if ($result === true) { @@ -2244,62 +2296,6 @@ class Diaspora return true; } - /** - * 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 - */ - private static function addReshareActivity(array $item, int $parent_message_id, string $guid, string $author) - { - $parent = Post::selectFirst(['uri', 'guid'], ['id' => $parent_message_id]); - - $datarray = []; - - $datarray['uid'] = $item['uid']; - $datarray['contact-id'] = $item['contact-id']; - $datarray['network'] = $item['network']; - - $datarray['author-link'] = $item['author-link']; - $datarray['author-id'] = $item['author-id']; - - $datarray['owner-link'] = $datarray['author-link']; - $datarray['owner-id'] = $datarray['author-id']; - - $datarray['guid'] = $parent['guid'] . '-' . $guid; - $datarray['uri'] = self::getUriFromGuid($author, $datarray['guid']); - $datarray['thr-parent'] = $parent['uri']; - - $datarray['verb'] = $datarray['body'] = Activity::ANNOUNCE; - $datarray['gravity'] = Item::GRAVITY_ACTIVITY; - $datarray['object-type'] = Activity\ObjectType::NOTE; - - $datarray['protocol'] = $item['protocol']; - $datarray['source'] = $item['source']; - $datarray['direction'] = $item['direction']; - $datarray['post-reason'] = $item['post-reason']; - - $datarray['plink'] = self::plink($author, $datarray['guid']); - $datarray['private'] = $item['private']; - $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $item['created']; - - if (Item::isTooOld($datarray)) { - Logger::info('Reshare activity is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); - return false; - } - - $message_id = Item::insert($datarray); - - if ($message_id) { - Logger::info('Stored reshare activity.', ['guid' => $guid, 'id' => $message_id]); - if ($datarray['uid'] == 0) { - Item::distribute($message_id); - } - } - } - /** * Processes a reshare message * @@ -2314,15 +2310,20 @@ 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); + try { + $root_author = WebFingerUri::fromString(XML::unescape($data->root_author)); + } catch (\InvalidArgumentException $e) { + return false; + } + $root_guid = XML::unescape($data->root_guid); /// @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; } @@ -2336,8 +2337,9 @@ class Diaspora return true; } - $original_person = FContact::getByURL($root_author); - if (!$original_person) { + try { + $original_person = DI::dsprContact()->getByAddr($root_author); + } catch (HTTPException\NotFoundException $e) { return false; } @@ -2354,7 +2356,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; @@ -2365,7 +2367,7 @@ class Diaspora $datarray = self::setDirection($datarray, $direction); - $datarray['quote-uri-id'] = self::getQuoteUriId($root_guid, $importer['uid'], $original_person['url']); + $datarray['quote-uri-id'] = self::getQuoteUriId($root_guid, $importer['uid'], $original_person->url); if (empty($datarray['quote-uri-id'])) { return false; } @@ -2386,11 +2388,6 @@ class Diaspora self::sendParticipation($contact, $datarray); - $root_message_id = self::messageExists($importer['uid'], $root_guid); - if ($root_message_id) { - self::addReshareActivity($datarray, $root_message_id, $guid, $author); - } - if ($message_id) { Logger::info('Stored reshare ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { @@ -2433,19 +2430,18 @@ class Diaspora */ private static function itemRetraction(array $importer, array $contact, SimpleXMLElement $data): bool { - $author = XML::unescape($data->author); + $author_uri = WebFingerUri::fromString(XML::unescape($data->author)); $target_guid = XML::unescape($data->target_guid); $target_type = XML::unescape($data->target_type); - $person = FContact::getByURL($author); - if (!is_array($person)) { - Logger::notice('Unable to find author detail for ' . $author); + try { + $author = DI::dsprContact()->getByAddr($author_uri); + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + Logger::notice('Unable to find details for author', ['author' => $author_uri->getAddr()]); return false; } - if (empty($contact['url'])) { - $contact['url'] = $person['url']; - } + $contact_url = $contact['url'] ?? '' ?: (string)$author->url; // Fetch items that are about to be deleted $fields = ['uid', 'id', 'parent', 'author-link', 'uri-id']; @@ -2473,8 +2469,8 @@ class Diaspora $parent = Post::selectFirst(['author-link'], ['id' => $item['parent']]); // Only delete it if the parent author really fits - if (!Strings::compareLink($parent['author-link'], $contact['url']) && !Strings::compareLink($item['author-link'], $contact['url'])) { - Logger::info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact['url']); + if (!Strings::compareLink($parent['author-link'], $contact_url) && !Strings::compareLink($item['author-link'], $contact_url)) { + Logger::info("Thread author " . $parent['author-link'] . " and item author " . $item['author-link'] . " don't fit to expected contact " . $contact_url); continue; } @@ -2490,14 +2486,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); @@ -2570,19 +2566,21 @@ class Diaspora * * @param int $uriid * @param object $photo + * * @return void */ private static function storePhotoAsMedia(int $uriid, $photo) { // @TODO Need to find object type, roland@f.haeder.net - Logger::debug('photo='.get_class($photo)); - $data = []; - $data['uri-id'] = $uriid; - $data['type'] = Post\Media::IMAGE; - $data['url'] = XML::unescape($photo->remote_photo_path) . XML::unescape($photo->remote_photo_name); - $data['height'] = (int)XML::unescape($photo->height ?? 0); - $data['width'] = (int)XML::unescape($photo->width ?? 0); - $data['description'] = XML::unescape($photo->text ?? ''); + Logger::debug('photo=' . get_class($photo)); + $data = [ + 'uri-id' => $uriid, + 'type' => Post\Media::IMAGE, + 'url' => XML::unescape($photo->remote_photo_path) . XML::unescape($photo->remote_photo_name), + 'height' => (int)XML::unescape($photo->height ?? 0), + 'width' => (int)XML::unescape($photo->width ?? 0), + 'description' => XML::unescape($photo->text ?? ''), + ]; Post\Media::insert($data); } @@ -2624,14 +2622,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; } @@ -2654,11 +2652,32 @@ class Diaspora $raw_body = $body = Markdown::toBBCode($text); - $datarray = []; + $datarray = [ + 'guid' => $guid, + 'plink' => self::plink($author, $guid), + 'uid' => $importer['uid'], + 'contact-id' => $contact['id'], + 'network' => Protocol::DIASPORA, + 'author-link' => $contact['url'], + 'author-id' => Contact::getIdForURL($contact['url'], 0), + 'verb' => Activity::POST, + 'gravity' => Item::GRAVITY_PARENT, + 'protocol' => Conversation::PARCEL_DIASPORA, + 'source' => $xml, + 'body' => self::replacePeopleGuid($body, $contact['url']), + 'raw-body' => self::replacePeopleGuid($raw_body, $contact['url']), + 'private' => (($public == 'false') ? Item::PRIVATE : Item::PUBLIC), + // Default is note (aka. comment), later below is being checked the real type + 'object-type' => Activity\ObjectType::NOTE, + 'post-type' => Item::PT_NOTE, + ]; - $datarray['guid'] = $guid; - $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid); - $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); + $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author); + $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); + $datarray['owner-link'] = $datarray['author-link']; + $datarray['owner-id'] = $datarray['author-id']; + + $datarray = self::setDirection($datarray, $direction); // Attach embedded pictures to the body if ($data->photo) { @@ -2669,11 +2688,7 @@ class Diaspora $datarray['object-type'] = Activity\ObjectType::IMAGE; $datarray['post-type'] = Item::PT_IMAGE; } elseif ($data->poll) { - $datarray['object-type'] = Activity\ObjectType::NOTE; $datarray['post-type'] = Item::PT_POLL; - } else { - $datarray['object-type'] = Activity\ObjectType::NOTE; - $datarray['post-type'] = Item::PT_NOTE; } /// @todo enable support for polls @@ -2685,27 +2700,6 @@ class Diaspora /// @todo enable support for events - $datarray['uid'] = $importer['uid']; - $datarray['contact-id'] = $contact['id']; - $datarray['network'] = Protocol::DIASPORA; - - $datarray['author-link'] = $contact['url']; - $datarray['author-id'] = Contact::getIdForURL($contact['url'], 0); - - $datarray['owner-link'] = $datarray['author-link']; - $datarray['owner-id'] = $datarray['author-id']; - - $datarray['verb'] = Activity::POST; - $datarray['gravity'] = Item::GRAVITY_PARENT; - - $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['source'] = $xml; - - $datarray = self::setDirection($datarray, $direction); - - $datarray['body'] = self::replacePeopleGuid($body, $contact['url']); - $datarray['raw-body'] = self::replacePeopleGuid($raw_body, $contact['url']); - self::storeMentions($datarray['uri-id'], $text); Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray['body']); @@ -2718,8 +2712,6 @@ class Diaspora $datarray['app'] = $provider_display_name; } - $datarray['plink'] = self::plink($author, $guid); - $datarray['private'] = (($public == 'false') ? Item::PRIVATE : Item::PUBLIC); $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; if (isset($address['address'])) { @@ -2757,7 +2749,7 @@ class Diaspora * ************************************************************************************** */ /** - * returnes the handle of a contact + * returns the handle of a contact * * @param array $contact contact array * @@ -2771,7 +2763,7 @@ class Diaspora } // Normally we should have a filled "addr" field - but in the past this wasn't the case - // So - just in case - we build the the address here. + // So - just in case - we build the address here. if ($contact['nickname'] != '') { $nick = $contact['nickname']; } else { @@ -2840,7 +2832,7 @@ class Diaspora public static function buildMagicEnvelope(string $msg, array $user): string { $b64url_data = Strings::base64UrlEncode($msg); - $data = str_replace(["\n", "\r", " ", "\t"], ['', '', '', ''], $b64url_data); + $data = str_replace(["\n", "\r", ' ', "\t"], ['', '', '', ''], $b64url_data); $key_id = Strings::base64UrlEncode(self::myHandle($user)); $type = 'application/xml'; @@ -2858,18 +2850,18 @@ class Diaspora $xmldata = [ 'me:env' => [ - 'me:data' => $data, - '@attributes' => ['type' => $type], - 'me:encoding' => $encoding, - 'me:alg' => $alg, - 'me:sig' => $sig, + 'me:data' => $data, + '@attributes' => ['type' => $type], + 'me:encoding' => $encoding, + 'me:alg' => $alg, + 'me:sig' => $sig, '@attributes2' => ['key_id' => $key_id] ] ]; $namespaces = ['me' => ActivityNamespace::SALMON_ME]; - return XML::fromArray($xmldata, $xml, false, $namespaces); + return XML::fromArray($xmldata, $dummy, false, $namespaces); } /** @@ -2939,13 +2931,12 @@ class Diaspora $logid = Strings::getRandomHex(4); - // We always try to use the data from the fcontact table. + // We always try to use the data from the diaspora-contact table. // This is important for transmitting data to Friendica servers. - if (!empty($contact['addr'])) { - $fcontact = FContact::getByURL($contact['addr']); - if (!empty($fcontact)) { - $dest_url = ($public_batch ? $fcontact['batch'] : $fcontact['notify']); - } + try { + $target = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr'])); + $dest_url = $public_batch ? $target->batch : $target->notify; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { } if (empty($dest_url)) { @@ -2957,7 +2948,7 @@ class Diaspora return 0; } - Logger::notice('transmit: ' . $logid . '-' . $guid . ' ' . $dest_url); + Logger::info('transmit: ' . $logid . '-' . $guid . ' ' . $dest_url); if (!intval(DI::config()->get('system', 'diaspora_test'))) { $content_type = (($public_batch) ? 'application/magic-envelope+xml' : 'application/json'); @@ -2969,7 +2960,13 @@ class Diaspora return 200; } - Logger::notice('transmit: ' . $logid . '-' . $guid . ' to ' . $dest_url . ' returns: ' . $return_code); + if (!empty($contact['gsid']) && (empty($return_code) || $postResult->isTimeout())) { + GServer::setFailureById($contact['gsid']); + } elseif (!empty($contact['gsid']) && ($return_code >= 200) && ($return_code <= 299)) { + GServer::setReachableById($contact['gsid'], Protocol::DIASPORA); + } + + Logger::info('transmit: ' . $logid . '-' . $guid . ' to ' . $dest_url . ' returns: ' . $return_code); return $return_code ? $return_code : -1; } @@ -2982,12 +2979,11 @@ class Diaspora * @param array $message The message data * * @return string The post XML + * @throws \Exception */ public static function buildPostXml(string $type, array $message): string { - $data = [$type => $message]; - - return XML::fromArray($data, $xml); + return XML::fromArray([$type => $message]); } /** @@ -3014,18 +3010,18 @@ class Diaspora } // When sending content to Friendica contacts using the Diaspora protocol - // we have to fetch the public key from the fcontact. + // we have to fetch the public key from the diaspora-contact. // This is due to the fact that legacy DFRN had unique keys for every contact. $pubkey = $contact['pubkey']; if (!empty($contact['addr'])) { - $fcontact = FContact::getByURL($contact['addr']); - if (!empty($fcontact)) { - $pubkey = $fcontact['pubkey']; + try { + $pubkey = DI::dsprContact()->getByAddr(WebFingerUri::fromString($contact['addr']))->pubKey; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { } } else { // The "addr" field should always be filled. // If this isn't the case, it will raise a notice some lines later. - // And in the log we will see where it came from and we can handle it there. + // And in the log we will see where it came from, and we can handle it there. Logger::notice('Empty addr', ['contact' => $contact ?? [], 'callstack' => System::callstack(20)]); } @@ -3066,24 +3062,26 @@ class Diaspora // If the item belongs to a user, we take this user id. if ($item['uid'] == 0) { // @todo Possibly use an administrator account? - $condition = ['verified' => true, 'blocked' => false, - 'account_removed' => false, 'account_expired' => false, 'account-type' => User::ACCOUNT_TYPE_PERSON]; + $condition = [ + 'verified' => true, 'blocked' => false, + 'account_removed' => false, 'account_expired' => false, 'account-type' => User::ACCOUNT_TYPE_PERSON + ]; $first_user = DBA::selectFirst('user', ['uid'], $condition, ['order' => ['uid']]); $owner = User::getOwnerDataById($first_user['uid']); } else { $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); @@ -3107,7 +3105,7 @@ class Diaspora $old_handle = DI::pConfig()->get($uid, 'system', 'previous_addr'); $profile = self::createProfileData($uid); - $signed_text = 'AccountMigration:'.$old_handle.':'.$profile['author']; + $signed_text = 'AccountMigration:' . $old_handle . ':' . $profile['author']; $signature = base64_encode(Crypto::rsaSign($signed_text, $owner['uprvkey'], 'sha256')); $message = [ @@ -3201,7 +3199,7 @@ class Diaspora */ public static function getReshareDetails(array $item): array { - $reshared = DI::contentItem()->getSharedPost($item, ['network', 'author-addr']); + $reshared = DI::contentItem()->getSharedPost($item, ['guid', 'network', 'author-addr']); if (empty($reshared)) { return []; } @@ -3213,7 +3211,7 @@ class Diaspora return [ 'root_handle' => strtolower($reshared['post']['author-addr']), - 'root_guid' => $reshared['guid'] + 'root_guid' => $reshared['post']['guid'], ]; } @@ -3324,14 +3322,23 @@ class Diaspora $type = 'reshare'; } else { + $native_photos = DI::config()->get('diaspora', 'native_photos'); + if ($native_photos) { + $item['body'] = Post\Media::removeFromEndOfBody($item['body']); + $attach_media = [Post\Media::AUDIO, Post\Media::VIDEO]; + } else { + $attach_media = [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]; + } + $title = $item['title']; - $body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item)); + $body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item), $attach_media); + $body = Post\Media::addHTMLLinkToBody($item['uri-id'], $body); // Fetch the title from an attached link - if there is one if (empty($item['title']) && DI::pConfig()->get($owner['uid'], 'system', 'attach_link_title')) { - $page_data = BBCode::getAttachmentData($item['body']); - if (!empty($page_data['type']) && !empty($page_data['title']) && ($page_data['type'] == 'link')) { - $title = $page_data['title']; + $media = Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]); + if (!empty($media) && !empty($media[0]['name']) && ($media[0]['name'] != $media[0]['url'])) { + $title = $media[0]['name']; } } @@ -3343,7 +3350,7 @@ class Diaspora $body = '### ' . html_entity_decode($title) . "\n\n" . $body; } - $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT, Post\Media::UNKNOWN]); + $attachments = Post\Media::getByURIId($item['uri-id'], [Post\Media::DOCUMENT, Post\Media::TORRENT]); if (!empty($attachments)) { $body .= "\n[hr]\n"; foreach ($attachments as $attachment) { @@ -3373,6 +3380,10 @@ class Diaspora 'location' => $location ]; + if ($native_photos) { + $message = self::addPhotos($item, $message); + } + // Diaspora rejects messages when they contain a location without "lat" or "lng" if (!isset($location['lat']) || !isset($location['lng'])) { unset($message['location']); @@ -3383,9 +3394,11 @@ class Diaspora if (count($event)) { $message['event'] = $event; - if (!empty($event['location']['address']) && + if ( + !empty($event['location']['address']) && !empty($event['location']['lat']) && - !empty($event['location']['lng'])) { + !empty($event['location']['lng']) + ) { $message['location'] = $event['location']; } @@ -3407,10 +3420,49 @@ class Diaspora return $msg; } + /** + * Add photo elements to the message array + * + * @param array $item + * @param array $message + * @return array + */ + private static function addPhotos(array $item, array $message): array + { + $medias = Post\Media::getByURIId($item['uri-id'], [Post\Media::IMAGE]); + $public = ($item['private'] == Item::PRIVATE ? 'false' : 'true'); + + $counter = 0; + foreach ($medias as $media) { + if (Item::containsLink($item['body'], $media['preview'] ?? $media['url'], $media['type'])) { + continue; + } + + $name = basename($media['url']); + $path = str_replace($name, '', $media['url']); + + $message[++$counter . ':photo'] = [ + 'guid' => Item::guid(['uri' => $media['url']], false), + 'author' => $item['author-addr'], + 'public' => $public, + 'created_at' => $item['created'], + 'remote_photo_path' => $path, + 'remote_photo_name' => $name, + 'status_message_guid' => $item['guid'], + 'height' => $media['height'], + 'width' => $media['width'], + 'text' => $media['description'], + ]; + } + + return $message; + } + private static function prependParentAuthorMention(string $body, string $profile_url): string { $profile = Contact::getByURL($profile_url, false, ['addr', 'name']); - if (!empty($profile['addr']) + if ( + !empty($profile['addr']) && !strstr($body, $profile['addr']) && !strstr($body, $profile_url) ) { @@ -3543,6 +3595,7 @@ class Diaspora } $body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item)); + $body = Post\Media::addHTMLLinkToBody($item['uri-id'], $body); // The replied to autor mention is prepended for clarity if: // - Item replied isn't yours @@ -3864,9 +3917,9 @@ class Diaspora $kw = str_replace(' ', ' ', $kw); $arr = explode(' ', $kw); if (count($arr)) { - for ($x = 0; $x < 5; $x ++) { + for ($x = 0; $x < 5; $x++) { if (!empty($arr[$x])) { - $data['tag_string'] .= '#'. trim($arr[$x]) .' '; + $data['tag_string'] .= '#' . trim($arr[$x]) . ' '; } } } @@ -3989,7 +4042,7 @@ class Diaspora return false; } - if (!self::parentSupportDiaspora($item['thr-parent-id'])) { + if (!self::parentSupportDiaspora($item['thr-parent-id'], $uid)) { Logger::info('One of the parents does not support Diaspora. A signature will not be created.', ['uri-id' => $item['uri-id'], 'guid' => $item['guid']]); return false; } @@ -4008,28 +4061,31 @@ class Diaspora * Check if the parent and their parents support Diaspora * * @param integer $parent_id + * @param integer $uid * @return boolean + * @throws InternalServerErrorException + * @throws \ImagickException */ - private static function parentSupportDiaspora(int $parent_id): bool + private static function parentSupportDiaspora(int $parent_id, int $uid): bool { - $parent_post = Post::selectFirstPost(['gravity', 'signed_text', 'author-link', 'thr-parent-id'], ['uri-id' => $parent_id]); + $parent_post = Post::selectFirst(['gravity', 'signed_text', 'author-link', 'thr-parent-id', 'protocol'], ['uri-id' => $parent_id, 'uid' => [0, $uid]]); if (empty($parent_post['thr-parent-id'])) { Logger::warning('Parent post does not exist.', ['parent-id' => $parent_id]); return false; } - if (empty(FContact::getByURL($parent_post['author-link'], false))) { + if (!self::isSupportedByContactUrl($parent_post['author-link'])) { Logger::info('Parent author is no Diaspora contact.', ['parent-id' => $parent_id]); return false; } - if (($parent_post['gravity'] == Item::GRAVITY_COMMENT) && empty($parent_post['signed_text'])) { + if (($parent_post['protocol'] != Conversation::PARCEL_DIASPORA) && ($parent_post['gravity'] == Item::GRAVITY_COMMENT) && empty($parent_post['signed_text'])) { Logger::info('Parent comment has got no Diaspora signature.', ['parent-id' => $parent_id]); return false; } if ($parent_post['gravity'] == Item::GRAVITY_COMMENT) { - return self::parentSupportDiaspora($parent_post['thr-parent-id']); + return self::parentSupportDiaspora($parent_post['thr-parent-id'], $uid); } return true; @@ -4049,7 +4105,7 @@ class Diaspora 'body' => '', 'quote-uri-id' => $UriId, 'allow_cid' => $owner['allow_cid'] ?? '', - 'allow_gid' => $owner['allow_gid']?? '', + 'allow_gid' => $owner['allow_gid'] ?? '', 'deny_cid' => $owner['deny_cid'] ?? '', 'deny_gid' => $owner['deny_gid'] ?? '', ];