Use "author_handle" for the author handle extracted from Diaspora XML messages

- We have structured data under the variable name "author"
- Remove unused $uid parameter from Diaspora::storeByGuid
- Convert $person parameter to just its URL in Diaspora::authorContactByUrl
- Flip parameters in getUriFromGuid to get rid of $onlyfound
This commit is contained in:
Michael 2021-08-28 08:42:20 +00:00 committed by Hypolite Petovan
parent d5e005f90d
commit ace80ca1b4
3 changed files with 416 additions and 154 deletions

View file

@ -42,6 +42,7 @@ use Friendica\Model\Post;
use Friendica\Model\Tag; use Friendica\Model\Tag;
use Friendica\Model\User; use Friendica\Model\User;
use Friendica\Network\HTTPClient\Client\HttpClientAccept; use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Network\Probe; use Friendica\Network\Probe;
use Friendica\Util\Crypto; use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
@ -161,8 +162,12 @@ class Diaspora
return false; return false;
} }
$key = self::key($handle); try {
$key = self::key(WebFingerUri::fromString($handle));
if ($key == '') { if ($key == '') {
throw new \InvalidArgumentException();
}
} catch (\InvalidArgumentException $e) {
Logger::notice("Couldn't get a key for handle " . $handle . ". Discarding."); Logger::notice("Couldn't get a key for handle " . $handle . ". Discarding.");
return false; return false;
} }
@ -300,8 +305,13 @@ class Diaspora
} }
} }
$key = self::key($author_addr); try {
$author = WebFingerUri::fromString($author_addr);
$key = self::key($author);
if ($key == '') { if ($key == '') {
throw new \InvalidArgumentException();
}
} catch (\InvalidArgumentException $e) {
Logger::notice("Couldn't get a key for handle " . $author_addr . ". Discarding."); Logger::notice("Couldn't get a key for handle " . $author_addr . ". Discarding.");
if ($no_exit) { if ($no_exit) {
return false; return false;
@ -322,7 +332,7 @@ class Diaspora
return [ return [
'message' => (string)Strings::base64UrlDecode($base->data), 'message' => (string)Strings::base64UrlDecode($base->data),
'author' => XML::unescape($author_addr), 'author' => $author->getAddr(),
'key' => (string)$key 'key' => (string)$key
]; ];
} }
@ -356,7 +366,7 @@ class Diaspora
if ($children->header) { if ($children->header) {
$public = true; $public = true;
$author_link = str_replace('acct:', '', $children->header->author_id); $idom = $children->header;
} else { } else {
// This happens with posts from a relais // This happens with posts from a relais
if (empty($privKey)) { if (empty($privKey)) {
@ -384,8 +394,13 @@ class Diaspora
$inner_iv = base64_decode($idom->iv); $inner_iv = base64_decode($idom->iv);
$inner_aes_key = base64_decode($idom->aes_key); $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); $dom = $basedom->children(ActivityNamespace::SALMON_ME);
@ -439,17 +454,12 @@ class Diaspora
$inner_decrypted = self::aesDecrypt($inner_aes_key, $inner_iv, $inner_encrypted); $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 // 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 fcontact cache)
// This will also convert diaspora public key from pkcs#1 to pkcs#8 // This will also convert diaspora public key from pkcs#1 to pkcs#8
Logger::notice('Fetching key for '.$author_link); Logger::notice('Fetching key for ' . $author);
$key = self::key($author_link); $key = self::key($author);
if (!$key) { if (!$key) {
Logger::notice('Could not retrieve author key.'); Logger::notice('Could not retrieve author key.');
throw new \Friendica\Network\HTTPException\BadRequestException(); throw new \Friendica\Network\HTTPException\BadRequestException();
@ -465,9 +475,9 @@ class Diaspora
Logger::notice('Message verified.'); Logger::notice('Message verified.');
return [ return [
'message' => (string)$inner_decrypted, 'message' => $inner_decrypted,
'author' => XML::unescape($author_link), 'author' => $author->getAddr(),
'key' => (string)$key 'key' => $key
]; ];
} }
@ -520,7 +530,7 @@ class Diaspora
{ {
// The sender is the handle of the contact that sent the message. // 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") // 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 // This is only needed for private postings since this is already done for public ones before
if (is_null($fields)) { if (is_null($fields)) {
@ -535,7 +545,7 @@ class Diaspora
$type = $fields->getName(); $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) { switch ($type) {
case 'account_migration': case 'account_migration':
@ -743,7 +753,7 @@ class Diaspora
} }
if (isset($parent_author_signature)) { if (isset($parent_author_signature)) {
$key = self::key($msg['author']); $key = self::key(WebFingerUri::fromString($msg['author']));
if (empty($key)) { 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; return false;
@ -755,8 +765,12 @@ class Diaspora
} }
} }
$key = self::key($fields->author); try {
$key = self::key(WebFingerUri::fromString($fields->author));
if (empty($key)) { if (empty($key)) {
throw new \InvalidArgumentException();
}
} catch (\Throwable $e) {
Logger::info('No key found', ['author' => $fields->author]); Logger::info('No key found', ['author' => $fields->author]);
return false; return false;
} }
@ -772,19 +786,17 @@ class Diaspora
/** /**
* Fetches the public key for a given handle * Fetches the public key for a given handle
* *
* @param string $handle The handle * @param WebFingerUri $uri The handle
* *
* @return string The public key * @return string The public key
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
private static function key(string $handle = null): string private static function key(WebFingerUri $uri): string
{ {
$handle = strval($handle); Logger::notice('Fetching diaspora key', ['handle' => $uri->getAddr(), 'callstack' => System::callstack(20)]);
Logger::notice('Fetching diaspora key', ['handle' => $handle, 'callstack' => System::callstack(20)]); $fcontact = FContact::getByURL($uri);
$fcontact = FContact::getByURL($handle);
if (!empty($fcontact['pubkey'])) { if (!empty($fcontact['pubkey'])) {
return $fcontact['pubkey']; return $fcontact['pubkey'];
} }
@ -795,18 +807,16 @@ class Diaspora
/** /**
* Get a contact id for a given handle * Get a contact id for a given handle
* *
* @todo Move to Friendica\Model\Contact
*
* @param int $uid The user id * @param int $uid The user id
* @param string $handle The handle in the format user@domain.tld * @param WebFingerUri $uri
* *
* @return array Contact data * @return array Contact data
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
private static function contactByHandle(int $uid, string $handle): array private static function contactByHandle(int $uid, WebFingerUri $uri): array
{ {
return Contact::getByURL($handle, null, [], $uid); return Contact::getByURL($uri->getAddr(), null, [], $uid);
} }
/** /**
@ -875,20 +885,21 @@ class Diaspora
* Fetches the contact id for a handle and checks if posting is allowed * Fetches the contact id for a handle and checks if posting is allowed
* *
* @param array $importer Array of the importer user * @param array $importer Array of the importer user
* @param string $handle The checked handle in the format user@domain.tld * @param WebFingerUri $contact_uri The checked contact
* @param bool $is_comment Is the check for a comment? * @param bool $is_comment Is the check for a comment?
* *
* @return array|bool The contact data or false on error * @return array|bool The contact data or false on error
* @throws \Exception * @throws InternalServerErrorException
* @throws \ImagickException
*/ */
private static function allowedContactByHandle(array $importer, string $handle, bool $is_comment = false) private static function allowedContactByHandle(array $importer, WebFingerUri $contact_uri, bool $is_comment = false)
{ {
$contact = self::contactByHandle($importer['uid'], $handle); $contact = self::contactByHandle($importer['uid'], $contact_uri);
if (!$contact) { 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 a contact isn't found, we accept it anyway if it is a comment
if ($is_comment && ($importer['uid'] != 0)) { if ($is_comment && ($importer['uid'] != 0)) {
return self::contactByHandle(0, $handle); return self::contactByHandle(0, $contact_uri);
} elseif ($is_comment) { } elseif ($is_comment) {
return $importer; return $importer;
} else { } else {
@ -897,7 +908,7 @@ class Diaspora
} }
if (!self::postAllow($importer, $contact, $is_comment)) { 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 false;
} }
return $contact; return $contact;
@ -1011,7 +1022,7 @@ class Diaspora
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
public static function storeByGuid(string $guid, string $server, bool $force) private static function storeByGuid(string $guid, string $server, bool $force)
{ {
$serverparts = parse_url($server); $serverparts = parse_url($server);
@ -1092,24 +1103,26 @@ class Diaspora
return self::message($source_xml->root_guid, $server, ++$level); return self::message($source_xml->root_guid, $server, ++$level);
} }
$author = ''; $author_handle = '';
// Fetch the author - for the old and the new Diaspora version // Fetch the author - for the old and the new Diaspora version
if ($source_xml->post->status_message && $source_xml->post->status_message->diaspora_handle) { if ($source_xml->post->status_message && $source_xml->post->status_message->diaspora_handle) {
$author = (string)$source_xml->post->status_message->diaspora_handle; $author_handle = (string)$source_xml->post->status_message->diaspora_handle;
} elseif ($source_xml->author && ($source_xml->getName() == 'status_message')) { } elseif ($source_xml->author && ($source_xml->getName() == 'status_message')) {
$author = (string)$source_xml->author; $author_handle = (string)$source_xml->author;
} }
try {
$author = WebFingerUri::fromString($author_handle);
} catch (\InvalidArgumentException $e) {
// If this isn't a "status_message" then quit // If this isn't a "status_message" then quit
if (!$author) {
Logger::info("Message doesn't seem to be a status message"); Logger::info("Message doesn't seem to be a status message");
return false; return false;
} }
return [ return [
'message' => $x, 'message' => $x,
'author' => $author, 'author' => $author->getAddr(),
'key' => self::key($author) 'key' => self::key($author)
]; ];
} }
@ -1159,13 +1172,13 @@ class Diaspora
* *
* @param int $uid The user id * @param int $uid The user id
* @param string $guid message guid * @param string $guid message guid
* @param string $author The handle of the item * @param WebFingerUri $author
* @param array $contact The contact of the item owner * @param array $contact The contact of the item owner
* *
* @return array|bool the item record or false on failure * @return array|bool the item record or false on failure
* @throws \Exception * @throws \Exception
*/ */
private static function parentItem(int $uid, string $guid, string $author, array $contact) private static function parentItem(int $uid, string $guid, WebFingerUri $author, array $contact)
{ {
$fields = ['id', 'parent', 'body', 'wall', 'uri', 'guid', 'private', 'origin', $fields = ['id', 'parent', 'body', 'wall', 'uri', 'guid', 'private', 'origin',
'author-name', 'author-link', 'author-avatar', 'gravity', 'author-name', 'author-link', 'author-avatar', 'gravity',
@ -1200,10 +1213,10 @@ class Diaspora
} }
/** /**
* returns contact details * returns contact details for the given user
* *
* @param array $def_contact The default contact if the person isn't found * @param array $def_contact The default details if the contact isn't found
* @param array $person The record of the person * @param string $contact_url The url of the contact
* @param int $uid The user id * @param int $uid The user id
* *
* @return array * @return array
@ -1211,9 +1224,9 @@ class Diaspora
* 'network' => network type * 'network' => network type
* @throws \Exception * @throws \Exception
*/ */
private static function authorContactByUrl(array $def_contact, array $person, int $uid): array private static function authorContactByUrl(array $def_contact, string $contact_url, int $uid): array
{ {
$condition = ['nurl' => Strings::normaliseLink($person['url']), 'uid' => $uid]; $condition = ['nurl' => Strings::normaliseLink($contact_url), 'uid' => $uid];
$contact = DBA::selectFirst('contact', ['id', 'network'], $condition); $contact = DBA::selectFirst('contact', ['id', 'network'], $condition);
if (DBA::isResult($contact)) { if (DBA::isResult($contact)) {
$cid = $contact['id']; $cid = $contact['id'];
@ -1318,21 +1331,27 @@ class Diaspora
*/ */
private static function receiveAccountMigration(array $importer, SimpleXMLElement $data): bool private static function receiveAccountMigration(array $importer, SimpleXMLElement $data): bool
{ {
$old_handle = XML::unescape($data->author); try {
$new_handle = XML::unescape($data->profile->author); $old_author = WebFingerUri::fromString(XML::unescape($data->author));
$signature = XML::unescape($data->signature); $new_author = WebFingerUri::fromString(XML::unescape($data->profile->author));
} catch (\Throwable $e) {
$contact = self::contactByHandle($importer['uid'], $old_handle); Logger::notice('Cannot find handles for sender and user', ['data' => $data]);
if (!$contact) {
Logger::notice('Cannot find contact for sender: ' . $old_handle . ' and user ' . $importer['uid']);
return false; return false;
} }
Logger::notice('Got migration for ' . $old_handle . ', to ' . $new_handle . ' with user ' . $importer['uid']); $signature = XML::unescape($data->signature);
$contact = self::contactByHandle($importer['uid'], $old_author);
if (!$contact) {
Logger::notice('Cannot find contact for sender: ' . $old_author . ' and user ' . $importer['uid']);
return false;
}
Logger::notice('Got migration for ' . $old_author . ', to ' . $new_author . ' with user ' . $importer['uid']);
// Check signature // Check signature
$signed_text = 'AccountMigration:' . $old_handle . ':' . $new_handle; $signed_text = 'AccountMigration:' . $old_author . ':' . $new_author;
$key = self::key($old_handle); $key = self::key($old_author);
if (!Crypto::rsaVerify($signed_text, $signature, $key, 'sha256')) { if (!Crypto::rsaVerify($signed_text, $signature, $key, 'sha256')) {
Logger::notice('No valid signature for migration.'); Logger::notice('No valid signature for migration.');
return false; return false;
@ -1342,9 +1361,9 @@ class Diaspora
self::receiveProfile($importer, $data->profile); self::receiveProfile($importer, $data->profile);
// change the technical stuff in contact // change the technical stuff in contact
$data = Probe::uri($new_handle); $data = Probe::uri($new_author);
if ($data['network'] == Protocol::PHANTOM) { 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; return false;
} }
@ -1360,7 +1379,7 @@ class Diaspora
'network' => $data['network'], 'network' => $data['network'],
]; ];
Contact::update($fields, ['addr' => $old_handle]); Contact::update($fields, ['addr' => $old_author->getAddr()]);
Logger::notice('Contacts are updated.'); Logger::notice('Contacts are updated.');
@ -1377,15 +1396,15 @@ class Diaspora
*/ */
private static function receiveAccountDeletion(SimpleXMLElement $data): bool 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)) { while ($contact = DBA::fetch($contacts)) {
Contact::remove($contact['id']); Contact::remove($contact['id']);
} }
DBA::close($contacts); DBA::close($contacts);
Logger::notice('Removed contacts for ' . $author); Logger::notice('Removed contacts for ' . $author_handle);
return true; return true;
} }
@ -1393,21 +1412,20 @@ class Diaspora
/** /**
* Fetch the uri from our database if we already have this item (maybe from ourselves) * 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 string $guid Message guid
* @param boolean $onlyfound Only return uri when found in the database * @param WebFingerUri|null $person_uri Optional person to derive the base URL from
* *
* @return string The constructed uri or the one from our database or empty string on if $onlyfound is true * @return string The constructed uri or the one from our database or empty string
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
private static function getUriFromGuid(string $author, string $guid, bool $onlyfound = false): string private static function getUriFromGuid(string $guid, WebFingerUri $person_uri = null): string
{ {
$item = Post::selectFirst(['uri'], ['guid' => $guid]); $item = Post::selectFirst(['uri'], ['guid' => $guid]);
if (DBA::isResult($item)) { if (DBA::isResult($item)) {
return $item['uri']; return $item['uri'];
} elseif (!$onlyfound) { } elseif ($person_uri) {
$person = FContact::getByURL($author); $person = FContact::getByURL($person_uri);
$parts = parse_url($person['url']); $parts = parse_url($person['url']);
unset($parts['path']); unset($parts['path']);
@ -1457,18 +1475,18 @@ class Diaspora
* Processes an incoming comment * Processes an incoming comment
* *
* @param array $importer Array of the importer user * @param array $importer Array of the importer user
* @param string $sender The sender of the message * @param WebFingerUri $sender The sender of the message
* @param SimpleXMLElement $data The message object * @param SimpleXMLElement $data The message object
* @param string $xml The original XML of the message * @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 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 \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
private static function receiveComment(array $importer, string $sender, SimpleXMLElement $data, string $xml, int $direction): bool private static function receiveComment(array $importer, WebFingerUri $sender, SimpleXMLElement $data, string $xml, int $direction): bool
{ {
$author = XML::unescape($data->author); $author = WebFingerUri::fromString(XML::unescape($data->author));
$guid = XML::unescape($data->guid); $guid = XML::unescape($data->guid);
$parent_guid = XML::unescape($data->parent_guid); $parent_guid = XML::unescape($data->parent_guid);
$text = XML::unescape($data->text); $text = XML::unescape($data->text);
@ -1481,7 +1499,7 @@ class Diaspora
if (isset($data->thread_parent_guid)) { if (isset($data->thread_parent_guid)) {
$thread_parent_guid = XML::unescape($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 { } else {
$thr_parent = ''; $thr_parent = '';
} }
@ -1512,7 +1530,7 @@ class Diaspora
} }
// Fetch the contact id - if we know this contact // Fetch the contact id - if we know this contact
$author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
$datarray = []; $datarray = [];
@ -1530,7 +1548,7 @@ class Diaspora
$datarray = self::setDirection($datarray, $direction); $datarray = self::setDirection($datarray, $direction);
$datarray['guid'] = $guid; $datarray['guid'] = $guid;
$datarray['uri'] = self::getUriFromGuid($author, $guid); $datarray['uri'] = self::getUriFromGuid($guid, $author);
$datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
$datarray['verb'] = Activity::POST; $datarray['verb'] = Activity::POST;
@ -1601,16 +1619,16 @@ class Diaspora
*/ */
private static function receiveConversationMessage(array $importer, array $contact, SimpleXMLElement $data, array $msg, $mesg, array $conversation): bool 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); $guid = XML::unescape($data->guid);
$subject = XML::unescape($data->subject); $subject = XML::unescape($data->subject);
// "diaspora_handle" is the element name from the old version // "diaspora_handle" is the element name from the old version
// "author" is the element name from the new version // "author" is the element name from the new version
if ($mesg->author) { if ($mesg->author) {
$msg_author = XML::unescape($mesg->author); $msg_author_handle = XML::unescape($mesg->author);
} elseif ($mesg->diaspora_handle) { } elseif ($mesg->diaspora_handle) {
$msg_author = XML::unescape($mesg->diaspora_handle); $msg_author_handle = XML::unescape($mesg->diaspora_handle);
} else { } else {
return false; return false;
} }
@ -1626,9 +1644,8 @@ class Diaspora
} }
$body = Markdown::toBBCode($msg_text); $body = Markdown::toBBCode($msg_text);
$message_uri = $msg_author . ':' . $msg_guid;
$person = FContact::getByURL($msg_author); $person = FContact::getByURL($msg_author_handle);
return Mail::insert([ return Mail::insert([
'uid' => $importer['uid'], 'uid' => $importer['uid'],
@ -1640,8 +1657,8 @@ class Diaspora
'contact-id' => $contact['id'], 'contact-id' => $contact['id'],
'title' => $subject, 'title' => $subject,
'body' => $body, 'body' => $body,
'uri' => $message_uri, 'uri' => $msg_author_handle . ':' . $msg_guid,
'parent-uri' => $author . ':' . $guid, 'parent-uri' => $author_handle . ':' . $guid,
'created' => $msg_created_at 'created' => $msg_created_at
]); ]);
} }
@ -1658,7 +1675,7 @@ class Diaspora
*/ */
private static function receiveConversation(array $importer, array $msg, SimpleXMLElement $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); $guid = XML::unescape($data->guid);
$subject = XML::unescape($data->subject); $subject = XML::unescape($data->subject);
$created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
@ -1671,7 +1688,7 @@ class Diaspora
return false; return false;
} }
$contact = self::allowedContactByHandle($importer, $msg['author'], true); $contact = self::allowedContactByHandle($importer, WebFingerUri::fromString($msg['author']), true);
if (!$contact) { if (!$contact) {
return false; return false;
} }
@ -1685,7 +1702,7 @@ class Diaspora
$r = DBA::insert('conv', [ $r = DBA::insert('conv', [
'uid' => $importer['uid'], 'uid' => $importer['uid'],
'guid' => $guid, 'guid' => $guid,
'creator' => $author, 'creator' => $author_handle,
'created' => $created_at, 'created' => $created_at,
'updated' => DateTimeFormat::utcNow(), 'updated' => DateTimeFormat::utcNow(),
'subject' => $subject, 'subject' => $subject,
@ -1712,7 +1729,7 @@ class Diaspora
* Processes "like" messages * Processes "like" messages
* *
* @param array $importer Array of the importer user * @param array $importer Array of the importer user
* @param string $sender The sender of the message * @param WebFingerUri $sender The sender of the message
* @param SimpleXMLElement $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) * @param int $direction Indicates if the message had been fetched or pushed (self::PUSHED, self::FETCHED, self::FORCED_FETCH)
* *
@ -1720,9 +1737,9 @@ class Diaspora
* @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException * @throws \ImagickException
*/ */
private static function receiveLike(array $importer, string $sender, SimpleXMLElement $data, int $direction): bool private static function receiveLike(array $importer, WebFingerUri $sender, SimpleXMLElement $data, int $direction): bool
{ {
$author = XML::unescape($data->author); $author = WebFingerUri::fromString(XML::unescape($data->author));
$guid = XML::unescape($data->guid); $guid = XML::unescape($data->guid);
$parent_guid = XML::unescape($data->parent_guid); $parent_guid = XML::unescape($data->parent_guid);
$parent_type = XML::unescape($data->parent_type); $parent_type = XML::unescape($data->parent_type);
@ -1760,7 +1777,7 @@ class Diaspora
} }
// Fetch the contact id - if we know this contact // Fetch the contact id - if we know this contact
$author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
// "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora // "positive" = "false" would be a Dislike - wich isn't currently supported by Diaspora
// We would accept this anyhow. // We would accept this anyhow.
@ -1784,7 +1801,7 @@ class Diaspora
$datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0);
$datarray['guid'] = $guid; $datarray['guid'] = $guid;
$datarray['uri'] = self::getUriFromGuid($author, $guid); $datarray['uri'] = self::getUriFromGuid($guid, $author);
$datarray['verb'] = $verb; $datarray['verb'] = $verb;
$datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['gravity'] = Item::GRAVITY_ACTIVITY;
@ -1843,7 +1860,7 @@ class Diaspora
*/ */
private static function receiveMessage(array $importer, SimpleXMLElement $data): bool private static function receiveMessage(array $importer, SimpleXMLElement $data): bool
{ {
$author = XML::unescape($data->author); $author = WebFingerUri::fromString(XML::unescape($data->author));
$guid = XML::unescape($data->guid); $guid = XML::unescape($data->guid);
$conversation_guid = XML::unescape($data->conversation_guid); $conversation_guid = XML::unescape($data->conversation_guid);
$text = XML::unescape($data->text); $text = XML::unescape($data->text);
@ -1858,18 +1875,13 @@ class Diaspora
GServer::setProtocol($contact['gsid'], Post\DeliveryData::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); $conversation = DBA::selectFirst('conv', [], $condition);
if (!DBA::isResult($conversation)) { if (!DBA::isResult($conversation)) {
Logger::notice('Conversation not available.'); Logger::notice('Conversation not available.');
return false; return false;
} }
$message_uri = $author . ':' . $guid;
$person = FContact::getByURL($author); $person = FContact::getByURL($author);
if (!$person) { if (!$person) {
Logger::notice('Unable to find author details'); Logger::notice('Unable to find author details');
@ -1891,7 +1903,7 @@ class Diaspora
'title' => $conversation['subject'], 'title' => $conversation['subject'],
'body' => $body, 'body' => $body,
'reply' => 1, 'reply' => 1,
'uri' => $message_uri, 'uri' => $author . ':' . $guid,
'parent-uri' => $author . ':' . $conversation['guid'], 'parent-uri' => $author . ':' . $conversation['guid'],
'created' => $created_at 'created' => $created_at
]); ]);
@ -1910,7 +1922,7 @@ class Diaspora
*/ */
private static function receiveParticipation(array $importer, SimpleXMLElement $data, int $direction): bool 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); $guid = XML::unescape($data->guid);
$parent_guid = XML::unescape($data->parent_guid); $parent_guid = XML::unescape($data->parent_guid);
@ -1947,7 +1959,7 @@ class Diaspora
return false; return false;
} }
$author_contact = self::authorContactByUrl($contact, $person, $importer['uid']); $author_contact = self::authorContactByUrl($contact, $person['url'], $importer['uid']);
// Store participation // Store participation
$datarray = []; $datarray = [];
@ -1964,7 +1976,7 @@ class Diaspora
$datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0); $datarray['owner-id'] = $datarray['author-id'] = Contact::getIdForURL($person['url'], 0);
$datarray['guid'] = $guid; $datarray['guid'] = $guid;
$datarray['uri'] = self::getUriFromGuid($author, $guid); $datarray['uri'] = self::getUriFromGuid($guid, $author);
$datarray['verb'] = Activity::FOLLOW; $datarray['verb'] = Activity::FOLLOW;
$datarray['gravity'] = Item::GRAVITY_ACTIVITY; $datarray['gravity'] = Item::GRAVITY_ACTIVITY;
@ -2056,7 +2068,7 @@ class Diaspora
*/ */
private static function receiveProfile(array $importer, SimpleXMLElement $data): bool 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) { if (!$contact) {
@ -2084,16 +2096,13 @@ class Diaspora
$keywords = implode(', ', $keywords); $keywords = implode(', ', $keywords);
$handle_parts = explode('@', $author);
$nick = $handle_parts[0];
if ($name === '') { if ($name === '') {
$name = $handle_parts[0]; $name = $author->getUser();
} }
if (preg_match('|^https?://|', $image_url) === 0) { if (preg_match('|^https?://|', $image_url) === 0) {
// @TODO No HTTPS here? // @TODO No HTTPS here?
$image_url = 'http://' . $handle_parts[1] . $image_url; $image_url = 'http://' . $author->getFullHost() . $image_url;
} }
Contact::updateAvatar($contact['id'], $image_url); Contact::updateAvatar($contact['id'], $image_url);
@ -2115,7 +2124,7 @@ class Diaspora
$fields = ['name' => $name, 'location' => $location, $fields = ['name' => $name, 'location' => $location,
'name-date' => DateTimeFormat::utcNow(), 'about' => $about, 'name-date' => DateTimeFormat::utcNow(), 'about' => $about,
'addr' => $author, 'nick' => $nick, 'keywords' => $keywords, 'addr' => $author->getAddr(), 'nick' => $author->getUser(), 'keywords' => $keywords,
'unsearchable' => !$searchable, 'sensitive' => $nsfw]; 'unsearchable' => !$searchable, 'sensitive' => $nsfw];
if (!empty($birthday)) { if (!empty($birthday)) {
@ -2158,13 +2167,15 @@ class Diaspora
*/ */
private static function receiveContactRequest(array $importer, SimpleXMLElement $data): bool 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); $recipient = XML::unescape($data->recipient);
if (!$author || !$recipient) { if (!$author_handle || !$recipient) {
return false; return false;
} }
$author = WebFingerUri::fromString($author_handle);
// the current protocol version doesn't know these fields // the current protocol version doesn't know these fields
// That means that we will assume their existance // That means that we will assume their existance
if (isset($data->following)) { if (isset($data->following)) {
@ -2265,9 +2276,12 @@ class Diaspora
* @param array $item Array of reshare post * @param array $item Array of reshare post
* @param integer $parent_message_id Id of the parent post * @param integer $parent_message_id Id of the parent post
* @param string $guid GUID string of reshare action * @param string $guid GUID string of reshare action
* @param string $author Author handle * @param WebFingerUri $author Author handle
* @return false|void
* @throws InternalServerErrorException
* @throws \ImagickException
*/ */
private static function addReshareActivity(array $item, int $parent_message_id, string $guid, string $author) private static function addReshareActivity(array $item, int $parent_message_id, string $guid, WebFingerUri $author)
{ {
$parent = Post::selectFirst(['uri', 'guid'], ['id' => $parent_message_id]); $parent = Post::selectFirst(['uri', 'guid'], ['id' => $parent_message_id]);
@ -2284,7 +2298,7 @@ class Diaspora
$datarray['owner-id'] = $datarray['author-id']; $datarray['owner-id'] = $datarray['author-id'];
$datarray['guid'] = $parent['guid'] . '-' . $guid; $datarray['guid'] = $parent['guid'] . '-' . $guid;
$datarray['uri'] = self::getUriFromGuid($author, $datarray['guid']); $datarray['uri'] = self::getUriFromGuid($datarray['guid'], $author);
$datarray['thr-parent'] = $parent['uri']; $datarray['thr-parent'] = $parent['uri'];
$datarray['verb'] = $datarray['body'] = Activity::ANNOUNCE; $datarray['verb'] = $datarray['body'] = Activity::ANNOUNCE;
@ -2329,7 +2343,7 @@ class Diaspora
*/ */
private static function receiveReshare(array $importer, SimpleXMLElement $data, string $xml, int $direction): bool 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); $guid = XML::unescape($data->guid);
$created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
$root_author = XML::unescape($data->root_author); $root_author = XML::unescape($data->root_author);
@ -2337,7 +2351,7 @@ class Diaspora
/// @todo handle unprocessed property "provider_display_name" /// @todo handle unprocessed property "provider_display_name"
$public = XML::unescape($data->public); $public = XML::unescape($data->public);
$contact = self::allowedContactByHandle($importer, $author, false); $contact = self::allowedContactByHandle($importer, $author);
if (!$contact) { if (!$contact) {
return false; return false;
} }
@ -2369,7 +2383,7 @@ class Diaspora
$datarray['owner-id'] = $datarray['author-id']; $datarray['owner-id'] = $datarray['author-id'];
$datarray['guid'] = $guid; $datarray['guid'] = $guid;
$datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid); $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author);
$datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
$datarray['verb'] = Activity::POST; $datarray['verb'] = Activity::POST;
@ -2448,13 +2462,13 @@ class Diaspora
*/ */
private static function itemRetraction(array $importer, array $contact, SimpleXMLElement $data): bool private static function itemRetraction(array $importer, array $contact, SimpleXMLElement $data): bool
{ {
$author = XML::unescape($data->author); $author_handle = XML::unescape($data->author);
$target_guid = XML::unescape($data->target_guid); $target_guid = XML::unescape($data->target_guid);
$target_type = XML::unescape($data->target_type); $target_type = XML::unescape($data->target_type);
$person = FContact::getByURL($author); $person = FContact::getByURL($author_handle);
if (!is_array($person)) { if (!is_array($person)) {
Logger::notice('Unable to find author detail for ' . $author); Logger::notice('Unable to find author detail for ' . $author_handle);
return false; return false;
} }
@ -2506,13 +2520,13 @@ class Diaspora
* Receives retraction messages * Receives retraction messages
* *
* @param array $importer Array of the importer user * @param array $importer Array of the importer user
* @param string $sender The sender of the message * @param WebFingerUri $sender The sender of the message
* @param SimpleXMLElement $data The message object * @param SimpleXMLElement $data The message object
* *
* @return bool Success * @return bool Success
* @throws \Exception * @throws \Exception
*/ */
private static function receiveRetraction(array $importer, string $sender, SimpleXMLElement $data) private static function receiveRetraction(array $importer, WebFingerUri $sender, SimpleXMLElement $data)
{ {
$target_type = XML::unescape($data->target_type); $target_type = XML::unescape($data->target_type);
@ -2639,14 +2653,14 @@ class Diaspora
*/ */
private static function receiveStatusMessage(array $importer, SimpleXMLElement $data, string $xml, int $direction) 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); $guid = XML::unescape($data->guid);
$created_at = DateTimeFormat::utc(XML::unescape($data->created_at)); $created_at = DateTimeFormat::utc(XML::unescape($data->created_at));
$public = XML::unescape($data->public); $public = XML::unescape($data->public);
$text = XML::unescape($data->text); $text = XML::unescape($data->text);
$provider_display_name = XML::unescape($data->provider_display_name); $provider_display_name = XML::unescape($data->provider_display_name);
$contact = self::allowedContactByHandle($importer, $author, false); $contact = self::allowedContactByHandle($importer, $author);
if (!$contact) { if (!$contact) {
return false; return false;
} }
@ -2672,7 +2686,7 @@ class Diaspora
$datarray = []; $datarray = [];
$datarray['guid'] = $guid; $datarray['guid'] = $guid;
$datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($author, $guid); $datarray['uri'] = $datarray['thr-parent'] = self::getUriFromGuid($guid, $author);
$datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]); $datarray['uri-id'] = ItemURI::insert(['uri' => $datarray['uri'], 'guid' => $datarray['guid']]);
// Attach embedded pictures to the body // Attach embedded pictures to the body
@ -3089,16 +3103,16 @@ class Diaspora
$owner = User::getOwnerDataById($item['uid']); $owner = User::getOwnerDataById($item['uid']);
} }
$author = self::myHandle($owner); $author_handle = self::myHandle($owner);
$message = [ $message = [
'author' => $author, 'author' => $author_handle,
'guid' => System::createUUID(), 'guid' => System::createUUID(),
'parent_type' => 'Post', 'parent_type' => 'Post',
'parent_guid' => $item['guid'] '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 // 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);

View file

@ -0,0 +1,113 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Protocol;
use GuzzleHttp\Psr7\Uri;
class WebFingerUri
{
/**
* @var string
*/
private $user;
/**
* @var string
*/
private $host;
/**
* @var int|null
*/
private $port;
/**
* @var string|null
*/
private $path;
private function __construct(string $user, string $host, int $port = null, string $path = null)
{
$this->user = $user;
$this->host = $host;
$this->port = $port;
$this->path = $path;
$this->validate();
}
/**
* @param string $addr
* @return WebFingerUri
*/
public static function fromString(string $addr): WebFingerUri
{
$uri = new Uri('acct://' . preg_replace('/^acct:/', '', $addr));
return new self($uri->getUserInfo(), $uri->getHost(), $uri->getPort(), $uri->getPath());
}
private function validate()
{
if (!$this->user) {
throw new \InvalidArgumentException('WebFinger URI User part is required');
}
if (!$this->host) {
throw new \InvalidArgumentException('WebFinger URI Host part is required');
}
}
public function getUser(): string
{
return $this->user;
}
public function getHost(): string
{
return $this->host;
}
public function getFullHost(): string
{
return $this->host
. ($this->port ? ':' . $this->port : '') .
($this->path ?: '');
}
public function getLongForm(): string
{
return 'acct:' . $this->getShortForm();
}
public function getShortForm(): string
{
return $this->user . '@' . $this->getFullHost();
}
public function getAddr(): string
{
return $this->getShortForm();
}
public function __toString(): string
{
return $this->getShortForm();
}
}

View file

@ -0,0 +1,135 @@
<?php
/**
* @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Main database structure configuration file.
*
* Here are described all the tables, fields and indexes Friendica needs to work.
* The entry order is mostly alphabetic - with the exception of tables that are used in foreign keys.
*
* Syntax (braces indicate optionale values):
* "<table name>" => [
* "comment" => "Description of the table",
* "fields" => [
* "<field name>" => [
* "type" => "<field type>{(<field size>)} <unsigned>",
* "not null" => 0|1,
* {"extra" => "auto_increment",}
* {"default" => "<default value>",}
* {"default" => NULL_DATE,} (for datetime fields)
* {"primary" => "1",}
* {"foreign|relation" => ["<foreign key table name>" => "<foreign key field name>"],}
* "comment" => "Description of the fields"
* ],
* ...
* ],
* "indexes" => [
* "PRIMARY" => ["<primary key field name>", ...],
* "<index name>" => [{"UNIQUE",} "<field name>{(<key size>)}", ...]
* ...
* ],
* ],
*
* Whenever possible prefer "foreign" before "relation" with the foreign keys.
* "foreign" adds true foreign keys on the database level, while "relation" is just an indicator of a table relation without any consequences
*
* If you need to make any change, make sure to increment the DB_UPDATE_VERSION constant value below.
*
*/
namespace Friendica\Test\src\Protocol;
use Friendica\Protocol\WebFingerUri;
use PHPUnit\Framework\TestCase;
class WebFingerUriTest extends TestCase
{
public function dataFromString(): array
{
return [
'long' => [
'expectedLong' => 'acct:selma@www.example.com:8080/friend',
'expectedShort' => 'selma@www.example.com:8080/friend',
'input' => 'acct:selma@www.example.com:8080/friend',
],
'short' => [
'expectedLong' => 'acct:selma@www.example.com:8080/friend',
'expectedShort' => 'selma@www.example.com:8080/friend',
'input' => 'selma@www.example.com:8080/friend',
],
'minimal' => [
'expectedLong' => 'acct:bob@example.com',
'expectedShort' => 'bob@example.com',
'input' => 'bob@example.com',
],
'acct:' => [
'expectedLong' => 'acct:alice@example.acct:90',
'expectedShort' => 'alice@example.acct:90',
'input' => 'alice@example.acct:90',
],
];
}
/**
* @dataProvider dataFromString
* @param string $expectedLong
* @param string $expectedShort
* @param string $input
* @return void
*/
public function testFromString(string $expectedLong, string $expectedShort, string $input)
{
$uri = WebFingerUri::fromString($input);
$this->assertEquals($expectedLong, $uri->getLongForm());
$this->assertEquals($expectedShort, $uri->getShortForm());
}
public function dataFromStringFailure()
{
return [
'missing user' => [
'input' => 'example.com',
],
'missing user @' => [
'input' => '@example.com',
],
'missing host' => [
'input' => 'alice',
],
'missing host @' => [
'input' => 'alice@',
],
'missing everything' => [
'input' => '',
],
];
}
/**
* @dataProvider dataFromStringFailure
* @param string $input
* @return void
*/
public function testFromStringFailure(string $input)
{
$this->expectException(\InvalidArgumentException::class);
WebFingerUri::fromString($input);
}
}