X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FDiaspora.php;h=dd6c2115b5e32714a868bae0ee589465ca76bf31;hb=fdaff4303952427f222ee21f6b501d5087e25932;hp=1f21ea498168602fe73cbf6ca0f11baa52a7f542;hpb=ee0da5d6f94c6430a1c530e91bffa9d181372221;p=friendica.git diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 1f21ea4981..dd6c2115b5 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -1,6 +1,6 @@ $item['parent'], 'gravity' => [GRAVITY_COMMENT, 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; } @@ -124,18 +132,18 @@ class Diaspora $basedom = XML::parseString($envelope, true); if (!is_object($basedom)) { - Logger::notice("Envelope is no XML file"); + Logger::notice('Envelope is no XML file'); return false; } - $children = $basedom->children('http://salmon-protocol.org/ns/magic-env'); + $children = $basedom->children(ActivityNamespace::SALMON_ME); if (sizeof($children) == 0) { - Logger::notice("XML has no children"); + Logger::notice('XML has no children'); return false; } - $handle = ""; + $handle = ''; $data = Strings::base64UrlDecode($children->data); $type = $children->data->attributes()->type[0]; @@ -160,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; } @@ -223,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)) { + 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'); + } + + $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; } @@ -250,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(); } } @@ -280,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(); } } @@ -300,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 ]; } @@ -340,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)) { @@ -368,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); @@ -387,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(); } @@ -407,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 { @@ -423,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 ]; } @@ -468,14 +496,13 @@ class Diaspora */ public static function dispatchPublic(array $msg, int $direction) { - $enabled = intval(DI::config()->get("system", "diaspora_enabled")); - if (!$enabled) { - Logger::notice("diaspora is disabled"); + if (!DI::config()->get('system', 'diaspora_enabled')) { + Logger::notice('Diaspora is disabled'); return false; } if (!($fields = self::validPosting($msg))) { - Logger::notice("Invalid posting"); + Logger::notice('Invalid posting', ['msg' => $msg]); return false; } @@ -504,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::notice("Invalid posting"); + Logger::notice('Invalid posting', ['msg' => $msg]); return false; } } else { @@ -519,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': @@ -589,7 +616,7 @@ class Diaspora return self::receiveStatusMessage($importer, $fields, $msg['message'], $direction); default: - Logger::notice("Unknown message type " . $type); + Logger::notice('Unknown message type ' . $type); return false; } } @@ -629,7 +656,7 @@ class Diaspora $type = $element->getName(); $orig_type = $type; - Logger::debug("Got message type " . $type . ": " . $msg['message']); + Logger::debug('Got message', ['type' => $type, 'message' => $msg['message']]); // All retractions are handled identically from now on. // In the new version there will only be "retraction". @@ -695,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); @@ -714,33 +742,43 @@ class Diaspora if (!in_array($type, ['comment', 'like'])) { return $fields; } + + if (!isset($author_signature) && ($msg['author'] == $fields->author)) { + Logger::debug('No author signature, but the sender matches the author', ['type' => $type, 'msg-author' => $msg['author'], 'message' => $msg['message']]); + return $fields; + } + // No author_signature? This is a must, so we quit. if (!isset($author_signature)) { - Logger::info("No author signature for type " . $type . " - Message: " . $msg['message']); + Logger::info('No author signature', ['type' => $type, 'msg-author' => $msg['author'], 'fields-author' => $fields->author, 'message' => $msg['message']]); return false; } 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; } if (!Crypto::rsaVerify($signed_data, $parent_author_signature, $key, 'sha256')) { - Logger::info("No valid parent author signature for parent author " . $msg['author'] . " in type " . $type . " - signed data: " . $signed_data . " - Message: " . $msg['message'] . " - Signature " . $parent_author_signature); + Logger::info('No valid parent author signature', ['author' => $msg['author'], 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $parent_author_signature]); return false; } } - $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; } if (!Crypto::rsaVerify($signed_data, $author_signature, $key, 'sha256')) { - Logger::info("No valid author signature for author " . $fields->author . " in type " . $type . " - signed data: " . $signed_data . " - Message: " . $msg['message'] . " - Signature " . $author_signature); + Logger::info('No valid author signature for author', ['author' => $fields->author, 'type' => $type, 'signed data' => $signed_data, 'message' => $msg['message'], 'signature' => $author_signature]); return false; } else { return $fields; @@ -750,85 +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 - { - $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']; - } - - return ''; - } - - /** - * get a handle (user@domain.tld) from a given contact id - * - * @param int $contact_id The id in the contact table - * @param int $pcontact_id The id in the contact table (Used for the public contact) - * - * @return string the handle - * @throws \Exception - */ - private static function handleFromContact(int $contact_id, int $pcontact_id = 0): string + private static function key(WebFingerUri $uri): string { - $handle = ''; - - if ($pcontact_id != 0) { - $contact = Contact::getById($pcontact_id, ['addr']); - if (DBA::isResult($contact)) { - $handle = $contact['addr']; - } - } - - if (empty($handle)) { - $contact = Contact::getById($contact_id, ['addr']); - if (DBA::isResult($contact)) { - $handle = $contact['addr']; - } + 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 strtolower($handle); } /** * 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; } /** @@ -882,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 { @@ -905,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; @@ -924,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']; } @@ -935,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) @@ -974,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]'; @@ -1029,7 +1042,7 @@ class Diaspora $server = $serverparts['scheme'] . '://' . $serverparts['host']; - Logger::info("Trying to fetch item " . $guid . " from " . $server); + Logger::info('Trying to fetch item ' . $guid . ' from ' . $server); $msg = self::message($guid, $server); @@ -1037,7 +1050,7 @@ class Diaspora return false; } - Logger::info("Successfully fetched item " . $guid . " from " . $server); + Logger::info('Successfully fetched item ' . $guid . ' from ' . $server); // Now call the dispatcher return self::dispatchPublic($msg, $force ? self::FORCED_FETCH : self::FETCHED); @@ -1065,16 +1078,16 @@ class Diaspora // This will work for new Diaspora servers and Friendica servers from 3.5 $source_url = $server . '/fetch/post/' . urlencode($guid); - Logger::info("Fetch post from " . $source_url); + Logger::info('Fetch post from ' . $source_url); $envelope = DI::httpClient()->fetch($source_url, HttpClientAccept::MAGIC); if ($envelope) { - Logger::info("Envelope was fetched."); + Logger::info('Envelope was fetched.'); $x = self::verifyMagicEnvelope($envelope); if (!$x) { - Logger::info("Envelope could not be verified."); + Logger::info('Envelope could not be verified.'); } else { - Logger::info("Envelope was verified."); + Logger::info('Envelope was verified.'); } } else { $x = false; @@ -1092,33 +1105,35 @@ class Diaspora if ($source_xml->post->reshare) { // Reshare of a reshare - old Diaspora version - Logger::info("Message is a reshare"); + Logger::info('Message is a reshare'); return self::message($source_xml->post->reshare->root_guid, $server, ++$level); - } elseif ($source_xml->getName() == "reshare") { + } elseif ($source_xml->getName() == 'reshare') { // Reshare of a reshare - new Diaspora version - Logger::info("Message is a new reshare"); + Logger::info('Message is a new reshare'); 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) ]; } @@ -1136,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; } @@ -1157,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; } } @@ -1165,62 +1180,68 @@ 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()]); } } if (!DBA::isResult($item)) { - Logger::notice("parent item not found: parent: " . $guid . " - user: " . $uid); + 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']; @@ -1325,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; @@ -1349,27 +1376,27 @@ 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; } $fields = [ - 'url' => $data['url'], - 'nurl' => Strings::normaliseLink($data['url']), - 'name' => $data['name'], - 'nick' => $data['nick'], - 'addr' => $data['addr'], - 'batch' => $data['batch'], - 'notify' => $data['notify'], - 'poll' => $data['poll'], - 'network' => $data['network'] + 'url' => $data['url'], + 'nurl' => Strings::normaliseLink($data['url']), + 'name' => $data['name'], + 'nick' => $data['nick'], + 'addr' => $data['addr'], + 'batch' => $data['batch'], + 'notify' => $data['notify'], + 'poll' => $data['poll'], + '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; } @@ -1384,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; } @@ -1400,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 = 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 ''; @@ -1451,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); @@ -1488,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 = ''; } @@ -1512,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 = []; @@ -1527,27 +1551,21 @@ 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 - if (in_array($direction, [self::FETCHED, self::FORCED_FETCH])) { - $datarray['post-reason'] = Item::PR_FETCHED; - } elseif ($datarray['uid'] == 0) { - $datarray['post-reason'] = Item::PR_GLOBAL; - } else { - $datarray['post-reason'] = Item::PR_COMMENT; - } + $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; - $datarray['gravity'] = GRAVITY_COMMENT; + $datarray['gravity'] = Item::GRAVITY_COMMENT; $datarray['thr-parent'] = $thr_parent ?: $toplevel_parent_item['uri']; @@ -1556,14 +1574,15 @@ class Diaspora $datarray['protocol'] = Conversation::PARCEL_DIASPORA; $datarray['source'] = $xml; - $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; + + $datarray = self::setDirection($datarray, $direction); $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; $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']); @@ -1588,7 +1607,7 @@ class Diaspora } if ($message_id) { - Logger::info("Stored comment " . $datarray['guid'] . " with message id " . $message_id); + Logger::info('Stored comment ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id, json_encode($data)); } @@ -1613,47 +1632,50 @@ 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); $msg_created_at = DateTimeFormat::utc(XML::unescape($mesg->created_at)); if ($msg_conversation_guid != $guid) { - Logger::notice("message conversation guid does not belong to the current conversation."); + Logger::notice('Message conversation guid does not belong to the current conversation.', ['guid' => $guid]); 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 ]); } @@ -1670,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)); @@ -1679,11 +1701,11 @@ class Diaspora $messages = $data->message; if (!count($messages)) { - Logger::notice("empty conversation"); + Logger::notice('Empty conversation'); return false; } - $contact = self::allowedContactByHandle($importer, $msg['author'], true); + $contact = self::allowedContactByHandle($importer, WebFingerUri::fromString($msg['author']), true); if (!$contact) { return false; } @@ -1697,17 +1719,19 @@ class Diaspora $r = DBA::insert('conv', [ 'uid' => $importer['uid'], 'guid' => $guid, - 'creator' => $author, + 'creator' => $author_handle, 'created' => $created_at, 'updated' => DateTimeFormat::utcNow(), 'subject' => $subject, - 'recips' => $participants]); + 'recips' => $participants + ]); + if ($r) { $conversation = DBA::selectFirst('conv', [], ['uid' => $importer['uid'], 'guid' => $guid]); } } if (!$conversation) { - Logger::notice("unable to create conversation."); + Logger::warning('Unable to create conversation.'); return false; } @@ -1721,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); @@ -1763,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. @@ -1783,20 +1808,21 @@ class Diaspora $datarray = []; $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $author_contact['cid']; $datarray['network'] = $author_contact['network']; - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray = self::setDirection($datarray, $direction); + + $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'] = GRAVITY_ACTIVITY; + $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['thr-parent'] = $toplevel_parent_item['uri']; $datarray['object-type'] = Activity\ObjectType::NOTE; @@ -1807,7 +1833,7 @@ class Diaspora $datarray['changed'] = $datarray['created'] = $datarray['edited'] = DateTimeFormat::utcNow(); // like on comments have the comment as parent. So we need to fetch the toplevel parent - if ($toplevel_parent_item['gravity'] != GRAVITY_PARENT) { + if ($toplevel_parent_item['gravity'] != Item::GRAVITY_PARENT) { $toplevel = Post::selectFirst(['origin'], ['id' => $toplevel_parent_item['parent']]); $origin = $toplevel['origin']; } else { @@ -1832,7 +1858,7 @@ class Diaspora } if ($message_id) { - Logger::info("Stored like " . $datarray['guid'] . " with message id " . $message_id); + Logger::info('Stored like ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id, json_encode($data)); } @@ -1852,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; } @@ -1867,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."); + 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 ]); } @@ -1919,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); @@ -1950,32 +1972,34 @@ 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 = []; $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; $datarray['uid'] = $importer['uid']; $datarray['contact-id'] = $author_contact['cid']; $datarray['network'] = $author_contact['network']; - $datarray['owner-link'] = $datarray['author-link'] = $person['url']; - $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); + $datarray = self::setDirection($datarray, $direction); + + $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'] = GRAVITY_ACTIVITY; + $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['thr-parent'] = $toplevel_parent_item['uri']; $datarray['object-type'] = Activity\ObjectType::NOTE; @@ -1995,11 +2019,13 @@ 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'], - ['parent' => $toplevel_parent_item['id'], 'gravity' => [GRAVITY_COMMENT, 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 (in_array($comment['verb'], [Activity::FOLLOW, Activity::TAG])) { - Logger::info('participation messages are not relayed', ['item' => $comment['id']]); + 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']]); continue; } @@ -2014,7 +2040,7 @@ class Diaspora } Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact['cid']]); - if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $author_contact['cid'])) { + if (Worker::add(Worker::PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['uri-id'], $author_contact['cid'], $datarray['uid'])) { Post\DeliveryData::incrementQueueCount($comment['uri-id'], 1); } } @@ -2039,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 @@ -2064,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)); @@ -2092,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); @@ -2121,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; @@ -2132,7 +2157,7 @@ class Diaspora Contact::update($fields, ['id' => $contact['id']]); - Logger::info("Profile of contact " . $contact['id'] . " stored for user " . $importer['uid']); + Logger::info('Profile of contact ' . $contact['id'] . ' stored for user ' . $importer['uid']); return true; } @@ -2166,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 { @@ -2193,7 +2220,7 @@ class Diaspora // That makes us friends. if ($contact) { if ($following) { - Logger::info("Author " . $author . " (Contact " . $contact['id'] . ") wants to follow us."); + Logger::info('Author ' . $author . ' (Contact ' . $contact['id'] . ') wants to follow us.'); self::receiveRequestMakeFriend($importer, $contact); // refetch the contact array @@ -2204,7 +2231,7 @@ class Diaspora if (in_array($contact['rel'], [Contact::FRIEND])) { $user = DBA::selectFirst('user', [], ['uid' => $importer['uid']]); if (DBA::isResult($user)) { - Logger::info("Sending share message to author " . $author . " - Contact: " . $contact['id'] . " - User: " . $importer['uid']); + Logger::info('Sending share message to author ' . $author . ' - Contact: ' . $contact['id'] . ' - User: ' . $importer['uid']); self::sendShare($user, $contact); } } @@ -2225,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) { @@ -2267,138 +2296,6 @@ class Diaspora return true; } - /** - * Fetches a message with a given guid - * - * @param string $guid message guid - * @param string $orig_author handle of the original post - * @return array|bool The fetched item or false on failure - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function originalItem(string $guid, string $orig_author) - { - if (empty($guid)) { - Logger::notice('Empty guid. Quitting.'); - return false; - } - - // Do we already have this item? - $fields = ['body', 'title', 'app', 'created', 'object-type', 'uri', 'guid', - 'author-name', 'author-link', 'author-avatar', 'plink', 'uri-id']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; - $item = Post::selectFirst($fields, $condition); - - if (DBA::isResult($item)) { - Logger::notice("reshared message " . $guid . " already exists on system."); - - // Maybe it is already a reshared item? - // Then refetch the content, if it is a reshare from a reshare. - // If it is a reshared post from another network then reformat to avoid display problems with two share elements - if (self::isReshare($item['body'], true)) { - $item = []; - } elseif (self::isReshare($item['body'], false) || strstr($item['body'], '[share')) { - $item['body'] = Markdown::toBBCode(BBCode::toMarkdown($item['body'])); - - $item['body'] = self::replacePeopleGuid($item['body'], $item['author-link']); - - return $item; - } else { - return $item; - } - } - - if (!DBA::isResult($item)) { - if (empty($orig_author)) { - Logger::notice('Empty author for guid ' . $guid . '. Quitting.'); - return false; - } - - $server = 'https://' . substr($orig_author, strpos($orig_author, '@') + 1); - Logger::notice("1st try: reshared message " . $guid . " will be fetched via SSL from the server " . $server); - $stored = self::storeByGuid($guid, $server, true); - - if (!$stored) { - $server = 'http://' . substr($orig_author, strpos($orig_author, '@') + 1); - Logger::notice("2nd try: reshared message " . $guid . " will be fetched without SSL from the server " . $server); - $stored = self::storeByGuid($guid, $server, true); - } - - if ($stored) { - $fields = ['body', 'title', 'app', 'created', 'object-type', 'uri', 'guid', - 'author-name', 'author-link', 'author-avatar', 'plink', 'uri-id']; - $condition = ['guid' => $guid, 'visible' => true, 'deleted' => false, 'private' => [Item::PUBLIC, Item::UNLISTED]]; - $item = Post::selectFirst($fields, $condition); - - if (DBA::isResult($item)) { - // If it is a reshared post from another network then reformat to avoid display problems with two share elements - if (self::isReshare($item['body'], false)) { - $item['body'] = Markdown::toBBCode(BBCode::toMarkdown($item['body'])); - $item['body'] = self::replacePeopleGuid($item['body'], $item['author-link']); - } - - return $item; - } - } - } - return false; - } - - /** - * 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'] = GRAVITY_ACTIVITY; - $datarray['object-type'] = Activity\ObjectType::NOTE; - - $datarray['protocol'] = $item['protocol']; - $datarray['source'] = $item['source']; - $datarray['direction'] = $item['direction']; - - $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 * @@ -2413,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; } @@ -2435,15 +2337,12 @@ class Diaspora return true; } - $original_item = self::originalItem($root_guid, $root_author); - if (!$original_item) { + try { + $original_person = DI::dsprContact()->getByAddr($root_author); + } catch (HTTPException\NotFoundException $e) { return false; } - if (empty($original_item['plink'])) { - $original_item['plink'] = self::plink($root_author, $root_guid); - } - $datarray = []; $datarray['uid'] = $importer['uid']; @@ -2457,43 +2356,27 @@ 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; - $datarray['gravity'] = GRAVITY_PARENT; + $datarray['gravity'] = Item::GRAVITY_PARENT; $datarray['protocol'] = Conversation::PARCEL_DIASPORA; $datarray['source'] = $xml; - $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; - - /// @todo Copy tag data from original post - $prefix = BBCode::getShareOpeningTag( - $original_item['author-name'], - $original_item['author-link'], - $original_item['author-avatar'], - $original_item['plink'], - $original_item['created'], - $original_item['guid'] - ); + $datarray = self::setDirection($datarray, $direction); - if (!empty($original_item['title'])) { - $prefix .= '[h3]' . $original_item['title'] . "[/h3]\n"; + $datarray['quote-uri-id'] = self::getQuoteUriId($root_guid, $importer['uid'], $original_person->url); + if (empty($datarray['quote-uri-id'])) { + return false; } - $datarray['body'] = $prefix.$original_item['body'] . '[/share]'; - - Tag::storeFromBody($datarray['uri-id'], $datarray['body']); - - $datarray['app'] = $original_item['app']; - - $datarray['plink'] = self::plink($author, $guid); + $datarray['body'] = ''; + $datarray['plink'] = self::plink($author, $guid); $datarray['private'] = (($public == 'false') ? Item::PRIVATE : Item::PUBLIC); $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; - $datarray['object-type'] = $original_item['object-type']; - self::fetchGuid($datarray); if (Item::isTooOld($datarray)) { @@ -2505,13 +2388,8 @@ 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); + Logger::info('Stored reshare ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id); } @@ -2521,6 +2399,25 @@ class Diaspora } } + private static function getQuoteUriId(string $guid, int $uid, string $host): int + { + $shared_item = Post::selectFirst(['uri-id'], ['guid' => $guid, 'uid' => [$uid, 0], 'private' => [Item::PUBLIC, Item::UNLISTED]]); + + if (!DBA::isResult($shared_item) && !empty($host) && Diaspora::storeByGuid($guid, $host, true)) { + Logger::debug('Fetched post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + $shared_item = Post::selectFirst(['uri-id'], ['guid' => $guid, 'uid' => [$uid, 0], 'private' => [Item::PUBLIC, Item::UNLISTED]]); + } elseif (DBA::isResult($shared_item)) { + Logger::debug('Found existing post', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + } + + if (!DBA::isResult($shared_item)) { + Logger::notice('Post does not exist.', ['guid' => $guid, 'host' => $host, 'uid' => $uid]); + return 0; + } + + return $shared_item['uri-id']; + } + /** * Processes retractions * @@ -2533,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']; @@ -2559,7 +2455,7 @@ class Diaspora $r = Post::select($fields, $condition); if (!DBA::isResult($r)) { - Logger::notice("Target guid " . $target_guid . " was not found on this system for user " . $importer['uid'] . "."); + Logger::notice('Target guid ' . $target_guid . ' was not found on this system for user ' . $importer['uid'] . '.'); return false; } @@ -2573,14 +2469,14 @@ 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; } Item::markForDeletion(['id' => $item['id']]); - Logger::info("Deleted target " . $target_guid . " (" . $item['id'] . ") from user " . $item['uid'] . " parent: " . $item['parent']); + Logger::info('Deleted target ' . $target_guid . ' (' . $item['id'] . ') from user ' . $item['uid'] . ' parent: ' . $item['parent']); } DBA::close($r); @@ -2590,20 +2486,20 @@ 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); $contact = self::contactByHandle($importer['uid'], $sender); if (!$contact && (in_array($target_type, ['Contact', 'Person']))) { - Logger::notice("cannot find contact for sender: " . $sender . " and user " . $importer['uid']); + Logger::notice('Cannot find contact for sender: ' . $sender . ' and user ' . $importer['uid']); return false; } @@ -2611,7 +2507,7 @@ class Diaspora $contact = []; } - Logger::info("Got retraction for " . $target_type . ", sender " . $sender . " and user " . $importer['uid']); + Logger::info('Got retraction for ' . $target_type . ', sender ' . $sender . ' and user ' . $importer['uid']); switch ($target_type) { case 'Comment': @@ -2627,7 +2523,7 @@ class Diaspora break; default: - Logger::notice("Unknown target type " . $target_type); + Logger::notice('Unknown target type ' . $target_type); return false; } return true; @@ -2670,23 +2566,48 @@ 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); } + /** + * Set direction and post reason + * + * @param array $datarray + * @param integer $direction + * + * @return array + */ + public static function setDirection(array $datarray, int $direction): array + { + $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; + + if (in_array($direction, [self::FETCHED, self::FORCED_FETCH])) { + $datarray['post-reason'] = Item::PR_FETCHED; + } elseif ($datarray['uid'] == 0) { + $datarray['post-reason'] = Item::PR_GLOBAL; + } else { + $datarray['post-reason'] = Item::PR_PUSHED; + } + + return $datarray; + } + /** * Receives status messages * @@ -2701,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; } @@ -2731,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) { @@ -2746,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 @@ -2762,32 +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'] = GRAVITY_PARENT; - - $datarray['protocol'] = Conversation::PARCEL_DIASPORA; - $datarray['source'] = $xml; - $datarray['direction'] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; - - if (in_array($direction, [self::FETCHED, self::FORCED_FETCH])) { - $datarray['post-reason'] = Item::PR_FETCHED; - } elseif ($datarray['uid'] == 0) { - $datarray['post-reason'] = Item::PR_GLOBAL; - } - - $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']); @@ -2800,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'])) { @@ -2809,7 +2719,7 @@ class Diaspora } if (isset($address['lat']) && isset($address['lng'])) { - $datarray['coord'] = $address['lat'] . " " . $address['lng']; + $datarray['coord'] = $address['lat'] . ' ' . $address['lng']; } self::fetchGuid($datarray); @@ -2824,7 +2734,7 @@ class Diaspora self::sendParticipation($contact, $datarray); if ($message_id) { - Logger::info("Stored item " . $datarray['guid'] . " with message id " . $message_id); + Logger::info('Stored item ' . $datarray['guid'] . ' with message id ' . $message_id); if ($datarray['uid'] == 0) { Item::distribute($message_id); } @@ -2839,7 +2749,7 @@ class Diaspora * ************************************************************************************** */ /** - * returnes the handle of a contact + * returns the handle of a contact * * @param array $contact contact array * @@ -2853,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 { @@ -2878,11 +2788,11 @@ class Diaspora */ public static function encodePrivateData(string $msg, array $user, array $contact, string $prvkey, string $pubkey): string { - Logger::debug("Message: ".$msg); + Logger::debug('Message: ' . $msg); // without a public key nothing will work if (!$pubkey) { - Logger::notice("pubkey missing: contact id: ".$contact["id"]); + Logger::notice('pubkey missing: contact id: ' . $contact['id']); return false; } @@ -2922,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'; @@ -2940,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' => 'http://salmon-protocol.org/ns/magic-env']; + $namespaces = ['me' => ActivityNamespace::SALMON_ME]; - return XML::fromArray($xmldata, $xml, false, $namespaces); + return XML::fromArray($xmldata, $dummy, false, $namespaces); } /** @@ -3021,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)) { @@ -3035,11 +2944,11 @@ class Diaspora } if (!$dest_url) { - Logger::notice("no url for contact: " . $contact['id'] . " batch mode =" . $public_batch); + Logger::notice('No URL for contact: ' . $contact['id'] . ' batch mode =' . $public_batch); 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'); @@ -3051,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; } @@ -3064,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]); } /** @@ -3096,22 +3010,22 @@ 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)]); } - $envelope = self::buildMessage($msg, $owner, $contact, $owner['uprvkey'], $pubkey, $public_batch); + $envelope = self::buildMessage($msg, $owner, $contact, $owner['uprvkey'], $pubkey ?? '', $public_batch); $return_code = self::transmit($owner, $contact, $envelope, $public_batch, $guid); @@ -3148,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); @@ -3189,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 = [ @@ -3261,10 +3177,10 @@ class Diaspora public static function sendUnshare(array $owner, array $contact): int { $message = [ - 'author' => self::myHandle($owner), + 'author' => self::myHandle($owner), 'recipient' => $contact['addr'], 'following' => 'false', - 'sharing' => 'false' + 'sharing' => 'false' ]; Logger::info('Send unshare', ['msg' => $message]); @@ -3273,61 +3189,30 @@ class Diaspora } /** - * Checks a message body if it is a reshare + * Fetch reshare details * - * @param string $body The message body that is to be check - * @param bool $complete Should it be a complete check or a simple check? + * @param array $item The message body that is to be check * - * @return array|bool Reshare details or "false" if no reshare + * @return array Reshare details (empty if the item is no reshare) * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isReshare(string $body, bool $complete = true) + public static function getReshareDetails(array $item): array { - $body = trim($body); - - $reshared = Item::getShareArray(['body' => $body]); + $reshared = DI::contentItem()->getSharedPost($item, ['guid', 'network', 'author-addr']); if (empty($reshared)) { - return false; - } - - // Skip if it isn't a pure repeated messages - // Does it start with a share? - if (!empty($reshared['comment']) && $complete) { - return false; - } - - if (!empty($reshared['guid']) && $complete) { - $condition = ['guid' => $reshared['guid'], 'network' => [Protocol::DFRN, Protocol::DIASPORA]]; - $item = Post::selectFirst(['contact-id'], $condition); - if (DBA::isResult($item)) { - $ret = []; - $ret['root_handle'] = self::handleFromContact($item['contact-id']); - $ret['root_guid'] = $reshared['guid']; - return $ret; - } elseif ($complete) { - // We are resharing something that isn't a DFRN or Diaspora post. - // So we have to return "false" on "$complete" to not trigger a reshare. - return false; - } - } elseif (empty($reshared['guid']) && $complete) { - return false; - } - - $ret = []; - - if (!empty($reshared['profile']) && ($cid = Contact::getIdForURL($reshared['profile']))) { - $contact = DBA::selectFirst('contact', ['addr'], ['id' => $cid]); - if (!empty($contact['addr'])) { - $ret['root_handle'] = $contact['addr']; - } + return []; } - if (empty($ret) && !$complete) { - return true; + // Skip if it isn't a pure repeated messages or not a real reshare + if (!empty($reshared['comment']) || !in_array($reshared['post']['network'], [Protocol::DFRN, Protocol::DIASPORA])) { + return []; } - return $ret; + return [ + 'root_handle' => strtolower($reshared['post']['author-addr']), + 'root_guid' => $reshared['post']['guid'], + ]; } /** @@ -3424,35 +3309,39 @@ class Diaspora $edited = DateTimeFormat::utc($item['edited'] ?? $item['created'], DateTimeFormat::ATOM); // Detect a share element and do a reshare - if (($item['private'] != Item::PRIVATE) && ($ret = self::isReshare($item['body']))) { + if (($item['private'] != Item::PRIVATE) && ($ret = self::getReshareDetails($item))) { $message = [ - 'author' => $myaddr, - 'guid' => $item['guid'], - 'created_at' => $created, - 'root_author' => $ret['root_handle'], - 'root_guid' => $ret['root_guid'], + 'author' => $myaddr, + 'guid' => $item['guid'], + 'created_at' => $created, + 'root_author' => $ret['root_handle'], + 'root_guid' => $ret['root_guid'], 'provider_display_name' => $item['app'], - 'public' => $public + 'public' => $public ]; $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'], $item['body']); + $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']; } } - if ($item['author-link'] != $item['owner-link']) { - $body = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], - $item['plink'], $item['created']) . $body . '[/share]'; - } - // convert to markdown $body = html_entity_decode(BBCode::toMarkdown($body)); @@ -3461,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) { @@ -3491,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']); @@ -3501,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']; } @@ -3516,7 +3411,7 @@ class Diaspora } $msg = [ - 'type' => $type, + 'type' => $type, 'message' => $message ]; @@ -3525,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) ) { @@ -3582,12 +3516,12 @@ class Diaspora } return [ - 'author' => self::myHandle($owner), - 'guid' => $item['guid'], - 'parent_guid' => $parent['guid'], - 'parent_type' => $target_type, - 'positive' => $positive, - 'author_signature' => '' + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'parent_guid' => $parent['guid'], + 'parent_type' => $target_type, + 'positive' => $positive, + 'author_signature' => '', ]; } @@ -3618,7 +3552,7 @@ class Diaspora $attend_answer = 'tentative'; break; default: - Logger::notice('Unknown verb '.$item['verb'].' in item '.$item['guid']); + Logger::warning('Unknown verb ' . $item['verb'] . ' in item ' . $item['guid']); return false; } @@ -3660,7 +3594,8 @@ class Diaspora $thread_parent_item = Post::selectFirst(['guid', 'author-id', 'author-link', 'gravity'], ['uri' => $item['thr-parent'], 'uid' => $item['uid']]); } - $body = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']); + $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 @@ -3668,7 +3603,7 @@ class Diaspora // - Implicit mentions are enabled if ( $item['author-id'] != $thread_parent_item['author-id'] - && ($thread_parent_item['gravity'] != GRAVITY_PARENT) + && ($thread_parent_item['gravity'] != Item::GRAVITY_PARENT) && (empty($item['uid']) || !Feature::isEnabled($item['uid'], 'explicit_mentions')) && !DI::config()->get('system', 'disable_implicit_mentions') ) { @@ -3686,7 +3621,7 @@ class Diaspora 'edited_at' => $edited, 'parent_guid' => $toplevel_item['guid'], 'text' => $text, - 'author_signature' => '' + 'author_signature' => '', ]; // Send the thread parent guid only if it is a threaded comment @@ -3754,9 +3689,9 @@ class Diaspora $type = 'comment'; } - Logger::info("Got relayable data " . $type . " for item " . $item['guid'] . " (" . $item['id'] . ")"); + Logger::info('Got relayable data ' . $type . ' for item ' . $item['guid'] . ' (' . $item['id'] . ')'); - $msg = json_decode($item['signed_text'], true); + $msg = json_decode($item['signed_text'] ?? '', true); $message = []; if (is_array($msg)) { @@ -3773,7 +3708,7 @@ class Diaspora $message[$field] = $data; } } else { - Logger::info("Signature text for item " . $item["guid"] . " (" . $item["id"] . ") couldn't be extracted: " . $item['signed_text']); + Logger::info('Signature text for item ' . $item['guid'] . ' (' . $item['id'] . ') could not be extracted: ' . $item['signed_text']); } $message['parent_author_signature'] = self::signature($owner, $message); @@ -3797,11 +3732,11 @@ class Diaspora */ public static function sendRetraction(array $item, array $owner, array $contact, bool $public_batch = false, bool $relay = false): int { - $itemaddr = self::handleFromContact($item['contact-id'], $item['author-id']); + $itemaddr = strtolower($item['author-addr']); $msg_type = 'retraction'; - if ($item['gravity'] == GRAVITY_PARENT) { + if ($item['gravity'] == Item::GRAVITY_PARENT) { $target_type = 'Post'; } elseif (in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { $target_type = 'Like'; @@ -3837,7 +3772,7 @@ class Diaspora $cnv = DBA::selectFirst('conv', [], ['id' => $item['convid'], 'uid' => $item['uid']]); if (!DBA::isResult($cnv)) { - Logger::notice("conversation not found."); + Logger::notice('Conversation not found.'); return -1; } @@ -3939,104 +3874,100 @@ class Diaspora private static function createProfileData(int $uid): array { $profile = DBA::selectFirst('owner-view', ['uid', 'addr', 'name', 'location', 'net-publish', 'dob', 'about', 'pub_keywords'], ['uid' => $uid]); + if (!DBA::isResult($profile)) { return []; } - $handle = $profile['addr']; - $split_name = self::splitName($profile['name']); - $first = $split_name['first']; - $last = $split_name['last']; - $large = DI::baseUrl().'/photo/custom/300/'.$profile['uid'].'.jpg'; - $medium = DI::baseUrl().'/photo/custom/100/'.$profile['uid'].'.jpg'; - $small = DI::baseUrl().'/photo/custom/50/' .$profile['uid'].'.jpg'; - $searchable = ($profile['net-publish'] ? 'true' : 'false'); + $data = [ + 'author' => $profile['addr'], + 'first_name' => $split_name['first'], + 'last_name' => $split_name['last'], + 'image_url' => DI::baseUrl() . '/photo/custom/300/' . $profile['uid'] . '.jpg', + 'image_url_medium' => DI::baseUrl() . '/photo/custom/100/' . $profile['uid'] . '.jpg', + 'image_url_small' => DI::baseUrl() . '/photo/custom/50/' . $profile['uid'] . '.jpg', + 'searchable' => ($profile['net-publish'] ? 'true' : 'false'), + 'birthday' => null, + 'about' => null, + 'location' => null, + 'tag_string' => null, + 'nsfw' => 'false', + ]; - $dob = null; - $about = null; - $location = null; - $tags = null; - if ($searchable === 'true') { - $dob = ''; + if ($data['searchable'] === 'true') { + $data['birthday'] = ''; if ($profile['dob'] && ($profile['dob'] > '0000-00-00')) { [$year, $month, $day] = sscanf($profile['dob'], '%4d-%2d-%2d'); if ($year < 1004) { $year = 1004; } - $dob = DateTimeFormat::utc($year . '-' . $month . '-'. $day, 'Y-m-d'); + $data['birthday'] = DateTimeFormat::utc($year . '-' . $month . '-' . $day, 'Y-m-d'); } - $about = BBCode::toMarkdown($profile['about']); + $data['about'] = BBCode::toMarkdown($profile['about'] ?? ''); + + $data['location'] = $profile['location']; + $data['tag_string'] = ''; - $location = $profile['location']; - $tags = ''; if ($profile['pub_keywords']) { $kw = str_replace(',', ' ', $profile['pub_keywords']); $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])) { - $tags .= '#'. trim($arr[$x]) .' '; + $data['tag_string'] .= '#' . trim($arr[$x]) . ' '; } } } } - $tags = trim($tags); + $data['tag_string'] = trim($data['tag_string']); } - return [ - 'author' => $handle, - 'first_name' => $first, - 'last_name' => $last, - 'image_url' => $large, - 'image_url_medium' => $medium, - 'image_url_small' => $small, - 'birthday' => $dob, - 'bio' => $about, - 'location' => $location, - 'searchable' => $searchable, - 'nsfw' => 'false', - 'tag_string' => $tags - ]; + return $data; } /** * Sends profile data * - * @param int $uid The user id - * @param array $recips optional, default empty array + * @param int $uid The user id + * @param array $recipients optional, default empty array + * * @return void * @throws \Exception */ - public static function sendProfile(int $uid, array $recips = []) + public static function sendProfile(int $uid, array $recipients = []) { if (!$uid) { + Logger::warning('Parameter "uid" is empty'); return; } $owner = User::getOwnerDataById($uid); - if (!$owner) { + if (empty($owner)) { + Logger::warning('Cannot fetch User record', ['uid' => $uid]); return; } - if (!$recips) { - $recips = DBA::selectToArray('contact', [], ['network' => Protocol::DIASPORA, 'uid' => $uid, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]); + if (empty($recipients)) { + Logger::debug('No recipients provided, fetching for user', ['uid' => $uid]); + $recipients = DBA::selectToArray('contact', [], ['network' => Protocol::DIASPORA, 'uid' => $uid, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]); } - if (!$recips) { + if (empty($recipients)) { + Logger::warning('Cannot fetch recipients', ['uid' => $uid]); return; } $message = self::createProfileData($uid); - // @ToDo Split this into single worker jobs - foreach ($recips as $recip) { - Logger::info("Send updated profile data for user " . $uid . " to contact " . $recip['id']); - self::buildAndTransmit($owner, $recip, 'profile', $message); + // @todo Split this into single worker jobs + foreach ($recipients as $recipient) { + Logger::info('Send updated profile data for user ' . $uid . ' to contact ' . $recipient['id']); + self::buildAndTransmit($owner, $recipient, 'profile', $message); } } @@ -4053,11 +3984,12 @@ class Diaspora { $owner = User::getOwnerDataById($uid); if (empty($owner)) { - Logger::info('No owner post, so not storing signature'); + Logger::info('No owner post, so not storing signature', ['uid' => $uid]); return false; } if (!in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { + Logger::warning('Item is neither a like nor a dislike', ['uid' => $uid, 'item[verb]' => $item['verb']]);; return false; } @@ -4081,6 +4013,7 @@ class Diaspora */ public static function createCommentSignature(array $item) { + $contact = []; if (!empty($item['author-link'])) { $url = $item['author-link']; } else { @@ -4094,7 +4027,7 @@ class Diaspora $uid = User::getIdForURL($url); if (empty($uid)) { - Logger::info('No owner post, so not storing signature', ['url' => $contact['url']]); + Logger::info('No owner post, so not storing signature', ['url' => $contact['url'] ?? 'No contact loaded']); return false; } @@ -4109,6 +4042,11 @@ class Diaspora return false; } + 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; + } + $message = self::constructComment($item, $owner); if ($message === false) { return false; @@ -4119,42 +4057,57 @@ class Diaspora return $message; } - public static function performReshare(int $UriId, int $uid): int + /** + * 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, int $uid): bool { - $fields = ['uri-id', 'body', 'title', 'author-name', 'author-link', 'author-avatar', 'guid', 'created', 'plink']; - $item = Post::selectFirst($fields, ['uri-id' => $UriId, 'uid' => [$uid, 0], 'private' => [Item::PUBLIC, Item::UNLISTED]]); - if (!DBA::isResult($item)) { - return 0; + $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 (strpos($item['body'], '[/share]') !== false) { - $pos = strpos($item['body'], '[share'); - $post = substr($item['body'], $pos); - } else { - $post = BBCode::getShareOpeningTag($item['author-name'], $item['author-link'], $item['author-avatar'], $item['plink'], $item['created'], $item['guid']); + if (!self::isSupportedByContactUrl($parent_post['author-link'])) { + Logger::info('Parent author is no Diaspora contact.', ['parent-id' => $parent_id]); + return false; + } - if (!empty($item['title'])) { - $post .= '[h3]' . $item['title'] . "[/h3]\n"; - } + 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; + } - $post .= $item['body']; - $post .= '[/share]'; + if ($parent_post['gravity'] == Item::GRAVITY_COMMENT) { + return self::parentSupportDiaspora($parent_post['thr-parent-id'], $uid); } + return true; + } + + public static function performReshare(int $UriId, int $uid): int + { $owner = User::getOwnerDataById($uid); $author = Contact::getPublicIdByUserId($uid); $item = [ - 'uid' => $uid, - 'verb' => Activity::POST, - 'contact-id' => $owner['id'], - 'author-id' => $author, - 'owner-id' => $author, - 'body' => $post, - 'allow_cid' => $owner['allow_cid'] ?? '', - 'allow_gid' => $owner['allow_gid']?? '', - 'deny_cid' => $owner['deny_cid'] ?? '', - 'deny_gid' => $owner['deny_gid'] ?? '', + 'uid' => $uid, + 'verb' => Activity::POST, + 'contact-id' => $owner['id'], + 'author-id' => $author, + 'owner-id' => $author, + 'body' => '', + 'quote-uri-id' => $UriId, + 'allow_cid' => $owner['allow_cid'] ?? '', + 'allow_gid' => $owner['allow_gid'] ?? '', + 'deny_cid' => $owner['deny_cid'] ?? '', + 'deny_gid' => $owner['deny_gid'] ?? '', ]; if (!empty($item['allow_cid'] . $item['allow_gid'] . $item['deny_cid'] . $item['deny_gid'])) {