namespace Friendica\Protocol\ActivityPub;
use Friendica\Database\DBA;
-use Friendica\Util\HTTPSignature;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Model\Contact;
use Friendica\Model\APContact;
+use Friendica\Model\Conversation;
use Friendica\Model\Item;
use Friendica\Model\User;
-use Friendica\Util\JsonLD;
-use Friendica\Util\LDSignature;
use Friendica\Protocol\ActivityPub;
-use Friendica\Model\Conversation;
use Friendica\Util\DateTimeFormat;
+use Friendica\Util\HTTPSignature;
+use Friendica\Util\JsonLD;
+use Friendica\Util\LDSignature;
+use Friendica\Util\Strings;
/**
* @brief ActivityPub Receiver Protocol class
/**
* Checks if the web request is done for the AP protocol
*
- * @return is it AP?
+ * @return bool is it AP?
*/
public static function isRequest()
{
/**
* Checks incoming message from the inbox
*
- * @param $body
- * @param $header
+ * @param $body
+ * @param $header
* @param integer $uid User ID
+ * @throws \Exception
*/
public static function processInbox($body, $header, $uid)
{
*
* @param array $activity
* @param string $object_id Object ID of the the provided object
- * @param integer $uid User ID
+ * @param integer $uid User ID
*
* @return string with object type
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
private static function fetchObjectType($activity, $object_id, $uid = 0)
{
/**
* Prepare the object array
*
- * @param array $activity
+ * @param array $activity
* @param integer $uid User ID
- * @param $trust_source
+ * @param $trust_source
*
* @return array with object data
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
private static function prepareObjectData($activity, $uid, &$trust_source)
{
}
// We had been able to retrieve the object data - so we can trust the source
$trust_source = true;
- } elseif (in_array($type, ['as:Like', 'as:Dislike'])) {
+ } elseif (in_array($type, ['as:Like', 'as:Dislike']) ||
+ (($type == 'as:Follow') && in_array($object_type, self::CONTENT_TYPES))) {
// Create a mostly empty array out of the activity data (instead of the object).
// This way we later don't have to check for the existence of ech individual array element.
$object_data = self::processObject($activity);
$object_data['type'] = $type;
$object_data['actor'] = $actor;
+ $object_data['item_receiver'] = $receivers;
$object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers);
Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG);
*
* @param array $activity Array with activity data
* @param string $body The raw message
+ * @throws \Exception
*/
private static function storeConversation($activity, $body)
{
* @param string $body
* @param integer $uid User ID
* @param boolean $trust_source Do we trust the source?
+ * @throws \Exception
*/
public static function processActivity($activity, $body = '', $uid = null, $trust_source = false)
{
}
+ // Don't trust the source if "actor" differs from "attributedTo". The content could be forged.
+ if ($trust_source && ($type == 'as:Create') && is_array($activity['as:object'])) {
+ $actor = JsonLD::fetchElement($activity, 'as:actor');
+ $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo');
+ $trust_source = ($actor == $attributed_to);
+ if (!$trust_source) {
+ Logger::log('Not trusting actor: ' . $actor . '. It differs from attributedTo: ' . $attributed_to, Logger::DEBUG);
+ }
+ }
+
// $trust_source is called by reference and is set to true if the content was retrieved successfully
$object_data = self::prepareObjectData($activity, $uid, $trust_source);
if (empty($object_data)) {
return;
}
- self::storeConversation($object_data, $body);
+ // Only store content related stuff - and no announces, since they possibly overwrite the original content
+ if (in_array($object_data['object_type'], self::CONTENT_TYPES) && ($type != 'as:Announce')) {
+ self::storeConversation($object_data, $body);
+ }
// Internal flag for thread completion. See Processor.php
if (!empty($activity['thread-completion'])) {
if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
ActivityPub\Processor::updateItem($object_data);
} elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
- ActivityPub\Processor::updatePerson($object_data, $body);
+ ActivityPub\Processor::updatePerson($object_data);
}
break;
case 'as:Delete':
if ($object_data['object_type'] == 'as:Tombstone') {
- ActivityPub\Processor::deleteItem($object_data, $body);
+ ActivityPub\Processor::deleteItem($object_data);
} elseif (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
- ActivityPub\Processor::deletePerson($object_data, $body);
+ ActivityPub\Processor::deletePerson($object_data);
}
break;
case 'as:Follow':
if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) {
ActivityPub\Processor::followUser($object_data);
+ } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
+ $object_data['reply-to-id'] = $object_data['object_id'];
+ ActivityPub\Processor::createActivity($object_data, ACTIVITY_FOLLOW);
}
break;
/**
* Fetch the receiver list from an activity array
*
- * @param array $activity
+ * @param array $activity
* @param string $actor
- * @param array $tags
+ * @param array $tags
*
* @return array with receivers (user id)
+ * @throws \Exception
*/
private static function getReceivers($activity, $actor, $tags = [])
{
if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) {
// This will most likely catch all OStatus connections to Mastodon
- $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]
+ $condition = ['alias' => [$actor, Strings::normaliseLink($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]
, 'archive' => false, 'pending' => false];
$contacts = DBA::select('contact', ['uid'], $condition);
while ($contact = DBA::fetch($contacts)) {
}
// Fetching all directly addressed receivers
- $condition = ['self' => true, 'nurl' => normalise_link($receiver)];
+ $condition = ['self' => true, 'nurl' => Strings::normaliseLink($receiver)];
$contact = DBA::selectFirst('contact', ['uid', 'contact-type'], $condition);
if (!DBA::isResult($contact)) {
continue;
// Check if the potential receiver is following the actor
// Exception: The receiver is targetted via "to" or this is a comment
- if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY)) {
+ if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) {
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
- $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
+ $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND],
'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']];
// Forum posts are only accepted from forum contacts
- if ($contact['contact-type'] == Contact::ACCOUNT_TYPE_COMMUNITY) {
+ if ($contact['contact-type'] == Contact::TYPE_COMMUNITY) {
$condition['rel'] = [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER];
}
* Fetch the receiver list of a given actor
*
* @param string $actor
- * @param array $tags
+ * @param array $tags
*
* @return array with receivers (user id)
+ * @throws \Exception
*/
public static function getReceiverForActor($actor, $tags)
{
$receivers = [];
$networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS];
- $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
+ $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER],
'network' => $networks, 'archive' => false, 'pending' => false];
$contacts = DBA::select('contact', ['uid', 'rel'], $condition);
while ($contact = DBA::fetch($contacts)) {
/**
* Tests if the contact is a valid receiver for this actor
*
- * @param array $contact
+ * @param array $contact
* @param string $actor
- * @param array $tags
+ * @param array $tags
*
- * @return array with receivers (user id)
+ * @return bool with receivers (user id)
+ * @throws \Exception
*/
private static function isValidReceiverForActor($contact, $actor, $tags)
{
// When the possible receiver isn't a community, then it is no valid receiver
$owner = User::getOwnerDataById($contact['uid']);
- if (empty($owner) || ($owner['contact-type'] != Contact::ACCOUNT_TYPE_COMMUNITY)) {
+ if (empty($owner) || ($owner['contact-type'] != Contact::TYPE_COMMUNITY)) {
return false;
}
*
* @param integer $cid Contact ID
* @param integer $uid User ID
- * @param string $url Profile URL
+ * @param string $url Profile URL
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
public static function switchContact($cid, $uid, $url)
{
$photo = defaults($profile, 'photo', null);
unset($profile['photo']);
unset($profile['baseurl']);
+ unset($profile['guid']);
- $profile['nurl'] = normalise_link($profile['url']);
+ $profile['nurl'] = Strings::normaliseLink($profile['url']);
DBA::update('contact', $profile, ['id' => $cid]);
Contact::updateAvatar($photo, $uid, $cid);
*
* @param $receivers
* @param $actor
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
private static function switchContacts($receivers, $actor)
{
}
foreach ($receivers as $receiver) {
- $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => normalise_link($actor)]);
+ $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]);
if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver, $actor);
}
- $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [normalise_link($actor), $actor]]);
+ $contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver, 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]);
if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver, $actor);
}
/**
*
*
- * @param $object_data
+ * @param $object_data
* @param array $activity
*
- * @return
+ * @return mixed
*/
private static function addActivityFields($object_data, $activity)
{
* @param integer $uid User ID for the signature that we use to fetch data
*
* @return array with trusted and valid object data
+ * @throws \Friendica\Network\HTTPException\InternalServerErrorException
+ * @throws \ImagickException
*/
private static function fetchObject($object_id, $object = [], $trust_source = false, $uid = 0)
{
continue;
}
- $taglist[] = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')),
+ $element = ['type' => str_replace('as:', '', JsonLD::fetchElement($tag, '@type')),
'href' => JsonLD::fetchElement($tag, 'as:href'),
'name' => JsonLD::fetchElement($tag, 'as:name')];
+
+ if (empty($element['type'])) {
+ continue;
+ }
+
+ $taglist[] = $element;
}
return $taglist;
}
+ /**
+ * Convert emojis from JSON-LD format into a simplified format
+ *
+ * @param $emojis
+ * @return array with emojis in a simplified format
+ */
+ private static function processEmojis($emojis)
+ {
+ $emojilist = [];
+
+ if (empty($emojis)) {
+ return [];
+ }
+
+ foreach ($emojis as $emoji) {
+ if (empty($emoji) || (JsonLD::fetchElement($emoji, '@type') != 'toot:Emoji') || empty($emoji['as:icon'])) {
+ continue;
+ }
+
+ $url = JsonLD::fetchElement($emoji['as:icon'], 'as:url');
+ $element = ['name' => JsonLD::fetchElement($emoji, 'as:name'),
+ 'href' => $url];
+
+ $emojilist[] = $element;
+ }
+ return $emojilist;
+ }
+
/**
* Convert attachments from JSON-LD format into a simplified format
*
* @param array $object
*
* @return array
+ * @throws \Exception
*/
private static function processObject($object)
{
$object_data = [];
$object_data['object_type'] = JsonLD::fetchElement($object, '@type');
$object_data['id'] = JsonLD::fetchElement($object, '@id');
-
$object_data['reply-to-id'] = JsonLD::fetchElement($object, 'as:inReplyTo');
- if (empty($object_data['reply-to-id'])) {
+ // An empty "id" field is translated to "./" by the compactor, so we have to check for this content
+ if (empty($object_data['reply-to-id']) || ($object_data['reply-to-id'] == './')) {
$object_data['reply-to-id'] = $object_data['id'];
}
$object_data['longitude'] = JsonLD::fetchElement($object_data, 'longitude', '@value');
$object_data['attachments'] = self::processAttachments(JsonLD::fetchElementArray($object, 'as:attachment'));
$object_data['tags'] = self::processTags(JsonLD::fetchElementArray($object, 'as:tag'));
+ $object_data['emojis'] = self::processEmojis(JsonLD::fetchElementArray($object, 'as:tag', 'toot:Emoji'));
$object_data['generator'] = JsonLD::fetchElement($object, 'as:generator', 'as:name', '@type', 'as:Application');
$object_data['alternate-url'] = JsonLD::fetchElement($object, 'as:url');