X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FDiaspora.php;h=dd6c2115b5e32714a868bae0ee589465ca76bf31;hb=fdaff4303952427f222ee21f6b501d5087e25932;hp=154d65f6f985ba3aedfb282763b7a0642194bf35;hpb=db60557a4f1066639e1af0650f1e5f13e576bcb1;p=friendica.git diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 154d65f6f9..dd6c2115b5 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -1,6 +1,6 @@ $item['guid'], 'private' => $item['private'], 'verb' => $item['verb']]); return $contacts; } - $items = Post::select(['author-id', 'author-link', 'parent-author-link', 'parent-guid', 'guid'], - ['parent' => $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; } @@ -114,27 +123,27 @@ class Diaspora * * @param string $envelope The magic envelope * - * @return string verified data + * @return string|bool verified data or false on error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function verifyMagicEnvelope($envelope) + private static function verifyMagicEnvelope(string $envelope) { $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]; @@ -145,22 +154,26 @@ class Diaspora $sig = Strings::base64UrlDecode($children->sig); $key_id = $children->sig->attributes()->key_id[0]; - if ($key_id != "") { + if ($key_id != '') { $handle = Strings::base64UrlDecode($key_id); } $b64url_data = Strings::base64UrlEncode($data); - $msg = str_replace(["\n", "\r", " ", "\t"], ["", "", "", ""], $b64url_data); + $msg = str_replace(["\n", "\r", " ", "\t"], ['', '', '', ''], $b64url_data); - $signable_data = $msg.".".Strings::base64UrlEncode($type).".".Strings::base64UrlEncode($encoding).".".Strings::base64UrlEncode($alg); + $signable_data = $msg . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); if ($handle == '') { Logger::notice('No author could be decoded. Discarding. Message: ' . $envelope); 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; } @@ -183,7 +196,7 @@ class Diaspora * * @return string encrypted data */ - private static function aesEncrypt($key, $iv, $data) + private static function aesEncrypt(string $key, string $iv, string $data): string { return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0")); } @@ -197,19 +210,19 @@ class Diaspora * * @return string decrypted data */ - private static function aesDecrypt($key, $iv, $encrypted) + private static function aesDecrypt(string $key, string $iv, string $encrypted): string { return openssl_decrypt($encrypted, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0")); } /** - * Decodes incoming Diaspora message in the new format + * Decodes incoming Diaspora message in the new format. This method returns false on an error. * * @param string $raw raw post message * @param string $privKey The private key of the importer * @param boolean $no_exit Don't do an http exit on error * - * @return array + * @return bool|array * 'message' -> decoded Diaspora XML message * 'author' -> author diaspora handle * 'key' -> author public key (converted to pkcs#8) @@ -222,26 +235,41 @@ class Diaspora // Is it a private post? Then decrypt the outer Salmon if (is_object($data)) { - $encrypted_aes_key_bundle = base64_decode($data->aes_key); - $ciphertext = base64_decode($data->encrypted_magic_envelope); + try { + if (!isset($data->aes_key) || !isset($data->encrypted_magic_envelope)) { + Logger::info('Missing keys "aes_key" and/or "encrypted_magic_envelope"', ['data' => $data]); + throw new \RuntimeException('Missing keys "aes_key" and/or "encrypted_magic_envelope"'); + } - $outer_key_bundle = ''; - @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); - $j_outer_key_bundle = json_decode($outer_key_bundle); + $encrypted_aes_key_bundle = base64_decode($data->aes_key); + $ciphertext = base64_decode($data->encrypted_magic_envelope); + + $outer_key_bundle = ''; + @openssl_private_decrypt($encrypted_aes_key_bundle, $outer_key_bundle, $privKey); + $j_outer_key_bundle = json_decode($outer_key_bundle); + + if (!is_object($j_outer_key_bundle)) { + Logger::info('Unable to decode outer key bundle', ['outer_key_bundle' => $outer_key_bundle]); + throw new \RuntimeException('Unable to decode outer key bundle'); + } - if (!is_object($j_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; } @@ -249,24 +277,24 @@ 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(); } } $base = $basedom->children(ActivityNamespace::SALMON_ME); // Not sure if this cleaning is needed - $data = str_replace([" ", "\t", "\r", "\n"], ["", "", "", ""], $base->data); + $data = str_replace([" ", "\t", "\r", "\n"], ['', '', '', ''], $base->data); // Build the signed data $type = $base->data[0]->attributes()->type[0]; $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); // This is the signature $signature = Strings::base64UrlDecode($base->sig); @@ -279,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(); } } @@ -299,13 +332,15 @@ 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]; + return [ + 'message' => (string)Strings::base64UrlDecode($base->data), + 'author' => $author->getAddr(), + 'key' => (string)$key + ]; } /** @@ -337,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)) { @@ -365,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); @@ -384,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(); } @@ -394,7 +434,7 @@ class Diaspora // unpack the data // strip whitespace so our data element will return to one big base64 blob - $data = str_replace([" ", "\t", "\r", "\n"], ["", "", "", ""], $base->data); + $data = str_replace([" ", "\t", "\r", "\n"], ['', '', '', ''], $base->data); // stash away some other stuff for later @@ -404,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 { @@ -420,34 +457,30 @@ 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]; + return [ + 'message' => $inner_decrypted, + 'author' => $author->getAddr(), + 'key' => $key + ]; } @@ -457,24 +490,26 @@ class Diaspora * @param array $msg The post that will be dispatched * @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 message, "true" or "false" if there was an error + * @return int|bool The message id of the generated message, "true" or "false" if there was an error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function dispatchPublic($msg, int $direction) + 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; } - $importer = ["uid" => 0, "page-flags" => User::PAGE_FLAGS_FREELOVE]; + $importer = [ + 'uid' => 0, + 'page-flags' => User::PAGE_FLAGS_FREELOVE + ]; $success = self::dispatch($importer, $msg, $fields, $direction); return $success; @@ -488,21 +523,21 @@ class Diaspora * @param SimpleXMLElement $fields SimpleXML object that contains 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 message, "true" or "false" if there was an error + * @return int|bool The message id of the generated message, "true" or "false" if there was an error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function dispatch(array $importer, $msg, SimpleXMLElement $fields = null, int $direction = self::PUSHED) + public static function dispatch(array $importer, array $msg, SimpleXMLElement $fields = null, int $direction = self::PUSHED) { // 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 { @@ -511,77 +546,77 @@ 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": + case 'account_migration': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveAccountMigration($importer, $fields); - case "account_deletion": + case 'account_deletion': return self::receiveAccountDeletion($fields); - case "comment": - return self::receiveComment($importer, $sender, $fields, $msg["message"], $direction); + case 'comment': + return self::receiveComment($importer, $sender, $fields, $msg['message'], $direction); - case "contact": + case 'contact': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveContactRequest($importer, $fields); - case "conversation": + case 'conversation': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveConversation($importer, $msg, $fields); - case "like": + case 'like': return self::receiveLike($importer, $sender, $fields, $direction); - case "message": + case 'message': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveMessage($importer, $fields); - case "participation": + case 'participation': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveParticipation($importer, $fields, $direction); - case "photo": // Not implemented + case 'photo': // Not implemented return self::receivePhoto($importer, $fields); - case "poll_participation": // Not implemented + case 'poll_participation': // Not implemented return self::receivePollParticipation($importer, $fields); - case "profile": + case 'profile': if (!$private) { Logger::notice('Message with type ' . $type . ' is not private, quitting.'); return false; } return self::receiveProfile($importer, $fields); - case "reshare": - return self::receiveReshare($importer, $fields, $msg["message"], $direction); + case 'reshare': + return self::receiveReshare($importer, $fields, $msg['message'], $direction); - case "retraction": + case 'retraction': return self::receiveRetraction($importer, $sender, $fields); - case "status_message": - return self::receiveStatusMessage($importer, $fields, $msg["message"], $direction); + case 'status_message': + return self::receiveStatusMessage($importer, $fields, $msg['message'], $direction); default: - Logger::notice("Unknown message type ".$type); + Logger::notice('Unknown message type ' . $type); return false; } } @@ -598,9 +633,9 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function validPosting($msg) + private static function validPosting(array $msg) { - $data = XML::parseString($msg["message"]); + $data = XML::parseString($msg['message']); if (!is_object($data)) { Logger::info('No valid XML', ['message' => $msg['message']]); @@ -608,7 +643,7 @@ class Diaspora } // Is this the new or the old version? - if ($data->getName() == "XML") { + if ($data->getName() == 'XML') { $oldXML = true; foreach ($data->post->children() as $child) { $element = $child; @@ -621,118 +656,129 @@ 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". - if (in_array($type, ["signed_retraction", "relayable_retraction"])) - $type = "retraction"; + if (in_array($type, ['signed_retraction', 'relayable_retraction'])) + $type = 'retraction'; - if ($type == "request") { - $type = "contact"; + if ($type == 'request') { + $type = 'contact'; } - $fields = new SimpleXMLElement("<".$type."/>"); + $fields = new SimpleXMLElement('<' . $type . '/>'); - $signed_data = ""; + $signed_data = ''; $author_signature = null; $parent_author_signature = null; foreach ($element->children() as $fieldname => $entry) { if ($oldXML) { // Translation for the old XML structure - if ($fieldname == "diaspora_handle") { - $fieldname = "author"; + if ($fieldname == 'diaspora_handle') { + $fieldname = 'author'; } - if ($fieldname == "participant_handles") { - $fieldname = "participants"; + if ($fieldname == 'participant_handles') { + $fieldname = 'participants'; } - if (in_array($type, ["like", "participation"])) { - if ($fieldname == "target_type") { - $fieldname = "parent_type"; + if (in_array($type, ['like', 'participation'])) { + if ($fieldname == 'target_type') { + $fieldname = 'parent_type'; } } - if ($fieldname == "sender_handle") { - $fieldname = "author"; + if ($fieldname == 'sender_handle') { + $fieldname = 'author'; } - if ($fieldname == "recipient_handle") { - $fieldname = "recipient"; + if ($fieldname == 'recipient_handle') { + $fieldname = 'recipient'; } - if ($fieldname == "root_diaspora_id") { - $fieldname = "root_author"; + if ($fieldname == 'root_diaspora_id') { + $fieldname = 'root_author'; } - if ($type == "status_message") { - if ($fieldname == "raw_message") { - $fieldname = "text"; + if ($type == 'status_message') { + if ($fieldname == 'raw_message') { + $fieldname = 'text'; } } - if ($type == "retraction") { - if ($fieldname == "post_guid") { - $fieldname = "target_guid"; + if ($type == 'retraction') { + if ($fieldname == 'post_guid') { + $fieldname = 'target_guid'; } - if ($fieldname == "type") { - $fieldname = "target_type"; + if ($fieldname == 'type') { + $fieldname = 'target_type'; } } } - if (($fieldname == "author_signature") && ($entry != "")) { + if (($fieldname == 'author_signature') && ($entry != '')) { $author_signature = base64_decode($entry); - } elseif (($fieldname == "parent_author_signature") && ($entry != "")) { + } elseif (($fieldname == 'parent_author_signature') && ($entry != '')) { $parent_author_signature = base64_decode($entry); - } elseif (!in_array($fieldname, ["author_signature", "parent_author_signature", "target_author_signature"])) { - if ($signed_data != "") { - $signed_data .= ";"; + } elseif (!in_array($fieldname, ['author_signature', 'parent_author_signature', 'target_author_signature'])) { + if ($signed_data != '') { + $signed_data .= ';'; } $signed_data .= $entry; } - if (!in_array($fieldname, ["parent_author_signature", "target_author_signature"]) - || ($orig_type == "relayable_retraction") + if ( + !in_array($fieldname, ['parent_author_signature', 'target_author_signature']) + || ($orig_type == 'relayable_retraction') ) { XML::copy($entry, $fields, $fieldname); } } // This is something that shouldn't happen at all. - if (in_array($type, ["status_message", "reshare", "profile"])) { - if ($msg["author"] != $fields->author) { - Logger::notice("Message handle is not the same as envelope sender. Quitting this message."); + if (in_array($type, ['status_message', 'reshare', 'profile'])) { + if ($msg['author'] != $fields->author) { + Logger::notice('Message handle is not the same as envelope sender. Quitting this message.', ['author1' => $msg['author'], 'author2' => $fields->author]); return false; } } // Only some message types have signatures. So we quit here for the other types. - if (!in_array($type, ["comment", "like"])) { + 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"]]); + 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); + if (!Crypto::rsaVerify($signed_data, $parent_author_signature, $key, 'sha256')) { + 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); + if (!Crypto::rsaVerify($signed_data, $author_signature, $key, 'sha256')) { + 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; @@ -742,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($handle) + private static function key(WebFingerUri $uri): string { - $handle = strval($handle); - - Logger::notice("Fetching diaspora key for: ".$handle); - - $r = FContact::getByURL($handle); - if ($r) { - return $r["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($contact_id, $pcontact_id = 0) - { - $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($uid, $handle) + 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($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; } /** @@ -832,7 +851,7 @@ class Diaspora * * @return bool is the contact allowed to post? */ - private static function postAllow(array $importer, array $contact, $is_comment = false) + private static function postAllow(array $importer, array $contact, bool $is_comment = false): bool { /* * Perhaps we were already sharing with this person. Now they're sharing with us. @@ -855,15 +874,15 @@ class Diaspora if (Network::isUrlBlocked($contact['url'])) { return false; // We don't seem to like that person - } elseif ($contact["blocked"]) { + } elseif ($contact['blocked']) { // Maybe blocked, don't accept. return false; // We are following this person? - } elseif (($contact["rel"] == Contact::SHARING) || ($contact["rel"] == Contact::FRIEND)) { + } elseif (($contact['rel'] == Contact::SHARING) || ($contact['rel'] == Contact::FRIEND)) { // Yes, then it is fine. return true; // Is the message a global user or a comment? - } elseif (($importer["uid"] == 0) || $is_comment) { + } elseif (($importer['uid'] == 0) || $is_comment) { // Messages for the global users and comments are always accepted return true; } @@ -874,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 The contact data - * @throws \Exception + * @return array|bool The contact data or false on error + * @throws InternalServerErrorException + * @throws \ImagickException */ - private static function allowedContactByHandle(array $importer, $handle, $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); + if ($is_comment && ($importer['uid'] != 0)) { + return self::contactByHandle(0, $contact_uri); } elseif ($is_comment) { return $importer; } else { @@ -897,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; @@ -912,12 +932,12 @@ class Diaspora * @return int|bool message id if the message already was stored into the system - or false. * @throws \Exception */ - private static function messageExists($uid, $guid) + private static function messageExists(int $uid, string $guid) { $item = Post::selectFirst(['id'], ['uid' => $uid, 'guid' => $guid]); if (DBA::isResult($item)) { - Logger::notice("message ".$guid." already exists for user ".$uid); - return $item["id"]; + Logger::notice('Message already exists.', ['uid' => $uid, 'guid' => $guid, 'id' => $item['id']]); + return $item['id']; } return false; @@ -927,17 +947,17 @@ class Diaspora * Checks for links to posts in a message * * @param array $item The item array + * * @return void */ private static function fetchGuid(array $item) { - $expression = "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism"; preg_replace_callback( - $expression, + "=diaspora://.*?/post/([0-9A-Za-z\-_@.:]{15,254}[0-9A-Za-z])=ism", function ($match) use ($item) { self::fetchGuidSub($match, $item); }, - $item["body"] + $item['body'] ); preg_replace_callback( @@ -945,7 +965,7 @@ class Diaspora function ($match) use ($item) { self::fetchGuidSub($match, $item); }, - $item["body"] + $item['body'] ); } @@ -958,7 +978,7 @@ class Diaspora * * @return string the replaced string */ - public static function replacePeopleGuid($body, $author_link) + public static function replacePeopleGuid(string $body, string $author_link): string { $return = preg_replace_callback( "&\[url=/people/([^\[\]]*)\](.*)\[\/url\]&Usi", @@ -967,14 +987,14 @@ 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]'; + $return = '@[url=' . $handle . ']' . $match[2] . '[/url]'; } else { // No local match, restoring absolute remote URL from author scheme and host $author_url = parse_url($author_link); - $return = '[url='.$author_url['scheme'].'://'.$author_url['host'].'/people/'.$match[1].']'.$match[2].'[/url]'; + $return = '[url=' . $author_url['scheme'] . '://' . $author_url['host'] . '/people/' . $match[1] . ']' . $match[2] . '[/url]'; } return $return; @@ -994,10 +1014,10 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function fetchGuidSub($match, $item) + private static function fetchGuidSub(array $match, array $item) { - if (!self::storeByGuid($match[1], $item["author-link"], true)) { - self::storeByGuid($match[1], $item["owner-link"], true); + if (!self::storeByGuid($match[1], $item['author-link'], true)) { + self::storeByGuid($match[1], $item['owner-link'], true); } } @@ -1008,21 +1028,21 @@ class Diaspora * @param string $server The server address * @param bool $force Forced fetch * - * @return int the message id of the stored message or false + * @return int|bool the message id of the stored message or false * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function storeByGuid($guid, $server, $force) + private static function storeByGuid(string $guid, string $server, bool $force) { $serverparts = parse_url($server); - if (empty($serverparts["host"]) || empty($serverparts["scheme"])) { + if (empty($serverparts['host']) || empty($serverparts['scheme'])) { return false; } - $server = $serverparts["scheme"]."://".$serverparts["host"]; + $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); @@ -1030,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); @@ -1049,25 +1069,25 @@ class Diaspora * 'key' => The public key of the author * @throws \Exception */ - public static function message($guid, $server, $level = 0) + public static function message(string $guid, string $server, int $level = 0) { if ($level > 5) { return false; } // This will work for new Diaspora servers and Friendica servers from 3.5 - $source_url = $server."/fetch/post/".urlencode($guid); + $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, 0, HttpClient::ACCEPT_MAGIC); + $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; @@ -1085,50 +1105,53 @@ 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; - } elseif ($source_xml->author && ($source_xml->getName() == "status_message")) { - $author = (string)$source_xml->author; + $author_handle = (string)$source_xml->post->status_message->diaspora_handle; + } elseif ($source_xml->author && ($source_xml->getName() == 'status_message')) { + $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; } - $msg = ["message" => $x, "author" => $author]; - - $msg["key"] = self::key($msg["author"]); - - return $msg; + return [ + 'message' => $x, + 'author' => $author->getAddr(), + 'key' => self::key($author) + ]; } /** * Fetches an item with a given URL * * @param string $url the message url + * @param int $uid User id * - * @return int the message id of the stored message or false + * @return int|bool the message id of the stored message or false * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function fetchByURL($url, $uid = 0) + public static function fetchByURL(string $url, int $uid = 0) { // 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]); + if (!preg_match('=(https?://.+)/(?:posts|display|objects)/([a-zA-Z0-9-_@.:%]+[a-zA-Z0-9])=i', $url, $matches)) { + Logger::notice('Invalid url', ['url' => $url]); return false; } @@ -1149,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; } } @@ -1157,72 +1180,81 @@ 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 the item record + * @return array|bool the item record or false on failure * @throws \Exception */ - private static function parentItem($uid, $guid, $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($def_contact, $person, $uid) + 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"]; - $network = $contact["network"]; + $cid = $contact['id']; + $network = $contact['network']; } else { - $cid = $def_contact["id"]; + $cid = $def_contact['id']; $network = Protocol::DIASPORA; } - return ["cid" => $cid, "network" => $network]; + return [ + 'cid' => $cid, + 'network' => $network + ]; } /** @@ -1232,9 +1264,9 @@ class Diaspora * * @return bool is it a hubzilla server? */ - private static function isHubzilla($url) + private static function isHubzilla(string $url): bool { - return(strstr($url, '/channel/')); + return strstr($url, '/channel/'); } /** @@ -1248,7 +1280,7 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function plink(string $addr, string $guid, string $parent_guid = '') + private static function plink(string $addr, string $guid, string $parent_guid = ''): string { $contact = Contact::getByURL($addr); if (empty($contact)) { @@ -1306,30 +1338,36 @@ class Diaspora * Receives account migration * * @param array $importer Array of the importer user - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveAccountMigration(array $importer, $data) + 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); - if (!Crypto::rsaVerify($signed_text, $signature, $key, "sha256")) { + $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; } @@ -1338,21 +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']]; + $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'], + ]; - 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; } @@ -1360,22 +1404,22 @@ class Diaspora /** * Processes an account deletion * - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function receiveAccountDeletion($data) + 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"]); + Contact::remove($contact['id']); } DBA::close($contacts); - Logger::notice('Removed contacts for ' . $author); + Logger::info('Removed contacts for ' . $author_handle); return true; } @@ -1383,30 +1427,27 @@ 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 + * @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($author, $guid, $onlyfound = false) + private static function getUriFromGuid(string $guid, WebFingerUri $person_uri = null): string { $item = Post::selectFirst(['uri'], ['guid' => $guid]); - if (DBA::isResult($item)) { - return $item["uri"]; - } elseif (!$onlyfound) { - $person = FContact::getByURL($author); - - $parts = parse_url($person['url']); - unset($parts['path']); - $host_url = Network::unparseURL($parts); - - return $host_url . '/objects/' . $guid; + if ($item) { + return $item['uri']; + } elseif ($person_uri) { + try { + return DI::dsprContact()->selectOneByAddr($person_uri)->baseurl . '/objects/' . $guid; + } catch (HTTPException\NotFoundException | \InvalidArgumentException $e) { + return ''; + } } - return ""; + return ''; } /** @@ -1434,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 object $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 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) * - * @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, $sender, $data, $xml, int $direction) + 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); @@ -1471,9 +1511,9 @@ 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 = ""; + $thr_parent = ''; } $contact = self::allowedContactByHandle($importer, $sender, true); @@ -1485,77 +1525,73 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $message_id = self::messageExists($importer["uid"], $guid); + $message_id = self::messageExists($importer['uid'], $guid); if ($message_id) { return true; } - $toplevel_parent_item = self::parentItem($importer["uid"], $parent_guid, $author, $contact); + $toplevel_parent_item = self::parentItem($importer['uid'], $parent_guid, $author, $contact); if (!$toplevel_parent_item) { 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 = []; - $datarray["uid"] = $importer["uid"]; - $datarray["contact-id"] = $author_contact["cid"]; - $datarray["network"] = $author_contact["network"]; + $datarray['uid'] = $importer['uid']; + $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-link'] = $contact['url']; + $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['guid'] = $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['verb'] = Activity::POST; + $datarray['gravity'] = Item::GRAVITY_COMMENT; $datarray['thr-parent'] = $thr_parent ?: $toplevel_parent_item['uri']; - $datarray["object-type"] = Activity\ObjectType::COMMENT; - $datarray["post-type"] = Item::PT_NOTE; + $datarray['object-type'] = Activity\ObjectType::COMMENT; + $datarray['post-type'] = Item::PT_NOTE; - $datarray["protocol"] = Conversation::PARCEL_DIASPORA; - $datarray["source"] = $xml; - $datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; + $datarray['protocol'] = Conversation::PARCEL_DIASPORA; + $datarray['source'] = $xml; - $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; + $datarray = self::setDirection($datarray, $direction); - $datarray["plink"] = self::plink($author, $guid, $toplevel_parent_item['guid']); + $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"]); + Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray['body']); self::fetchGuid($datarray); // If we are the origin of the parent we store the original data. // We notify our followers during the item storage. - if ($toplevel_parent_item["origin"]) { + if ($toplevel_parent_item['origin']) { $datarray['diaspora_signed_text'] = json_encode($data); } @@ -1571,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)); } @@ -1585,57 +1621,61 @@ class Diaspora * * @param array $importer Array of the importer user * @param array $contact The contact of the message - * @param object $data The message object + * @param SimpleXMLElement $data The message object * @param array $msg Array of the processed message, author handle and key * @param object $mesg The private message * @param array $conversation The conversation record to which this message belongs * * @return bool "true" if it was successful * @throws \Exception + * @todo Find type-hint for $mesg and update documentation */ - private static function receiveConversationMessage(array $importer, array $contact, $data, $msg, $mesg, $conversation) + 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 ]); } @@ -1645,14 +1685,14 @@ class Diaspora * * @param array $importer Array of the importer user * @param array $msg Array of the processed message, author handle and key - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Exception */ - private static function receiveConversation(array $importer, $msg, $data) + 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)); @@ -1661,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; } @@ -1674,22 +1714,24 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $conversation = DBA::selectFirst('conv', [], ['uid' => $importer["uid"], 'guid' => $guid]); + $conversation = DBA::selectFirst('conv', [], ['uid' => $importer['uid'], 'guid' => $guid]); if (!DBA::isResult($conversation)) { $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]); + $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; } @@ -1703,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 object $data The message object - * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH) + * @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) * - * @return int The message id of the generated like or "false" if there was an error + * @return bool Success or failure * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveLike(array $importer, $sender, $data, int $direction) + 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); @@ -1722,7 +1764,7 @@ class Diaspora // likes on comments aren't supported by Diaspora - only on posts // But maybe this will be supported in the future, so we will accept it. - if (!in_array($parent_type, ["Post", "Comment"])) { + if (!in_array($parent_type, ['Post', 'Comment'])) { return false; } @@ -1735,28 +1777,29 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $message_id = self::messageExists($importer["uid"], $guid); + $message_id = self::messageExists($importer['uid'], $guid); if ($message_id) { return true; } - $toplevel_parent_item = self::parentItem($importer["uid"], $parent_guid, $author, $contact); + $toplevel_parent_item = self::parentItem($importer['uid'], $parent_guid, $author, $contact); if (!$toplevel_parent_item) { 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. - if ($positive == "true") { + if ($positive == 'true') { $verb = Activity::LIKE; } else { $verb = Activity::DISLIKE; @@ -1764,36 +1807,37 @@ class Diaspora $datarray = []; - $datarray["protocol"] = Conversation::PARCEL_DIASPORA; - $datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; + $datarray['protocol'] = Conversation::PARCEL_DIASPORA; + + $datarray['uid'] = $importer['uid']; + $datarray['contact-id'] = $author_contact['cid']; + $datarray['network'] = $author_contact['network']; - $datarray["uid"] = $importer["uid"]; - $datarray["contact-id"] = $author_contact["cid"]; - $datarray["network"] = $author_contact["network"]; + $datarray = self::setDirection($datarray, $direction); - $datarray["owner-link"] = $datarray["author-link"] = $person["url"]; - $datarray["owner-id"] = $datarray["author-id"] = Contact::getIdForURL($person["url"], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); - $datarray["guid"] = $guid; - $datarray["uri"] = self::getUriFromGuid($author, $guid); + $datarray['guid'] = $guid; + $datarray['uri'] = self::getUriFromGuid($guid, $author); - $datarray["verb"] = $verb; - $datarray["gravity"] = GRAVITY_ACTIVITY; + $datarray['verb'] = $verb; + $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['thr-parent'] = $toplevel_parent_item['uri']; - $datarray["object-type"] = Activity\ObjectType::NOTE; + $datarray['object-type'] = Activity\ObjectType::NOTE; - $datarray["body"] = $verb; + $datarray['body'] = $verb; // Diaspora doesn't provide a date for likes - $datarray["changed"] = $datarray["created"] = $datarray["edited"] = DateTimeFormat::utcNow(); + $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"]; + $origin = $toplevel['origin']; } else { - $origin = $toplevel_parent_item["origin"]; + $origin = $toplevel_parent_item['origin']; } // If we are the origin of the parent we store the original data. @@ -1814,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)); } @@ -1827,20 +1871,20 @@ class Diaspora * Processes private messages * * @param array $importer Array of the importer user - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success? * @throws \Exception */ - private static function receiveMessage(array $importer, $data) + 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; } @@ -1849,41 +1893,37 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $conversation = null; - - $condition = ['uid' => $importer["uid"], 'guid' => $conversation_guid]; + $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 ]); } @@ -1892,16 +1932,16 @@ class Diaspora * Processes participations - unsupported by now * * @param array $importer Array of the importer user - * @param object $data The message object + * @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) * * @return bool success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveParticipation(array $importer, $data, int $direction) + 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); @@ -1914,11 +1954,11 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - if (self::messageExists($importer["uid"], $guid)) { + if (self::messageExists($importer['uid'], $guid)) { return true; } - $toplevel_parent_item = self::parentItem($importer["uid"], $parent_guid, $author, $contact); + $toplevel_parent_item = self::parentItem($importer['uid'], $parent_guid, $author, $contact); if (!$toplevel_parent_item) { return false; } @@ -1932,40 +1972,42 @@ 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['protocol'] = Conversation::PARCEL_DIASPORA; + + $datarray['uid'] = $importer['uid']; + $datarray['contact-id'] = $author_contact['cid']; + $datarray['network'] = $author_contact['network']; - $datarray["uid"] = $importer["uid"]; - $datarray["contact-id"] = $author_contact["cid"]; - $datarray["network"] = $author_contact["network"]; + $datarray = self::setDirection($datarray, $direction); - $datarray["owner-link"] = $datarray["author-link"] = $person["url"]; - $datarray["owner-id"] = $datarray["author-id"] = Contact::getIdForURL($person["url"], 0); + $datarray['owner-link'] = $datarray['author-link'] = $author_url; + $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($author_url); - $datarray["guid"] = $guid; - $datarray["uri"] = self::getUriFromGuid($author, $guid); + $datarray['guid'] = $guid; + $datarray['uri'] = self::getUriFromGuid($guid, $author); - $datarray["verb"] = Activity::FOLLOW; - $datarray["gravity"] = GRAVITY_ACTIVITY; + $datarray['verb'] = Activity::FOLLOW; + $datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['thr-parent'] = $toplevel_parent_item['uri']; - $datarray["object-type"] = Activity\ObjectType::NOTE; + $datarray['object-type'] = Activity\ObjectType::NOTE; - $datarray["body"] = Activity::FOLLOW; + $datarray['body'] = Activity::FOLLOW; // Diaspora doesn't provide a date for a participation - $datarray["changed"] = $datarray["created"] = $datarray["edited"] = DateTimeFormat::utcNow(); + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = DateTimeFormat::utcNow(); if (Item::isTooOld($datarray)) { Logger::info('Participation is too old', ['created' => $datarray['created'], 'uid' => $datarray['uid'], 'guid' => $datarray['guid']]); @@ -1977,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; } @@ -1995,8 +2039,8 @@ class Diaspora continue; } - Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $author_contact["cid"]]); - if (Worker::add(PRIORITY_HIGH, 'Delivery', Delivery::POST, $comment['id'], $author_contact["cid"])) { + Logger::info('Deliver participation', ['item' => $comment['id'], 'contact' => $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); } } @@ -2009,7 +2053,7 @@ class Diaspora * Processes photos - unneeded * * @param array $importer Array of the importer user - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool always true */ @@ -2021,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 @@ -2038,74 +2082,74 @@ class Diaspora * Processes incoming profile updates * * @param array $importer Array of the importer user - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveProfile(array $importer, $data) + 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); + $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)); $location = Markdown::toBBCode(XML::unescape($data->location)); - $searchable = (XML::unescape($data->searchable) == "true"); - $nsfw = (XML::unescape($data->nsfw) == "true"); + $searchable = (XML::unescape($data->searchable) == 'true'); + $nsfw = (XML::unescape($data->nsfw) == 'true'); $tags = XML::unescape($data->tag_string); - $tags = explode("#", $tags); + $tags = explode('#', $tags); $keywords = []; foreach ($tags as $tag) { $tag = trim(strtolower($tag)); - if ($tag != "") { + if ($tag != '') { $keywords[] = $tag; } } - $keywords = implode(", ", $keywords); + $keywords = implode(', ', $keywords); - $handle_parts = explode("@", $author); - $nick = $handle_parts[0]; - - if ($name === "") { - $name = $handle_parts[0]; + if ($name === '') { + $name = $author->getUser(); } - if (preg_match("|^https?://|", $image_url) === 0) { - $image_url = "http://".$handle_parts[1].$image_url; + if (preg_match('|^https?://|', $image_url) === 0) { + // @TODO No HTTPS here? + $image_url = 'http://' . $author->getFullHost() . $image_url; } - Contact::updateAvatar($contact["id"], $image_url); + Contact::updateAvatar($contact['id'], $image_url); // Generic birthday. We don't know the timezone. The year is irrelevant. - $birthday = str_replace("1000", "1901", $birthday); + $birthday = str_replace('1000', '1901', $birthday); - if ($birthday != "") { - $birthday = DateTimeFormat::utc($birthday, "Y-m-d"); + if ($birthday != '') { + $birthday = DateTimeFormat::utc($birthday, 'Y-m-d'); } // this is to prevent multiple birthday notifications in a single year // if we already have a stored birthday and the 'm-d' part hasn't changed, preserve the entry, which will preserve the notify year - if (substr($birthday, 5) === substr($contact["bd"], 5)) { - $birthday = $contact["bd"]; + if (substr($birthday, 5) === substr($contact['bd'], 5)) { + $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; @@ -2113,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; } @@ -2128,10 +2172,10 @@ class Diaspora */ private static function receiveRequestMakeFriend(array $importer, array $contact) { - if ($contact["rel"] == Contact::SHARING) { + if ($contact['rel'] == Contact::SHARING) { Contact::update( ['rel' => Contact::FRIEND, 'writable' => true], - ['id' => $contact["id"], 'uid' => $importer["uid"]] + ['id' => $contact['id'], 'uid' => $importer['uid']] ); } } @@ -2140,100 +2184,104 @@ class Diaspora * Processes incoming sharing notification * * @param array $importer Array of the importer user - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool Success * @throws \Exception */ - private static function receiveContactRequest(array $importer, $data) + 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"); + $following = (XML::unescape($data->following) == 'true'); } else { $following = true; } if (isset($data->sharing)) { - $sharing = (XML::unescape($data->sharing) == "true"); + $sharing = (XML::unescape($data->sharing) == 'true'); } else { $sharing = true; } - $contact = self::contactByHandle($importer["uid"], $author); + $contact = self::contactByHandle($importer['uid'], $author); // perhaps we were already sharing with this person. Now they're sharing with us. // 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 - $contact = self::contactByHandle($importer["uid"], $author); + $contact = self::contactByHandle($importer['uid'], $author); // If we are now friends, we are sending a share message. // Normally we needn't to do so, but the first message could have been vanished. - if (in_array($contact["rel"], [Contact::FRIEND])) { - $user = DBA::selectFirst('user', [], ['uid' => $importer["uid"]]); + 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); } } return true; } else { - Logger::info("Author ".$author." doesn't want to follow us anymore."); + Logger::info("Author " . $author . " doesn't want to follow us anymore."); Contact::removeFollower($contact); return true; } } - if (!$following && $sharing && in_array($importer["page-flags"], [User::PAGE_FLAGS_SOAPBOX, User::PAGE_FLAGS_NORMAL])) { - Logger::info("Author ".$author." wants to share with us - but doesn't want to listen. Request is ignored."); + if (!$following && $sharing && in_array($importer['page-flags'], [User::PAGE_FLAGS_SOAPBOX, User::PAGE_FLAGS_NORMAL])) { + Logger::info("Author " . $author . " wants to share with us - but doesn't want to listen. Request is ignored."); return false; } elseif (!$following && !$sharing) { - Logger::info("Author ".$author." doesn't want anything - and we don't know the author. Request is ignored."); + Logger::info("Author " . $author . " doesn't want anything - and we don't know the author. Request is ignored."); return false; } elseif (!$following && $sharing) { - Logger::info("Author ".$author." wants to share with us."); + 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."); + 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) { $contact_record = self::contactByHandle($importer['uid'], $author); if (!$contact_record) { Logger::info('unable to locate newly created contact record.'); - return; + return false; } $user = DBA::selectFirst('user', [], ['uid' => $importer['uid']]); @@ -2248,161 +2296,34 @@ 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 The fetched item - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - * @throws \ImagickException - */ - public static function originalItem($guid, $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($item, $parent_message_id, $guid, $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 * * @param array $importer Array of the importer user - * @param object $data The message object + * @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) * - * @return int the message id + * @return bool Success or failure * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveReshare(array $importer, $data, $xml, int $direction) + 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; } @@ -2411,69 +2332,50 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $message_id = self::messageExists($importer["uid"], $guid); + $message_id = self::messageExists($importer['uid'], $guid); if ($message_id) { 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"]; - $datarray["contact-id"] = $contact["id"]; - $datarray["network"] = Protocol::DIASPORA; + $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['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['owner-link'] = $datarray['author-link']; + $datarray['owner-id'] = $datarray['author-id']; - $datarray["guid"] = $guid; - $datarray["uri"] = $datarray["thr-parent"] = self::getUriFromGuid($author, $guid); + $datarray['guid'] = $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["protocol"] = Conversation::PARCEL_DIASPORA; - $datarray["source"] = $xml; - $datarray["direction"] = in_array($direction, [self::FETCHED, self::FORCED_FETCH]) ? Conversation::PULL : Conversation::PUSH; + $datarray['verb'] = Activity::POST; + $datarray['gravity'] = Item::GRAVITY_PARENT; - /// @todo Copy tag data from original post + $datarray['protocol'] = Conversation::PARCEL_DIASPORA; + $datarray['source'] = $xml; - $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["private"] = (($public == "false") ? Item::PRIVATE : Item::PUBLIC); - $datarray["changed"] = $datarray["created"] = $datarray["edited"] = $created_at; - - $datarray["object-type"] = $original_item["object-type"]; + $datarray['body'] = ''; + $datarray['plink'] = self::plink($author, $guid); + $datarray['private'] = (($public == 'false') ? Item::PRIVATE : Item::PUBLIC); + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; self::fetchGuid($datarray); @@ -2486,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); } @@ -2502,31 +2399,49 @@ 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 * * @param array $importer Array of the importer user * @param array $contact The contact of the item owner - * @param object $data The message object + * @param SimpleXMLElement $data The message object * * @return bool success * @throws \Exception */ - private static function itemRetraction(array $importer, array $contact, $data) + 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']; @@ -2540,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; } @@ -2554,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); @@ -2571,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 object $data The message object + * @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, $sender, $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"]); + $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']); return false; } @@ -2592,23 +2507,23 @@ 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": - case "Like": - case "Post": - case "Reshare": - case "StatusMessage": + case 'Comment': + case 'Like': + case 'Post': + case 'Reshare': + case 'StatusMessage': return self::itemRetraction($importer, $contact, $data); - case "PollParticipation": - case "Photo": + case 'PollParticipation': + case 'Photo': // Currently unsupported break; default: - Logger::notice("Unknown target type ".$target_type); + Logger::notice('Unknown target type ' . $target_type); return false; } return true; @@ -2624,11 +2539,10 @@ class Diaspora * * @return boolean Is the message wanted? */ - private static function isSolicitedMessage(array $item, string $author, string $body, int $direction) + private static function isSolicitedMessage(array $item, string $author, string $body, int $direction): bool { $contact = Contact::getByURL($author); - if (DBA::exists('contact', ["`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)", - $contact['nurl'], 0, Contact::FRIEND, Contact::SHARING])) { + if (DBA::exists('contact', ['`nurl` = ? AND `uid` != ? AND `rel` IN (?, ?)', $contact['nurl'], 0, Contact::FRIEND, Contact::SHARING])) { Logger::debug('Author has got followers - accepted', ['uri-id' => $item['uri-id'], 'guid' => $item['guid'], 'url' => $item['uri'], 'author' => $author]); return true; } @@ -2652,21 +2566,48 @@ class Diaspora * * @param int $uriid * @param object $photo + * * @return void */ private static function storePhotoAsMedia(int $uriid, $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 ?? ''); + // @TODO Need to find object type, roland@f.haeder.net + 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 * @@ -2675,20 +2616,20 @@ class Diaspora * @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 newly created item + * @return int|bool The message id of the newly created item or false on error * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, $xml, int $direction) + 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; } @@ -2697,7 +2638,7 @@ class Diaspora GServer::setProtocol($contact['gsid'], Post\DeliveryData::DIASPORA); } - $message_id = self::messageExists($importer["uid"], $guid); + $message_id = self::messageExists($importer['uid'], $guid); if ($message_id) { return true; } @@ -2711,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) { @@ -2723,14 +2685,10 @@ class Diaspora self::storePhotoAsMedia($datarray['uri-id'], $photo); } - $datarray["object-type"] = Activity\ObjectType::IMAGE; - $datarray["post-type"] = Item::PT_IMAGE; + $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; + $datarray['post-type'] = Item::PT_POLL; } /// @todo enable support for polls @@ -2742,54 +2700,26 @@ 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"]); + Tag::storeRawTagsFromBody($datarray['uri-id'], $datarray['body']); if (!self::isSolicitedMessage($datarray, $author, $body, $direction)) { DBA::delete('item-uri', ['uri' => $datarray['uri']]); return false; } - if ($provider_display_name != "") { - $datarray["app"] = $provider_display_name; + if ($provider_display_name != '') { + $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; + $datarray['changed'] = $datarray['created'] = $datarray['edited'] = $created_at; - if (isset($address["address"])) { - $datarray["location"] = $address["address"]; + if (isset($address['address'])) { + $datarray['location'] = $address['address']; } - if (isset($address["lat"]) && isset($address["lng"])) { - $datarray["coord"] = $address["lat"]." ".$address["lng"]; + if (isset($address['lat']) && isset($address['lng'])) { + $datarray['coord'] = $address['lat'] . ' ' . $address['lng']; } self::fetchGuid($datarray); @@ -2804,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); } @@ -2819,28 +2749,28 @@ class Diaspora * ************************************************************************************** */ /** - * returnes the handle of a contact + * returns the handle of a contact * * @param array $contact contact array * * @return string the handle in the format user@domain.tld * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function myHandle(array $contact) + private static function myHandle(array $contact): string { - if (!empty($contact["addr"])) { - return $contact["addr"]; + if (!empty($contact['addr'])) { + return $contact['addr']; } // 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. - if ($contact["nickname"] != "") { - $nick = $contact["nickname"]; + // So - just in case - we build the address here. + if ($contact['nickname'] != '') { + $nick = $contact['nickname']; } else { - $nick = $contact["nick"]; + $nick = $contact['nick']; } - return $nick . "@" . substr(DI::baseUrl(), strpos(DI::baseUrl(), "://") + 3); + return $nick . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3); } @@ -2856,13 +2786,13 @@ class Diaspora * @return string The encrypted data * @throws \Exception */ - public static function encodePrivateData($msg, array $user, array $contact, $prvkey, $pubkey) + 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; } @@ -2873,16 +2803,18 @@ class Diaspora $ciphertext = self::aesEncrypt($aes_key, $iv, $msg); - $json = json_encode(["iv" => $b_iv, "key" => $b_aes_key]); + $json = json_encode(['iv' => $b_iv, 'key' => $b_aes_key]); - $encrypted_key_bundle = ""; + $encrypted_key_bundle = ''; if (!@openssl_public_encrypt($json, $encrypted_key_bundle, $pubkey)) { return false; } $json_object = json_encode( - ["aes_key" => base64_encode($encrypted_key_bundle), - "encrypted_magic_envelope" => base64_encode($ciphertext)] + [ + 'aes_key' => base64_encode($encrypted_key_bundle), + 'encrypted_magic_envelope' => base64_encode($ciphertext) + ] ); return $json_object; @@ -2897,35 +2829,39 @@ class Diaspora * @return string The envelope * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function buildMagicEnvelope($msg, array $user) + 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"; - $encoding = "base64url"; - $alg = "RSA-SHA256"; - $signable_data = $data.".".Strings::base64UrlEncode($type).".".Strings::base64UrlEncode($encoding).".".Strings::base64UrlEncode($alg); + $type = 'application/xml'; + $encoding = 'base64url'; + $alg = 'RSA-SHA256'; + $signable_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg); // Fallback if the private key wasn't transmitted in the expected field - if ($user['uprvkey'] == "") { + if ($user['uprvkey'] == '') { $user['uprvkey'] = $user['prvkey']; } - $signature = Crypto::rsaSign($signable_data, $user["uprvkey"]); + $signature = Crypto::rsaSign($signable_data, $user['uprvkey']); $sig = Strings::base64UrlEncode($signature); - $xmldata = ["me:env" => ["me:data" => $data, - "@attributes" => ["type" => $type], - "me:encoding" => $encoding, - "me:alg" => $alg, - "me:sig" => $sig, - "@attributes2" => ["key_id" => $key_id]]]; + $xmldata = [ + 'me:env' => [ + '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); } /** @@ -2941,7 +2877,7 @@ class Diaspora * @return string The message that will be transmitted to other servers * @throws \Exception */ - public static function buildMessage($msg, array $user, array $contact, $prvkey, $pubkey, $public = false) + public static function buildMessage(string $msg, array $user, array $contact, string $prvkey, string $pubkey, bool $public = false): string { // The message is put into an envelope with the sender's signature $envelope = self::buildMagicEnvelope($msg, $user); @@ -2962,15 +2898,15 @@ class Diaspora * * @return string The signature */ - private static function signature($owner, $message) + private static function signature(array $owner, array $message): string { $sigmsg = $message; - unset($sigmsg["author_signature"]); - unset($sigmsg["parent_author_signature"]); + unset($sigmsg['author_signature']); + unset($sigmsg['parent_author_signature']); - $signed_text = implode(";", $sigmsg); + $signed_text = implode(';', $sigmsg); - return base64_encode(Crypto::rsaSign($signed_text, $owner["uprvkey"], "sha256")); + return base64_encode(Crypto::rsaSign($signed_text, $owner['uprvkey'], 'sha256')); } /** @@ -2986,46 +2922,51 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function transmit(array $owner, array $contact, $envelope, $public_batch, $guid = "") + private static function transmit(array $owner, array $contact, string $envelope, bool $public_batch, string $guid = ''): int { - $enabled = intval(DI::config()->get("system", "diaspora_enabled")); + $enabled = intval(DI::config()->get('system', 'diaspora_enabled')); if (!$enabled) { return 200; } $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)) { - $dest_url = ($public_batch ? $contact["batch"] : $contact["notify"]); + $dest_url = ($public_batch ? $contact['batch'] : $contact['notify']); } 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"); + if (!intval(DI::config()->get('system', 'diaspora_test'))) { + $content_type = (($public_batch) ? 'application/magic-envelope+xml' : 'application/json'); - $postResult = DI::httpClient()->post($dest_url . "/", $envelope, ['Content-Type' => $content_type]); + $postResult = DI::httpClient()->post($dest_url . '/', $envelope, ['Content-Type' => $content_type]); $return_code = $postResult->getReturnCode(); } else { - Logger::notice("test_mode"); + Logger::notice('test_mode'); 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; } @@ -3038,12 +2979,11 @@ class Diaspora * @param array $message The message data * * @return string The post XML + * @throws \Exception */ - public static function buildPostXml($type, $message) + public static function buildPostXml(string $type, array $message): string { - $data = [$type => $message]; - - return XML::fromArray($data, $xml); + return XML::fromArray([$type => $message]); } /** @@ -3060,7 +3000,7 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function buildAndTransmit(array $owner, array $contact, $type, $message, $public_batch = false, $guid = "") + private static function buildAndTransmit(array $owner, array $contact, string $type, array $message, bool $public_batch = false, string $guid = '') { $msg = self::buildPostXml($type, $message); @@ -3070,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); @@ -3103,18 +3043,18 @@ class Diaspora * @return int The result of the transmission * @throws \Exception */ - private static function sendParticipation(array $contact, array $item) + private static function sendParticipation(array $contact, array $item): int { // Don't send notifications for private postings if ($item['private'] == Item::PRIVATE) { - return; + return 0; } - $cachekey = "diaspora:sendParticipation:".$item['guid']; + $cachekey = 'diaspora:sendParticipation:' . $item['guid']; $result = DI::cache()->get($cachekey); if (!is_null($result)) { - return; + return -1; } // Fetch some user id to have a valid handle to transmit the participation. @@ -3122,27 +3062,31 @@ 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, - "guid" => System::createUUID(), - "parent_type" => "Post", - "parent_guid" => $item["guid"]]; + $message = [ + '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); + DI::cache()->set($cachekey, $item['guid'], Duration::QUARTER_HOUR); - return self::buildAndTransmit($owner, $contact, "participation", $message); + return self::buildAndTransmit($owner, $contact, 'participation', $message); } /** @@ -3156,21 +3100,23 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function sendAccountMigration(array $owner, array $contact, $uid) + public static function sendAccountMigration(array $owner, array $contact, int $uid): int { $old_handle = DI::pConfig()->get($uid, 'system', 'previous_addr'); $profile = self::createProfileData($uid); - $signed_text = 'AccountMigration:'.$old_handle.':'.$profile['author']; - $signature = base64_encode(Crypto::rsaSign($signed_text, $owner["uprvkey"], "sha256")); + $signed_text = 'AccountMigration:' . $old_handle . ':' . $profile['author']; + $signature = base64_encode(Crypto::rsaSign($signed_text, $owner['uprvkey'], 'sha256')); - $message = ["author" => $old_handle, - "profile" => $profile, - "signature" => $signature]; + $message = [ + 'author' => $old_handle, + 'profile' => $profile, + 'signature' => $signature + ]; Logger::info('Send account migration', ['msg' => $message]); - return self::buildAndTransmit($owner, $contact, "account_migration", $message); + return self::buildAndTransmit($owner, $contact, 'account_migration', $message); } /** @@ -3182,7 +3128,7 @@ class Diaspora * @return int The result of the transmission * @throws \Exception */ - public static function sendShare(array $owner, array $contact) + public static function sendShare(array $owner, array $contact): int { /** * @todo support the different possible combinations of "following" and "sharing" @@ -3207,14 +3153,16 @@ class Diaspora } */ - $message = ["author" => self::myHandle($owner), - "recipient" => $contact["addr"], - "following" => "true", - "sharing" => "true"]; + $message = [ + 'author' => self::myHandle($owner), + 'recipient' => $contact['addr'], + 'following' => 'true', + 'sharing' => 'true' + ]; Logger::info('Send share', ['msg' => $message]); - return self::buildAndTransmit($owner, $contact, "contact", $message); + return self::buildAndTransmit($owner, $contact, 'contact', $message); } /** @@ -3226,74 +3174,45 @@ class Diaspora * @return int The result of the transmission * @throws \Exception */ - public static function sendUnshare(array $owner, array $contact) + public static function sendUnshare(array $owner, array $contact): int { - $message = ["author" => self::myHandle($owner), - "recipient" => $contact["addr"], - "following" => "false", - "sharing" => "false"]; + $message = [ + 'author' => self::myHandle($owner), + 'recipient' => $contact['addr'], + 'following' => 'false', + 'sharing' => 'false' + ]; Logger::info('Send unshare', ['msg' => $message]); - return self::buildAndTransmit($owner, $contact, "contact", $message); + return self::buildAndTransmit($owner, $contact, 'contact', $message); } /** - * 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($body, $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'], + ]; } /** @@ -3304,7 +3223,7 @@ class Diaspora * @return array with event data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function buildEvent($event_id) + private static function buildEvent(string $event_id): array { $event = DBA::selectFirst('event', [], ['id' => $event_id]); if (!DBA::isResult($event)) { @@ -3327,7 +3246,7 @@ class Diaspora $mask = DateTimeFormat::ATOM; /// @todo - establish "all day" events in Friendica - $eventdata["all_day"] = "false"; + $eventdata['all_day'] = 'false'; $eventdata['timezone'] = 'UTC'; @@ -3348,13 +3267,13 @@ class Diaspora $coord = Map::getCoordinates($event['location']); $location = []; - $location["address"] = html_entity_decode(BBCode::toMarkdown($event['location'])); + $location['address'] = html_entity_decode(BBCode::toMarkdown($event['location'])); if (!empty($coord['lat']) && !empty($coord['lon'])) { - $location["lat"] = $coord['lat']; - $location["lng"] = $coord['lon']; + $location['lat'] = $coord['lat']; + $location['lng'] = $coord['lon']; } else { - $location["lat"] = 0; - $location["lng"] = 0; + $location['lat'] = 0; + $location['lng'] = 0; } $eventdata['location'] = $location; } @@ -3376,7 +3295,7 @@ class Diaspora */ public static function buildStatus(array $item, array $owner) { - $cachekey = "diaspora:buildStatus:".$item['guid']; + $cachekey = 'diaspora:buildStatus:' . $item['guid']; $result = DI::cache()->get($cachekey); if (!is_null($result)) { @@ -3385,47 +3304,53 @@ class Diaspora $myaddr = self::myHandle($owner); - $public = ($item["private"] == Item::PRIVATE ? "false" : "true"); + $public = ($item['private'] == Item::PRIVATE ? 'false' : 'true'); $created = DateTimeFormat::utc($item['received'], DateTimeFormat::ATOM); - $edited = DateTimeFormat::utc($item["edited"] ?? $item["created"], DateTimeFormat::ATOM); + $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"]))) { - $message = ["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]; - - $type = "reshare"; + 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'], + 'provider_display_name' => $item['app'], + 'public' => $public + ]; + + $type = 'reshare'; } else { - $title = $item["title"]; - $body = Post\Media::addAttachmentsToBody($item['uri-id'], $item['body']); + $native_photos = DI::config()->get('diaspora', 'native_photos'); + if ($native_photos) { + $item['body'] = Post\Media::removeFromEndOfBody($item['body']); + $attach_media = [Post\Media::AUDIO, Post\Media::VIDEO]; + } else { + $attach_media = [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO]; + } + + $title = $item['title']; + $body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item), $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']; + if (empty($item['title']) && DI::pConfig()->get($owner['uid'], 'system', 'attach_link_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)); // Adding the title if (strlen($title)) { - $body = "### ".html_entity_decode($title)."\n\n".$body; + $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) { @@ -3435,27 +3360,33 @@ class Diaspora $location = []; - if ($item["location"] != "") - $location["address"] = $item["location"]; + if ($item['location'] != '') + $location['address'] = $item['location']; - if ($item["coord"] != "") { - $coord = explode(" ", $item["coord"]); - $location["lat"] = $coord[0]; - $location["lng"] = $coord[1]; + if ($item['coord'] != '') { + $coord = explode(' ', $item['coord']); + $location['lat'] = $coord[0]; + $location['lng'] = $coord[1]; } - $message = ["author" => $myaddr, - "guid" => $item["guid"], - "created_at" => $created, - "edited_at" => $edited, - "public" => $public, - "text" => $body, - "provider_display_name" => $item["app"], - "location" => $location]; + $message = [ + 'author' => $myaddr, + 'guid' => $item['guid'], + 'created_at' => $created, + 'edited_at' => $edited, + 'public' => $public, + 'text' => $body, + 'provider_display_name' => $item['app'], + '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"]); + if (!isset($location['lat']) || !isset($location['lng'])) { + unset($message['location']); } if ($item['event-id'] > 0) { @@ -3463,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']; } @@ -3474,20 +3407,62 @@ class Diaspora } } - $type = "status_message"; + $type = 'status_message'; } - $msg = ["type" => $type, "message" => $message]; + $msg = [ + 'type' => $type, + 'message' => $message + ]; DI::cache()->set($cachekey, $msg, Duration::QUARTER_HOUR); return $msg; } - private static function prependParentAuthorMention($body, $profile_url) + /** + * 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) ) { @@ -3509,11 +3484,11 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function sendStatus(array $item, array $owner, array $contact, $public_batch = false) + public static function sendStatus(array $item, array $owner, array $contact, bool $public_batch = false): int { $status = self::buildStatus($item, $owner); - return self::buildAndTransmit($owner, $contact, $status["type"], $status["message"], $public_batch, $item["guid"]); + return self::buildAndTransmit($owner, $contact, $status['type'], $status['message'], $public_batch, $item['guid']); } /** @@ -3522,30 +3497,32 @@ class Diaspora * @param array $item The item that will be exported * @param array $owner the array of the item owner * - * @return array The data for a "like" + * @return array|bool The data for a "like" or false on error * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ private static function constructLike(array $item, array $owner) { - $parent = Post::selectFirst(['guid', 'uri', 'thr-parent'], ['uri' => $item["thr-parent"]]); + $parent = Post::selectFirst(['guid', 'uri', 'thr-parent'], ['uri' => $item['thr-parent']]); if (!DBA::isResult($parent)) { return false; } - $target_type = ($parent["uri"] === $parent["thr-parent"] ? "Post" : "Comment"); + $target_type = ($parent['uri'] === $parent['thr-parent'] ? 'Post' : 'Comment'); $positive = null; if ($item['verb'] === Activity::LIKE) { - $positive = "true"; + $positive = 'true'; } elseif ($item['verb'] === Activity::DISLIKE) { - $positive = "false"; + $positive = 'false'; } - return(["author" => self::myHandle($owner), - "guid" => $item["guid"], - "parent_guid" => $parent["guid"], - "parent_type" => $target_type, - "positive" => $positive, - "author_signature" => ""]); + return [ + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'parent_guid' => $parent['guid'], + 'parent_type' => $target_type, + 'positive' => $positive, + 'author_signature' => '', + ]; } /** @@ -3554,7 +3531,7 @@ class Diaspora * @param array $item The item that will be exported * @param array $owner the array of the item owner * - * @return array The data for an "EventParticipation" + * @return array|bool The data for an "EventParticipation" or false on error * @throws \Exception */ private static function constructAttend(array $item, array $owner) @@ -3575,15 +3552,17 @@ 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; } - return(["author" => self::myHandle($owner), - "guid" => $item["guid"], - "parent_guid" => $parent["guid"], - "status" => $attend_answer, - "author_signature" => ""]); + return [ + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'parent_guid' => $parent['guid'], + 'status' => $attend_answer, + 'author_signature' => '' + ]; } /** @@ -3597,7 +3576,7 @@ class Diaspora */ private static function constructComment(array $item, array $owner) { - $cachekey = "diaspora:constructComment:".$item['guid']; + $cachekey = 'diaspora:constructComment:' . $item['guid']; $result = DI::cache()->get($cachekey); if (!is_null($result)) { @@ -3615,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 @@ -3623,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') ) { @@ -3631,17 +3611,17 @@ class Diaspora } $text = html_entity_decode(BBCode::toMarkdown($body)); - $created = DateTimeFormat::utc($item["created"], DateTimeFormat::ATOM); - $edited = DateTimeFormat::utc($item["edited"], DateTimeFormat::ATOM); + $created = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); + $edited = DateTimeFormat::utc($item['edited'], DateTimeFormat::ATOM); $comment = [ - "author" => self::myHandle($owner), - "guid" => $item["guid"], - "created_at" => $created, - "edited_at" => $edited, - "parent_guid" => $toplevel_item["guid"], - "text" => $text, - "author_signature" => "" + 'author' => self::myHandle($owner), + 'guid' => $item['guid'], + 'created_at' => $created, + 'edited_at' => $edited, + 'parent_guid' => $toplevel_item['guid'], + 'text' => $text, + 'author_signature' => '', ]; // Send the thread parent guid only if it is a threaded comment @@ -3651,7 +3631,7 @@ class Diaspora DI::cache()->set($cachekey, $comment, Duration::QUARTER_HOUR); - return($comment); + return $comment; } /** @@ -3666,26 +3646,26 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function sendFollowup(array $item, array $owner, array $contact, $public_batch = false) + public static function sendFollowup(array $item, array $owner, array $contact, bool $public_batch = false): int { if (in_array($item['verb'], [Activity::ATTEND, Activity::ATTENDNO, Activity::ATTENDMAYBE])) { $message = self::constructAttend($item, $owner); - $type = "event_participation"; - } elseif (in_array($item["verb"], [Activity::LIKE, Activity::DISLIKE])) { + $type = 'event_participation'; + } elseif (in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { $message = self::constructLike($item, $owner); - $type = "like"; - } elseif (!in_array($item["verb"], [Activity::FOLLOW, Activity::TAG])) { + $type = 'like'; + } elseif (!in_array($item['verb'], [Activity::FOLLOW, Activity::TAG])) { $message = self::constructComment($item, $owner); - $type = "comment"; + $type = 'comment'; } if (empty($message)) { - return false; + return -1; } - $message["author_signature"] = self::signature($owner, $message); + $message['author_signature'] = self::signature($owner, $message); - return self::buildAndTransmit($owner, $contact, $type, $message, $public_batch, $item["guid"]); + return self::buildAndTransmit($owner, $contact, $type, $message, $public_batch, $item['guid']); } /** @@ -3699,43 +3679,43 @@ class Diaspora * @return int The result of the transmission * @throws \Exception */ - public static function sendRelay(array $item, array $owner, array $contact, $public_batch = false) + public static function sendRelay(array $item, array $owner, array $contact, bool $public_batch = false): int { - if ($item["deleted"]) { + if ($item['deleted']) { return self::sendRetraction($item, $owner, $contact, $public_batch, true); - } elseif (in_array($item["verb"], [Activity::LIKE, Activity::DISLIKE])) { - $type = "like"; + } elseif (in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { + $type = 'like'; } else { - $type = "comment"; + $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)) { foreach ($msg as $field => $data) { - if (!$item["deleted"]) { - if ($field == "diaspora_handle") { - $field = "author"; + if (!$item['deleted']) { + if ($field == 'diaspora_handle') { + $field = 'author'; } - if ($field == "target_type") { - $field = "parent_type"; + if ($field == 'target_type') { + $field = 'parent_type'; } } $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); + $message['parent_author_signature'] = self::signature($owner, $message); Logger::info('Relayed data', ['msg' => $message]); - return self::buildAndTransmit($owner, $contact, $type, $message, $public_batch, $item["guid"]); + return self::buildAndTransmit($owner, $contact, $type, $message, $public_batch, $item['guid']); } /** @@ -3750,27 +3730,29 @@ class Diaspora * @return int The result of the transmission * @throws \Exception */ - public static function sendRetraction(array $item, array $owner, array $contact, $public_batch = false, $relay = false) + 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"; + $msg_type = 'retraction'; - if ($item['gravity'] == GRAVITY_PARENT) { - $target_type = "Post"; - } elseif (in_array($item["verb"], [Activity::LIKE, Activity::DISLIKE])) { - $target_type = "Like"; + if ($item['gravity'] == Item::GRAVITY_PARENT) { + $target_type = 'Post'; + } elseif (in_array($item['verb'], [Activity::LIKE, Activity::DISLIKE])) { + $target_type = 'Like'; } else { - $target_type = "Comment"; + $target_type = 'Comment'; } - $message = ["author" => $itemaddr, - "target_guid" => $item['guid'], - "target_type" => $target_type]; + $message = [ + 'author' => $itemaddr, + 'target_guid' => $item['guid'], + 'target_type' => $target_type + ]; Logger::info('Got message', ['msg' => $message]); - return self::buildAndTransmit($owner, $contact, $msg_type, $message, $public_batch, $item["guid"]); + return self::buildAndTransmit($owner, $contact, $msg_type, $message, $public_batch, $item['guid']); } /** @@ -3784,44 +3766,44 @@ class Diaspora * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function sendMail(array $item, array $owner, array $contact) + public static function sendMail(array $item, array $owner, array $contact): int { $myaddr = self::myHandle($owner); - $cnv = DBA::selectFirst('conv', [], ['id' => $item["convid"], 'uid' => $item["uid"]]); + $cnv = DBA::selectFirst('conv', [], ['id' => $item['convid'], 'uid' => $item['uid']]); if (!DBA::isResult($cnv)) { - Logger::notice("conversation not found."); - return; + Logger::notice('Conversation not found.'); + return -1; } - $body = BBCode::toMarkdown($item["body"]); - $created = DateTimeFormat::utc($item["created"], DateTimeFormat::ATOM); + $body = BBCode::toMarkdown($item['body']); + $created = DateTimeFormat::utc($item['created'], DateTimeFormat::ATOM); $msg = [ - "author" => $myaddr, - "guid" => $item["guid"], - "conversation_guid" => $cnv["guid"], - "text" => $body, - "created_at" => $created, + 'author' => $myaddr, + 'guid' => $item['guid'], + 'conversation_guid' => $cnv['guid'], + 'text' => $body, + 'created_at' => $created, ]; - if ($item["reply"]) { + if ($item['reply']) { $message = $msg; - $type = "message"; + $type = 'message'; } else { $message = [ - "author" => $cnv["creator"], - "guid" => $cnv["guid"], - "subject" => $cnv["subject"], - "created_at" => DateTimeFormat::utc($cnv['created'], DateTimeFormat::ATOM), - "participants" => $cnv["recips"], - "message" => $msg + 'author' => $cnv['creator'], + 'guid' => $cnv['guid'], + 'subject' => $cnv['subject'], + 'created_at' => DateTimeFormat::utc($cnv['created'], DateTimeFormat::ATOM), + 'participants' => $cnv['recips'], + 'message' => $msg ]; - $type = "conversation"; + $type = 'conversation'; } - return self::buildAndTransmit($owner, $contact, $type, $message, false, $item["guid"]); + return self::buildAndTransmit($owner, $contact, $type, $message, false, $item['guid']); } /** @@ -3831,7 +3813,8 @@ class Diaspora * * @return array The array with "first" and "last" */ - public static function splitName($name) { + public static function splitName(string $name): array + { $name = trim($name); // Is the name longer than 64 characters? Then cut the rest of it. @@ -3888,105 +3871,103 @@ class Diaspora * @return array The profile data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function createProfileData($uid) + 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); - } - - 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]; + $data['tag_string'] = trim($data['tag_string']); + } + + return $data; } /** * Sends profile data * - * @param int $uid The user id - * @param bool $recips optional, default false + * @param int $uid The user id + * @param array $recipients optional, default empty array + * * @return void * @throws \Exception */ - public static function sendProfile($uid, $recips = false) + 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); } } @@ -3996,18 +3977,19 @@ class Diaspora * @param integer $uid The user of that comment * @param array $item Item array * - * @return array Signed content + * @return array|bool Signed content or false on error * @throws \Exception */ - public static function createLikeSignature($uid, array $item) + public static function createLikeSignature(int $uid, array $item) { $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])) { + 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; } @@ -4016,7 +3998,7 @@ class Diaspora return false; } - $message["author_signature"] = self::signature($owner, $message); + $message['author_signature'] = self::signature($owner, $message); return $message; } @@ -4026,11 +4008,12 @@ class Diaspora * * @param array $item Item array * - * @return array Signed content + * @return array|bool Signed content or false on error * @throws \Exception */ public static function createCommentSignature(array $item) { + $contact = []; if (!empty($item['author-link'])) { $url = $item['author-link']; } else { @@ -4044,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; } @@ -4059,52 +4042,72 @@ 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; } - $message["author_signature"] = self::signature($owner, $message); + $message['author_signature'] = self::signature($owner, $message); return $message; } - public static function performReshare(int $UriId, int $uid) + /** + * 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'])) {