From 0a5476591d99af6155dff047795a06da6eb9fc62 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 10 Sep 2018 21:07:25 +0000 Subject: [PATCH 001/261] Activitity pub - first commit with much test code --- src/Module/Inbox.php | 47 +++ src/Protocol/ActivityPub.php | 772 +++++++++++++++++++++++++++++++++++ 2 files changed, 819 insertions(+) create mode 100644 src/Module/Inbox.php create mode 100644 src/Protocol/ActivityPub.php diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php new file mode 100644 index 0000000000..86fdae7c90 --- /dev/null +++ b/src/Module/Inbox.php @@ -0,0 +1,47 @@ + $_SERVER, 'body' => $obj])); + + logger('Blubb: init ' . $tempfile); + exit(); +// goaway($dest); + } + + public static function post() + { + $a = self::getApp(); + + logger('Blubb: post'); + exit(); +// goaway($dest); + } +} diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php new file mode 100644 index 0000000000..f1d885b6c6 --- /dev/null +++ b/src/Protocol/ActivityPub.php @@ -0,0 +1,772 @@ +get_curl_code(); +echo $return_code."\n"; + print_r(BaseObject::getApp()->get_curl_headers()); + print_r($headers); + } + + /** + * Return the ActivityPub profile of the given user + * + * @param integer $uid User ID + * @return array + */ + public static function profile($uid) + { + $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application']; + + $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false, + 'account_removed' => false, 'verified' => true]; + $fields = ['guid', 'nickname', 'pubkey', 'account-type']; + $user = DBA::selectFirst('user', $fields, $condition); + if (!DBA::isResult($user)) { + return []; + } + + $fields = ['locality', 'region', 'country-name']; + $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]); + if (!DBA::isResult($profile)) { + return []; + } + + $fields = ['name', 'url', 'location', 'about', 'avatar']; + $contact = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]); + if (!DBA::isResult($contact)) { + return []; + } + + $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', + ['uuid' => 'http://schema.org/identifier', 'sensitive' => 'as:sensitive', + 'vcard' => 'http://www.w3.org/2006/vcard/ns#']]]; + + $data['id'] = $contact['url']; + $data['uuid'] = $user['guid']; + $data['type'] = $accounttype[$user['account-type']]; + $data['following'] = System::baseUrl() . '/following/' . $user['nickname']; + $data['followers'] = System::baseUrl() . '/followers/' . $user['nickname']; + $data['inbox'] = System::baseUrl() . '/inbox/' . $user['nickname']; + $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname']; + $data['preferredUsername'] = $user['nickname']; + $data['name'] = $contact['name']; + $data['vcard:hasAddress'] = ['@type' => 'Home', 'vcard:country-name' => $profile['country-name'], + 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; + $data['summary'] = $contact['about']; + $data['url'] = $contact['url']; + $data['manuallyApprovesFollowers'] = false; + $data['publicKey'] = ['id' => $contact['url'] . '#main-key', + 'owner' => $contact['url'], + 'publicKeyPem' => $user['pubkey']]; + $data['endpoints'] = ['sharedInbox' => System::baseUrl() . '/inbox']; + $data['icon'] = ['type' => 'Image', + 'url' => $contact['avatar']]; + + // tags: https://kitty.town/@inmysocks/100656097926961126.json + return $data; + } + + public static function createActivityFromItem($item_id) + { + $item = Item::selectFirst([], ['id' => $item_id]); + + $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', + ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', + 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', + 'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', + 'toot' => 'http://joinmastodon.org/ns#']]]; + + $data['type'] = 'Create'; + $data['id'] = $item['plink']; + $data['actor'] = $item['author-link']; + $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; + $data['object'] = self::createNote($item); +// print_r($data); +// print_r($item); + return $data; + } + + public static function createNote($item) + { + $data = []; + $data['type'] = 'Note'; + $data['id'] = $item['plink']; + //$data['context'] = $data['conversation'] = $item['parent-uri']; + $data['actor'] = $item['author-link']; +// if (!$item['private']) { +// $data['to'] = []; +// $data['to'][] = '"https://pleroma.soykaf.com/users/heluecht"'; + $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; +// $data['cc'] = 'https://pleroma.soykaf.com/users/heluecht'; +// } + $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); + $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); + $data['attributedTo'] = $item['author-link']; + $data['title'] = BBCode::convert($item['title'], false, 7); + $data['content'] = BBCode::convert($item['body'], false, 7); + //$data['summary'] = ''; + //$data['sensitive'] = false; + //$data['emoji'] = []; + //$data['tag'] = []; + //$data['attachment'] = []; + return $data; + } + + /** + * Fetches ActivityPub content from the given url + * + * @param string $url content url + * @return array + */ + public static function fetchContent($url) + { + $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json']); + + if (!$ret['success'] || empty($ret['body'])) { + return; + } + + return json_decode($ret['body'], true); + } + + /** + * Resolves the profile url from the address by using webfinger + * + * @param string $addr profile address (user@domain.tld) + * @return string url + */ + private static function addrToUrl($addr) + { + $addr_parts = explode('@', $addr); + if (count($addr_parts) != 2) { + return false; + } + + $webfinger = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr); + + $ret = Network::curl($webfinger, false, $redirects, ['accept_content' => 'application/jrd+json,application/json']); + if (!$ret['success'] || empty($ret['body'])) { + return false; + } + + $data = json_decode($ret['body'], true); + + if (empty($data['links'])) { + return false; + } + + foreach ($data['links'] as $link) { + if (empty($link['href']) || empty($link['rel']) || empty($link['type'])) { + continue; + } + + if (($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) { + return $link['href']; + } + } + + return false; + } + + /** + * Fetches a profile from the given url + * + * @param string $url profile url + * @return array + */ + public static function fetchProfile($url) + { + if (empty(parse_url($url, PHP_URL_SCHEME))) { + $url = self::addrToUrl($url); + if (empty($url)) { + return false; + } + } + + $data = self::fetchContent($url); + + if (empty($data) || empty($data['id']) || empty($data['inbox'])) { + return false; + } + + $profile = ['network' => Protocol::ACTIVITYPUB]; + $profile['nick'] = $data['preferredUsername']; + $profile['name'] = defaults($data, 'name', $profile['nick']); + $profile['guid'] = defaults($data, 'uuid', null); + $profile['url'] = $data['id']; + $profile['alias'] = self::processElement($data, 'url', 'href'); + + $parts = parse_url($profile['url']); + unset($parts['scheme']); + unset($parts['path']); + $profile['addr'] = $profile['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); + + $profile['photo'] = self::processElement($data, 'icon', 'url'); + $profile['about'] = defaults($data, 'summary', ''); + $profile['batch'] = self::processElement($data, 'endpoints', 'sharedInbox'); + $profile['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); + $profile['notify'] = $data['inbox']; + $profile['poll'] = $data['outbox']; + + // Check if the address is resolvable + if (self::addrToUrl($profile['addr']) == $profile['url']) { + $parts = parse_url($profile['url']); + unset($parts['path']); + $profile['baseurl'] = Network::unparseURL($parts); + } else { + unset($profile['addr']); + } + + if ($profile['url'] == $profile['alias']) { + unset($profile['alias']); + } + + // Remove all "null" fields + foreach ($profile as $field => $content) { + if (is_null($content)) { + unset($profile[$field]); + } + } + + // Handled + unset($data['id']); + unset($data['inbox']); + unset($data['outbox']); + unset($data['preferredUsername']); + unset($data['name']); + unset($data['summary']); + unset($data['url']); + unset($data['publicKey']); + unset($data['endpoints']); + unset($data['icon']); + unset($data['uuid']); + + // To-Do + unset($data['type']); + unset($data['manuallyApprovesFollowers']); + + // Unhandled + unset($data['@context']); + unset($data['tag']); + unset($data['attachment']); + unset($data['image']); + unset($data['nomadicLocations']); + unset($data['signature']); + unset($data['following']); + unset($data['followers']); + unset($data['featured']); + unset($data['movedTo']); + unset($data['liked']); + unset($data['sharedInbox']); // Misskey + unset($data['isCat']); // Misskey + unset($data['kroeg:blocks']); // Kroeg + unset($data['updated']); // Kroeg + +/* if (!empty($data)) { + print_r($data); + die(); + } +*/ + return $profile; + } + + public static function fetchOutbox($url) + { + $data = self::fetchContent($url); + if (empty($data)) { + return; + } + + if (!empty($data['orderedItems'])) { + $items = $data['orderedItems']; + } elseif (!empty($data['first']['orderedItems'])) { + $items = $data['first']['orderedItems']; + } elseif (!empty($data['first'])) { + self::fetchOutbox($data['first']); + return; + } else { + $items = []; + } + + foreach ($items as $activity) { + self::processActivity($activity, $url); + } + } + + function processActivity($activity, $url) + { + if (empty($activity['type'])) { + return; + } + + if (empty($activity['object'])) { + return; + } + + if (empty($activity['actor'])) { + return; + } + + $actor = self::processElement($activity, 'actor', 'id'); + if (empty($actor)) { + return; + } + + if (is_string($activity['object'])) { + $object_url = $activity['object']; + } elseif (!empty($activity['object']['id'])) { + $object_url = $activity['object']['id']; + } else { + return; + } + + $receivers = self::getReceivers($activity); + if (empty($receivers)) { + return; + } + + // ---------------------------------- + // unhandled + unset($activity['@context']); + unset($activity['id']); + + // Non standard + unset($activity['title']); + unset($activity['atomUri']); + unset($activity['context_id']); + unset($activity['statusnetConversationId']); + + $structure = $activity; + + // To-Do? + unset($activity['context']); + unset($activity['location']); + + // handled + unset($activity['to']); + unset($activity['cc']); + unset($activity['bto']); + unset($activity['bcc']); + unset($activity['type']); + unset($activity['actor']); + unset($activity['object']); + unset($activity['published']); + unset($activity['updated']); + unset($activity['instrument']); + unset($activity['inReplyTo']); + + if (!empty($activity)) { + echo "Activity\n"; + print_r($activity); + die($url."\n"); + } + + $activity = $structure; + // ---------------------------------- + + $item = self::fetchObject($object_url, $url); + if (empty($item)) { + return; + } + + $item = self::addActivityFields($item, $activity); + + $item['owner'] = $actor; + + $item['receiver'] = array_merge($item['receiver'], $receivers); + + switch ($activity['type']) { + case 'Create': + case 'Update': + self::createItem($item); + break; + + case 'Announce': + self::announceItem($item); + break; + + case 'Like': + case 'Dislike': + self::activityItem($item); + break; + + case 'Follow': + break; + + default: + echo "Unknown activity: ".$activity['type']."\n"; + print_r($item); + die(); + break; + } + } + + private static function getReceivers($activity) + { + $receivers = []; + + $elements = ['to', 'cc', 'bto', 'bcc']; + foreach ($elements as $element) { + if (empty($activity[$element])) { + continue; + } + + // The receiver can be an arror or a string + if (is_string($activity[$element])) { + $activity[$element] = [$activity[$element]]; + } + + foreach ($activity[$element] as $receiver) { + if ($receiver == self::PUBLIC) { + $receivers[$receiver] = 0; + } + + $condition = ['self' => true, 'nurl' => normalise_link($receiver)]; + $contact = DBA::selectFirst('contact', ['id'], $condition); + if (!DBA::isResult($contact)) { + continue; + } + $receivers[$receiver] = $contact['id']; + } + } + return $receivers; + } + + private static function addActivityFields($item, $activity) + { + if (!empty($activity['published']) && empty($item['published'])) { + $item['published'] = $activity['published']; + } + + if (!empty($activity['updated']) && empty($item['updated'])) { + $item['updated'] = $activity['updated']; + } + + if (!empty($activity['inReplyTo']) && empty($item['parent-uri'])) { + $item['parent-uri'] = self::processElement($activity, 'inReplyTo', 'id'); + } + + if (!empty($activity['instrument'])) { + $item['service'] = self::processElement($activity, 'instrument', 'name', 'Service'); + } + + // Remove all "null" fields + foreach ($item as $field => $content) { + if (is_null($content)) { + unset($item[$field]); + } + } + + return $item; + } + + private static function fetchObject($object_url, $url) + { + $data = self::fetchContent($object_url); + if (empty($data)) { + return false; + } + + if (empty($data['type'])) { + return false; + } else { + $type = $data['type']; + } + + if (in_array($type, ['Note', 'Article', 'Video'])) { + $common = self::processCommonData($data, $url); + } + + switch ($type) { + case 'Note': + return array_merge($common, self::processNote($data, $url)); + case 'Article': + return array_merge($common, self::processArticle($data, $url)); + case 'Video': + return array_merge($common, self::processVideo($data, $url)); + + case 'Announce': + if (empty($data['object'])) { + return false; + } + return self::fetchObject($data['object'], $url); + + case 'Person': + case 'Tombstone': + break; + + default: + echo "Unknown object type: ".$data['type']."\n"; + print_r($data); + die($url."\n"); + break; + } + } + + private static function processCommonData(&$object, $url) + { + if (empty($object['id']) || empty($object['attributedTo'])) { + return false; + } + + $item = []; + $item['uri'] = $object['id']; + + if (!empty($object['inReplyTo'])) { + $item['reply-to-uri'] = self::processElement($object, 'inReplyTo', 'id'); + } else { + $item['reply-to-uri'] = $item['uri']; + } + + $item['published'] = defaults($object, 'published', null); + $item['updated'] = defaults($object, 'updated', $item['published']); + + if (empty($item['published']) && !empty($item['updated'])) { + $item['published'] = $item['updated']; + } + + $item['uuid'] = defaults($object, 'uuid', null); + $item['owner'] = $item['author'] = self::processElement($object, 'attributedTo', 'id'); + $item['context'] = defaults($object, 'context', null); + $item['conversation'] = defaults($object, 'conversation', null); + $item['sensitive'] = defaults($object, 'sensitive', null); + $item['name'] = defaults($object, 'name', null); + $item['title'] = defaults($object, 'title', null); + $item['content'] = defaults($object, 'content', null); + $item['summary'] = defaults($object, 'summary', null); + $item['location'] = self::processElement($object, 'location', 'name', 'Place'); + $item['attachments'] = defaults($object, 'attachment', null); + $item['tags'] = defaults($object, 'tag', null); + $item['service'] = self::processElement($object, 'instrument', 'name', 'Service'); + $item['alternate-url'] = self::processElement($object, 'url', 'href'); + $item['receiver'] = self::getReceivers($object); + + // handled + unset($object['id']); + unset($object['inReplyTo']); + unset($object['published']); + unset($object['updated']); + unset($object['uuid']); + unset($object['attributedTo']); + unset($object['context']); + unset($object['conversation']); + unset($object['sensitive']); + unset($object['name']); + unset($object['title']); + unset($object['content']); + unset($object['summary']); + unset($object['location']); + unset($object['attachment']); + unset($object['tag']); + unset($object['instrument']); + unset($object['url']); + unset($object['to']); + unset($object['cc']); + unset($object['bto']); + unset($object['bcc']); + + // To-Do + unset($object['source']); + + // Unhandled + unset($object['@context']); + unset($object['type']); + unset($object['actor']); + unset($object['signature']); + unset($object['mediaType']); + unset($object['duration']); + unset($object['replies']); + unset($object['icon']); + + /* + audience, preview, endTime, startTime, generator, image + */ + + return $item; + } + + private static function processNote($object, $url) + { + $item = []; + + // To-Do? + unset($object['emoji']); + unset($object['atomUri']); + unset($object['inReplyToAtomUri']); + + // Unhandled + unset($object['contentMap']); + unset($object['announcement_count']); + unset($object['announcements']); + unset($object['context_id']); + unset($object['likes']); + unset($object['like_count']); + unset($object['inReplyToStatusId']); + unset($object['shares']); + unset($object['quoteUrl']); + unset($object['statusnetConversationId']); + + if (empty($object)) + return $item; + + echo "Unknown Note\n"; + print_r($object); + print_r($item); + die($url."\n"); + + return []; + } + + private static function processArticle($object, $url) + { + $item = []; + + if (empty($object)) + return $item; + + echo "Unknown Article\n"; + print_r($object); + print_r($item); + die($url."\n"); + + return []; + } + + private static function processVideo($object, $url) + { + $item = []; + + // To-Do? + unset($object['category']); + unset($object['licence']); + unset($object['language']); + unset($object['commentsEnabled']); + + // Unhandled + unset($object['views']); + unset($object['waitTranscoding']); + unset($object['state']); + unset($object['support']); + unset($object['subtitleLanguage']); + unset($object['likes']); + unset($object['dislikes']); + unset($object['shares']); + unset($object['comments']); + + if (empty($object)) + return $item; + + echo "Unknown Video\n"; + print_r($object); + print_r($item); + die($url."\n"); + + return []; + } + + private static function processElement($array, $element, $key, $type = null) + { + if (empty($array)) { + return false; + } + + if (empty($array[$element])) { + return false; + } + + if (is_string($array[$element])) { + return $array[$element]; + } + + if (is_null($type)) { + if (!empty($array[$element][$key])) { + return $array[$element][$key]; + } + + if (!empty($array[$element][0][$key])) { + return $array[$element][0][$key]; + } + + return false; + } + + if (!empty($array[$element][$key]) && !empty($array[$element]['type']) && ($array[$element]['type'] == $type)) { + return $array[$element][$key]; + } + + /// @todo Add array search + + return false; + } + + private static function createItem($item) + { +// print_r($item); + } + + private static function announceItem($item) + { +// print_r($item); + } + + private static function activityItem($item) + { + // print_r($item); + } + +} From 1afa6523bc6988843c576a5664b393d0d1e80e88 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 11 Sep 2018 07:07:56 +0000 Subject: [PATCH 002/261] Adding (temporary) calls to AP in existing stuff --- mod/display.php | 11 ++++++++++- mod/profile.php | 10 ++++++++++ mod/xrd.php | 4 ++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mod/display.php b/mod/display.php index 907bf8ebba..21e28d5617 100644 --- a/mod/display.php +++ b/mod/display.php @@ -17,6 +17,7 @@ use Friendica\Model\Group; use Friendica\Model\Item; use Friendica\Model\Profile; use Friendica\Protocol\DFRN; +use Friendica\Protocol\ActivityPub; function display_init(App $a) { @@ -43,7 +44,7 @@ function display_init(App $a) $item = null; - $fields = ['id', 'parent', 'author-id', 'body', 'uid']; + $fields = ['id', 'parent', 'author-id', 'body', 'uid', 'guid']; // If there is only one parameter, then check if this parameter could be a guid if ($a->argc == 2) { @@ -76,6 +77,14 @@ function display_init(App $a) displayShowFeed($item["id"], false); } + if (stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json')) { + $wall_item = Item::selectFirst(['id', 'uid'], ['guid' => $item['guid'], 'wall' => true]); + if ($wall_item['uid'] == 180) { + $data = ActivityPub::createActivityFromItem($wall_item['id']); + echo json_encode($data); + exit(); + } + } if ($item["id"] != $item["parent"]) { $item = Item::selectFirstForUser(local_user(), $fields, ['id' => $item["parent"]]); } diff --git a/mod/profile.php b/mod/profile.php index 2e3ccd28c5..fd23964e41 100644 --- a/mod/profile.php +++ b/mod/profile.php @@ -20,6 +20,7 @@ use Friendica\Model\Profile; use Friendica\Module\Login; use Friendica\Protocol\DFRN; use Friendica\Util\DateTimeFormat; +use Friendica\Protocol\ActivityPub; function profile_init(App $a) { @@ -49,6 +50,15 @@ function profile_init(App $a) DFRN::autoRedir($a, $which); } + if (stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json')) { + $user = DBA::selectFirst('user', ['uid'], ['nickname' => $which]); + if ($user['uid'] == 180) { + $data = ActivityPub::profile($user['uid']); + echo json_encode($data); + exit(); + } + } + Profile::load($a, $which, $profile); $blocked = !local_user() && !remote_user() && Config::get('system', 'block_public'); diff --git a/mod/xrd.php b/mod/xrd.php index 61505f2996..87766ca26e 100644 --- a/mod/xrd.php +++ b/mod/xrd.php @@ -92,6 +92,10 @@ function xrd_json($a, $uri, $alias, $profile_url, $r) ['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-dfrn+json', 'href' => System::baseUrl().'/owa'] ] ]; + if ($r['uid'] == 180) { + $json['links'][] = ['rel' => 'self', 'type' => 'application/activity+json', 'href' => $profile_url]; + } + echo json_encode($json); killme(); } From 8c07baf54b0d426d3e38d3d01aaeffd9ceb1d8f1 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 12 Sep 2018 06:01:28 +0000 Subject: [PATCH 003/261] Better http answers --- src/Module/Inbox.php | 23 +++++------------------ src/Protocol/ActivityPub.php | 3 +++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index 86fdae7c90..0bc78b030c 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -9,6 +9,7 @@ use Friendica\Database\DBA; use Friendica\Model\Contact; use Friendica\Util\HTTPSignature; use Friendica\Util\Network; +use Friendica\Core\System; /** * ActivityPub Inbox @@ -18,30 +19,16 @@ class Inbox extends BaseModule public static function init() { $a = self::getApp(); - logger('Blubb: init 1'); $postdata = file_get_contents('php://input'); - $obj = json_decode($postdata); - - if (empty($obj)) { - exit(); + if (empty($postdata)) { + System::httpExit(400); } $tempfile = tempnam(get_temppath(), 'activitypub'); - file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $obj])); + file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $postdata])); - logger('Blubb: init ' . $tempfile); - exit(); -// goaway($dest); - } - - public static function post() - { - $a = self::getApp(); - - logger('Blubb: post'); - exit(); -// goaway($dest); + System::httpExit(200); } } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index f1d885b6c6..8992c525b0 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -25,6 +25,9 @@ use Friendica\Content\Text\BBCode; * * https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/ * https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/ + * + * Digest: https://tools.ietf.org/html/rfc5843 + * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 */ class ActivityPub { From 969311cb44a2748ebc93c485e1cd57ad86e0f139 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 12 Sep 2018 18:48:18 +0000 Subject: [PATCH 004/261] Replacing the non standard "title" with "name" --- src/Protocol/ActivityPub.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 8992c525b0..07989621f1 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -160,7 +160,7 @@ echo $return_code."\n"; $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); $data['attributedTo'] = $item['author-link']; - $data['title'] = BBCode::convert($item['title'], false, 7); + $data['name'] = BBCode::convert($item['title'], false, 7); $data['content'] = BBCode::convert($item['body'], false, 7); //$data['summary'] = ''; //$data['sensitive'] = false; @@ -588,10 +588,10 @@ echo $return_code."\n"; $item['context'] = defaults($object, 'context', null); $item['conversation'] = defaults($object, 'conversation', null); $item['sensitive'] = defaults($object, 'sensitive', null); - $item['name'] = defaults($object, 'name', null); - $item['title'] = defaults($object, 'title', null); - $item['content'] = defaults($object, 'content', null); + $item['name'] = defaults($object, 'title', null); + $item['name'] = defaults($object, 'name', $item['name']); $item['summary'] = defaults($object, 'summary', null); + $item['content'] = defaults($object, 'content', null); $item['location'] = self::processElement($object, 'location', 'name', 'Place'); $item['attachments'] = defaults($object, 'attachment', null); $item['tags'] = defaults($object, 'tag', null); From 67fa0ed433fa2519108e5f9b4715b3fdea1cb9fd Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 12 Sep 2018 21:30:10 +0000 Subject: [PATCH 005/261] Signature check added --- src/Module/Inbox.php | 13 +-- src/Protocol/ActivityPub.php | 157 +++++++++++++++++++++++++++++++++-- 2 files changed, 158 insertions(+), 12 deletions(-) diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index 0bc78b030c..63de5de12d 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -5,10 +5,7 @@ namespace Friendica\Module; use Friendica\BaseModule; -use Friendica\Database\DBA; -use Friendica\Model\Contact; -use Friendica\Util\HTTPSignature; -use Friendica\Util\Network; +use Friendica\Protocol\ActivityPub; use Friendica\Core\System; /** @@ -26,7 +23,13 @@ class Inbox extends BaseModule System::httpExit(400); } - $tempfile = tempnam(get_temppath(), 'activitypub'); + if (ActivityPub::verifySignature($postdata, $_SERVER)) { + $filename = 'signed-activitypub'; + } else { + $filename = 'failed-activitypub'; + } + + $tempfile = tempnam(get_temppath(), filename); file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $postdata])); System::httpExit(200); diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 07989621f1..83aae72aa5 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -53,16 +53,11 @@ class ActivityPub $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",headers="(request-target) host date",signature="' . $signature . '"'; $headers[] = 'Content-Type: application/activity+json'; -//print_r($headers); -//die($signed_data); -//$headers = []; -// $headers = HTTPSignature::createSig('', $headers, $owner['uprvkey'], $owner['url'] . '#main-key', false, false, 'sha256'); Network::post($target, $content, $headers); $return_code = BaseObject::getApp()->get_curl_code(); -echo $return_code."\n"; - print_r(BaseObject::getApp()->get_curl_headers()); - print_r($headers); + + echo $return_code."\n"; } /** @@ -226,6 +221,154 @@ echo $return_code."\n"; return false; } + public static function verifySignature($content, $http_headers) + { + $object = json_decode($content, true); + + if (empty($object)) { + return false; + } + + $actor = self::processElement($object, 'actor', 'id'); + + $headers = []; + $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; + + // First take every header + foreach ($http_headers as $k => $v) { + $field = str_replace('_', '-', strtolower($k)); + $headers[$field] = $v; + } + + // Now add every http header + foreach ($http_headers as $k => $v) { + if (strpos($k, 'HTTP_') === 0) { + $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; + } + } + + $sig_block = ActivityPub::parseSigHeader($http_headers['HTTP_SIGNATURE']); + + if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { + return false; + } + + $signed_data = ''; + foreach ($sig_block['headers'] as $h) { + if (array_key_exists($h, $headers)) { + $signed_data .= $h . ': ' . $headers[$h] . "\n"; + } + } + $signed_data = rtrim($signed_data, "\n"); + + if (empty($signed_data)) { + return false; + } + + $algorithm = null; + + if ($sig_block['algorithm'] === 'rsa-sha256') { + $algorithm = 'sha256'; + } + + if ($sig_block['algorithm'] === 'rsa-sha512') { + $algorithm = 'sha512'; + } + + if (empty($algorithm)) { + return false; + } + + $key = self::fetchKey($sig_block['keyId'], $actor); + + if (empty($key)) { + return false; + } + + if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) { + return false; + } + + // Check the digest if it was part of the signed data + if (in_array('digest', $sig_block['headers'])) { + $digest = explode('=', $headers['digest'], 2); + if ($digest[0] === 'SHA-256') { + $hashalg = 'sha256'; + } + if ($digest[0] === 'SHA-512') { + $hashalg = 'sha512'; + } + + /// @todo addd all hashes from the rfc + + if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { + return false; + } + } + + // Check the content-length if it was part of the signed data + if (in_array('content-length', $sig_block['headers'])) { + if (strlen($content) != $headers['content-length']) { + return false; + } + } + + return true; + + } + + private static function fetchKey($id, $actor) + { + $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); + + $profile = self::fetchProfile($url); + if (!empty($profile)) { + return $profile['pubkey']; + } elseif ($url != $actor) { + $profile = self::fetchProfile($actor); + if (!empty($profile)) { + return $profile['pubkey']; + } + } + + return false; + } + + /** + * @brief + * + * @param string $header + * @return array associate array with + * - \e string \b keyID + * - \e string \b algorithm + * - \e array \b headers + * - \e string \b signature + */ + private static function parseSigHeader($header) + { + $ret = []; + $matches = []; + + if (preg_match('/keyId="(.*?)"/ism',$header,$matches)) { + $ret['keyId'] = $matches[1]; + } + + if (preg_match('/algorithm="(.*?)"/ism',$header,$matches)) { + $ret['algorithm'] = $matches[1]; + } + + if (preg_match('/headers="(.*?)"/ism',$header,$matches)) { + $ret['headers'] = explode(' ', $matches[1]); + } + + if (preg_match('/signature="(.*?)"/ism',$header,$matches)) { + $ret['signature'] = base64_decode(preg_replace('/\s+/','',$matches[1])); + } + + return $ret; + } + /** * Fetches a profile from the given url * From f7b03bc5f39e0efd8c1b278d665f7babaad58985 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 12 Sep 2018 21:33:44 +0000 Subject: [PATCH 006/261] Missing $ --- src/Module/Inbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index 63de5de12d..bb0d9ef040 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -29,7 +29,7 @@ class Inbox extends BaseModule $filename = 'failed-activitypub'; } - $tempfile = tempnam(get_temppath(), filename); + $tempfile = tempnam(get_temppath(), $filename); file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $postdata])); System::httpExit(200); From 3f85fee7e37701a1e0eaba75ac180b8c653a81b6 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 13 Sep 2018 23:23:53 +0200 Subject: [PATCH 007/261] Add api_friendships_destroy() like mod/unfollow.php --- include/api.php | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/include/api.php b/include/api.php index 5510eddb4f..d543c03dbf 100644 --- a/include/api.php +++ b/include/api.php @@ -3629,6 +3629,102 @@ function api_direct_messages_destroy($type) /// @TODO move to top of file or somewhere better api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', true, API_METHOD_DELETE); +function api_friendships_destroy($type) +{ + $a = get_app(); + + logger("OrigUser: ".$a->user['uid'], LOGGER_DEBUG); + logger("ContactUser: ".$_REQUEST['user_id'], LOGGER_DEBUG); + if (api_user() === false) { + throw new ForbiddenException(); + } + $uid = local_user(); + + $contact_id = (x($_REQUEST, 'user_id') ? $_REQUEST['user_id'] : null); + + if ($contact_id == null) { + logger("No POST user_id", LOGGER_DEBUG); + throw new BadRequestException("no user_id specified"); + } + + $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d AND `self` = 0", intval($contact_id), 0); + + if(!DBA::isResult($contact)) { + logger("No contact by _id", LOGGER_DEBUG); + throw new BadRequestException("no contact found to given ID"); + } + + $url = $contact[0]["url"]; + logger("Contact Url: ".$contact[0]["url"], LOGGER_DEBUG); + + $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", + $uid, Contact::SHARING, Contact::FRIEND, normalise_link($url), + normalise_link($url), $url]; + $contact = DBA::selectFirst('contact', [], $condition); + + if (!DBA::isResult($contact)) { + logger("No contact founded", LOGGER_DEBUG); + throw new BadRequestException("Not following Contact"); + } + + if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { + logger("Not supported", LOGGER_DEBUG); + throw new BadRequestException("Not supported"); + } + + $dissolve = ($contact['rel'] == Contact::SHARING); + + $owner = User::getOwnerDataById($uid); + if ($owner) { + Contact::terminateFriendship($owner, $contact, $dissolve); + } + + // Sharing-only contacts get deleted as there no relationship any more + if ($dissolve) { + Contact::remove($contact['id']); + $return_path = 'contacts'; + } else { + DBA::update('contact', ['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); + } + + /////////////////// + /* + $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d AND `self` = 0", intval($contact_id), 0); + + if(!DBA::isResult($contact)) { + throw new BadRequestException("no contact found to given ID"); + } + + logger("Contact Url: ".$contact[0]["url"], LOGGER_DEBUG); + + $contact_uid = Contact::getIdForUrl($contact[0]["url"], $a->user["uid"]); + if ($contact_uid == 0) { + logger("No UserURL founded", LOGGER_DEBUG); + throw new BadRequestException("No contact id found"); + } + logger("User found: ".$contact_uid, LOGGER_DEBUG); + + $contact_user = q("SELECT * FROM `user` WHERE uid = $contact_uid"); + if(!DBA::isResult($contact_user)) { + logger("No Contact to ContactId founded", LOGGER_DEBUG); + throw new BadRequestException("No Profile found"); + } + logger("Contact founded!", LOGGER_DEBUG); + + logger("Founded User: ".$contact_user[0][nick]." + ".$contact[0]["id"]); + + Contact::terminateFriendship($a->user, $contact_user[0]); + Contact::remove($contact_user[0]['uid']); + */ + + $answer = ['result' => 'ok', 'contact' => 'contact deleted']; + return api_format_data("friendships-destroy", $type, ['result' => $answer]); +} +api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST); + + + + /** * * @param string $type Return type (atom, rss, xml, json) From a89f9cf7d5df076dcd7db74be9650299c6bb8c03 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 13 Sep 2018 23:32:26 +0200 Subject: [PATCH 008/261] api_friendships_destroy cleanup --- include/api.php | 39 ++++----------------------------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/include/api.php b/include/api.php index d543c03dbf..c8d4e71de5 100644 --- a/include/api.php +++ b/include/api.php @@ -3633,8 +3633,6 @@ function api_friendships_destroy($type) { $a = get_app(); - logger("OrigUser: ".$a->user['uid'], LOGGER_DEBUG); - logger("ContactUser: ".$_REQUEST['user_id'], LOGGER_DEBUG); if (api_user() === false) { throw new ForbiddenException(); } @@ -3643,19 +3641,19 @@ function api_friendships_destroy($type) $contact_id = (x($_REQUEST, 'user_id') ? $_REQUEST['user_id'] : null); if ($contact_id == null) { - logger("No POST user_id", LOGGER_DEBUG); + logger("No given user_id", LOGGER_DEBUG); throw new BadRequestException("no user_id specified"); } + // Get Contact by given id $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d AND `self` = 0", intval($contact_id), 0); if(!DBA::isResult($contact)) { - logger("No contact by _id", LOGGER_DEBUG); + logger("No contact by id founded", LOGGER_DEBUG); throw new BadRequestException("no contact found to given ID"); } $url = $contact[0]["url"]; - logger("Contact Url: ".$contact[0]["url"], LOGGER_DEBUG); $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", $uid, Contact::SHARING, Contact::FRIEND, normalise_link($url), @@ -3663,7 +3661,7 @@ function api_friendships_destroy($type) $contact = DBA::selectFirst('contact', [], $condition); if (!DBA::isResult($contact)) { - logger("No contact founded", LOGGER_DEBUG); + logger("Not following Contact", LOGGER_DEBUG); throw new BadRequestException("Not following Contact"); } @@ -3687,35 +3685,6 @@ function api_friendships_destroy($type) DBA::update('contact', ['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); } - /////////////////// - /* - $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d AND `self` = 0", intval($contact_id), 0); - - if(!DBA::isResult($contact)) { - throw new BadRequestException("no contact found to given ID"); - } - - logger("Contact Url: ".$contact[0]["url"], LOGGER_DEBUG); - - $contact_uid = Contact::getIdForUrl($contact[0]["url"], $a->user["uid"]); - if ($contact_uid == 0) { - logger("No UserURL founded", LOGGER_DEBUG); - throw new BadRequestException("No contact id found"); - } - logger("User found: ".$contact_uid, LOGGER_DEBUG); - - $contact_user = q("SELECT * FROM `user` WHERE uid = $contact_uid"); - if(!DBA::isResult($contact_user)) { - logger("No Contact to ContactId founded", LOGGER_DEBUG); - throw new BadRequestException("No Profile found"); - } - logger("Contact founded!", LOGGER_DEBUG); - - logger("Founded User: ".$contact_user[0][nick]." + ".$contact[0]["id"]); - - Contact::terminateFriendship($a->user, $contact_user[0]); - Contact::remove($contact_user[0]['uid']); - */ $answer = ['result' => 'ok', 'contact' => 'contact deleted']; return api_format_data("friendships-destroy", $type, ['result' => $answer]); From c2f6b166c7302d39e6e754119679036a4fca7473 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 13 Sep 2018 21:57:41 +0000 Subject: [PATCH 009/261] We now use the regular probing function --- src/Network/Probe.php | 12 +++++++++++- src/Protocol/ActivityPub.php | 13 ++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Network/Probe.php b/src/Network/Probe.php index af2d1c9a16..99ecb668c8 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -19,6 +19,7 @@ use Friendica\Model\Contact; use Friendica\Model\Profile; use Friendica\Protocol\Email; use Friendica\Protocol\Feed; +use Friendica\Protocol\ActivityPub; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -328,7 +329,16 @@ class Probe $uid = local_user(); } - $data = self::detect($uri, $network, $uid); + if ($network != Protocol::ACTIVITYPUB) { + $data = self::detect($uri, $network, $uid); + } + + if (empty($data) || ($data['network'] == Protocol::PHANTOM)) { + $ap_profile = ActivityPub::fetchProfile($uri); + if (!empty($ap_profile) && ($ap_profile['network'] == Protocol::ACTIVITYPUB)) { + $data = $ap_profile; + } + } if (!isset($data["url"])) { $data["url"] = $uri; diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 83aae72aa5..12c849d5b4 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -15,6 +15,7 @@ use Friendica\Model\User; use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; +use Friendica\Network\Probe; /** * @brief ActivityPub Protocol class @@ -322,11 +323,11 @@ class ActivityPub { $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); - $profile = self::fetchProfile($url); + $profile = Probe::uri($url, Protocol::ACTIVITYPUB); if (!empty($profile)) { return $profile['pubkey']; } elseif ($url != $actor) { - $profile = self::fetchProfile($actor); + $profile = Probe::uri($actor, Protocol::ACTIVITYPUB); if (!empty($profile)) { return $profile['pubkey']; } @@ -395,19 +396,21 @@ class ActivityPub $profile['name'] = defaults($data, 'name', $profile['nick']); $profile['guid'] = defaults($data, 'uuid', null); $profile['url'] = $data['id']; - $profile['alias'] = self::processElement($data, 'url', 'href'); $parts = parse_url($profile['url']); unset($parts['scheme']); unset($parts['path']); $profile['addr'] = $profile['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); - + $profile['alias'] = self::processElement($data, 'url', 'href'); $profile['photo'] = self::processElement($data, 'icon', 'url'); + // $profile['community'] + // $profile['keywords'] + // $profile['location'] $profile['about'] = defaults($data, 'summary', ''); $profile['batch'] = self::processElement($data, 'endpoints', 'sharedInbox'); - $profile['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); $profile['notify'] = $data['inbox']; $profile['poll'] = $data['outbox']; + $profile['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); // Check if the address is resolvable if (self::addrToUrl($profile['addr']) == $profile['url']) { From 7b45bdea173496b7a5fdf994f28b986d4a80d193 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 13 Sep 2018 22:12:33 +0000 Subject: [PATCH 010/261] Preparation for adding more networks there --- src/Network/Probe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 99ecb668c8..d48617f43b 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -333,7 +333,7 @@ class Probe $data = self::detect($uri, $network, $uid); } - if (empty($data) || ($data['network'] == Protocol::PHANTOM)) { + if (in_array(defaults($data, 'network', ''), ['', Protocol::PHANTOM])) { $ap_profile = ActivityPub::fetchProfile($uri); if (!empty($ap_profile) && ($ap_profile['network'] == Protocol::ACTIVITYPUB)) { $data = $ap_profile; From 3eb539aefde3f0ef3d0d695c3b75deb4919188ec Mon Sep 17 00:00:00 2001 From: Jonny Tischbein Date: Fri, 14 Sep 2018 09:28:14 +0200 Subject: [PATCH 011/261] api_friendships_destroy idents, DBA::selectFirst, Excepions and LogMessages --- include/api.php | 58 ++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/include/api.php b/include/api.php index c8d4e71de5..faa465b18b 100644 --- a/include/api.php +++ b/include/api.php @@ -3631,29 +3631,30 @@ api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', function api_friendships_destroy($type) { - $a = get_app(); + $a = api_user(); - if (api_user() === false) { - throw new ForbiddenException(); - } - $uid = local_user(); - - $contact_id = (x($_REQUEST, 'user_id') ? $_REQUEST['user_id'] : null); - - if ($contact_id == null) { - logger("No given user_id", LOGGER_DEBUG); - throw new BadRequestException("no user_id specified"); - } - - // Get Contact by given id - $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d AND `self` = 0", intval($contact_id), 0); - - if(!DBA::isResult($contact)) { - logger("No contact by id founded", LOGGER_DEBUG); - throw new BadRequestException("no contact found to given ID"); + if ($a === false) { + throw new ForbiddenException(); } - $url = $contact[0]["url"]; + $uid = local_user(); + + $contact_id = defaults($_REQUEST, 'user_id'); + + if ($contact_id == null) { + logger("No user_id specified", LOGGER_DEBUG); + throw new BadRequestException("no user_id specified"); + } + + // Get Contact by given id + $contact = DBA::selectFirst('contact', ['url'], ['id' => $contact_id, 'uid' => 0, 'self' => false]); + + if(!DBA::isResult($contact)) { + logger("No contact found for ID" . $contact_id, LOGGER_DEBUG); + throw new NoFoundException("no contact found to given ID"); + } + + $url = $contact["url"]; $condition = ["`uid` = ? AND (`rel` = ? OR `rel` = ?) AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)", $uid, Contact::SHARING, Contact::FRIEND, normalise_link($url), @@ -3662,12 +3663,12 @@ function api_friendships_destroy($type) if (!DBA::isResult($contact)) { logger("Not following Contact", LOGGER_DEBUG); - throw new BadRequestException("Not following Contact"); + throw new NoFoundException("Not following Contact"); } if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { logger("Not supported", LOGGER_DEBUG); - throw new BadRequestException("Not supported"); + throw new ExpectationFailedException("Not supported"); } $dissolve = ($contact['rel'] == Contact::SHARING); @@ -3676,24 +3677,23 @@ function api_friendships_destroy($type) if ($owner) { Contact::terminateFriendship($owner, $contact, $dissolve); } + else { + logger("No owner found", LOGGER_DEBUG); + throw new Exception("Error Processing Request"); + } // Sharing-only contacts get deleted as there no relationship any more if ($dissolve) { Contact::remove($contact['id']); - $return_path = 'contacts'; } else { DBA::update('contact', ['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); } - - $answer = ['result' => 'ok', 'contact' => 'contact deleted']; - return api_format_data("friendships-destroy", $type, ['result' => $answer]); + $answer = ['result' => 'ok', 'user_id' => $contact_id, 'contact' => 'contact deleted']; + return api_format_data("friendships-destroy", $type, ['result' => $answer]); } api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST); - - - /** * * @param string $type Return type (atom, rss, xml, json) From 2c3a58d44e858381f8324643d7727b71a8e5c2dc Mon Sep 17 00:00:00 2001 From: Jonny Tischbein Date: Fri, 14 Sep 2018 10:06:26 +0200 Subject: [PATCH 012/261] api_friendhips_destroy function header --- include/api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/api.php b/include/api.php index faa465b18b..db42d63c5b 100644 --- a/include/api.php +++ b/include/api.php @@ -3679,7 +3679,7 @@ function api_friendships_destroy($type) } else { logger("No owner found", LOGGER_DEBUG); - throw new Exception("Error Processing Request"); + throw new NoFoundException("Error Processing Request"); } // Sharing-only contacts get deleted as there no relationship any more From 61e2c7d20dfcf3eba7442c2b181d22b8926062e3 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Sep 2018 16:51:32 +0000 Subject: [PATCH 013/261] Added AP to many network conditions / enabling inbox processing --- include/conversation.php | 6 +- mod/contacts.php | 10 +- mod/dirfind.php | 2 +- src/Content/ContactSelector.php | 33 ++++--- src/Content/Widget.php | 5 +- src/Model/Contact.php | 6 +- src/Model/Conversation.php | 6 +- src/Model/Item.php | 8 +- src/Module/Inbox.php | 6 +- src/Network/Probe.php | 2 + src/Protocol/ActivityPub.php | 163 ++++++++++++++++++++++++------- src/Protocol/PortableContact.php | 2 +- src/Worker/Notifier.php | 4 +- src/Worker/UpdateGContact.php | 4 +- 14 files changed, 178 insertions(+), 79 deletions(-) diff --git a/include/conversation.php b/include/conversation.php index 45e5d1caa3..835d227477 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -556,7 +556,7 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o if (in_array($mode, ['community', 'contacts'])) { $writable = true; } else { - $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]); + $writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]); } if (!local_user()) { @@ -807,7 +807,7 @@ function conversation_add_children(array $parents, $block_authors, $order, $uid) foreach ($items as $index => $item) { if ($item['uid'] == 0) { - $items[$index]['writable'] = in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]); + $items[$index]['writable'] = in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]); } } @@ -877,7 +877,7 @@ function item_photo_menu($item) { } if ((($cid == 0) || ($rel == Contact::FOLLOWER)) && - in_array($item['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) { + in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) { $menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']); } } else { diff --git a/mod/contacts.php b/mod/contacts.php index 68f68fec3b..031f6964c3 100644 --- a/mod/contacts.php +++ b/mod/contacts.php @@ -537,7 +537,7 @@ function contacts_content(App $a, $update = 0) $relation_text = ''; } - if (!in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) { + if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) { $relation_text = ""; } @@ -559,7 +559,7 @@ function contacts_content(App $a, $update = 0) } $lblsuggest = (($contact['network'] === Protocol::DFRN) ? L10n::t('Suggest friends') : ''); - $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]); + $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]); $nettype = L10n::t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact["url"])); @@ -968,7 +968,7 @@ function contact_conversations(App $a, $contact_id, $update) $profiledata = Contact::getDetailsByURL($contact["url"]); if (local_user()) { - if (in_array($profiledata["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { + if (in_array($profiledata["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { $profiledata["remoteconnect"] = System::baseUrl()."/follow?url=".urlencode($profiledata["url"]); } } @@ -992,7 +992,7 @@ function contact_posts(App $a, $contact_id) $profiledata = Contact::getDetailsByURL($contact["url"]); if (local_user()) { - if (in_array($profiledata["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { + if (in_array($profiledata["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { $profiledata["remoteconnect"] = System::baseUrl()."/follow?url=".urlencode($profiledata["url"]); } } @@ -1073,7 +1073,7 @@ function _contact_detail_for_template(array $rr) */ function contact_actions($contact) { - $poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]); + $poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]); $contact_actions = []; // Provide friend suggestion only for Friendica contacts diff --git a/mod/dirfind.php b/mod/dirfind.php index 332fe90f6c..4223bb6ecd 100644 --- a/mod/dirfind.php +++ b/mod/dirfind.php @@ -54,7 +54,7 @@ function dirfind_content(App $a, $prefix = "") { if ((valid_email($search) && Network::isEmailDomainValid($search)) || (substr(normalise_link($search), 0, 7) == "http://")) { $user_data = Probe::uri($search); - $discover_user = (in_array($user_data["network"], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])); + $discover_user = (in_array($user_data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])); } } diff --git a/src/Content/ContactSelector.php b/src/Content/ContactSelector.php index d5efecb806..dc158cfa5f 100644 --- a/src/Content/ContactSelector.php +++ b/src/Content/ContactSelector.php @@ -75,21 +75,22 @@ class ContactSelector public static function networkToName($s, $profile = "") { $nets = [ - Protocol::DFRN => L10n::t('Friendica'), - Protocol::OSTATUS => L10n::t('OStatus'), - Protocol::FEED => L10n::t('RSS/Atom'), - Protocol::MAIL => L10n::t('Email'), - Protocol::DIASPORA => L10n::t('Diaspora'), - Protocol::ZOT => L10n::t('Zot!'), - Protocol::LINKEDIN => L10n::t('LinkedIn'), - Protocol::XMPP => L10n::t('XMPP/IM'), - Protocol::MYSPACE => L10n::t('MySpace'), - Protocol::GPLUS => L10n::t('Google+'), - Protocol::PUMPIO => L10n::t('pump.io'), - Protocol::TWITTER => L10n::t('Twitter'), - Protocol::DIASPORA2 => L10n::t('Diaspora Connector'), - Protocol::STATUSNET => L10n::t('GNU Social Connector'), - Protocol::PNUT => L10n::t('pnut'), + Protocol::DFRN => L10n::t('Friendica'), + Protocol::OSTATUS => L10n::t('OStatus'), + Protocol::FEED => L10n::t('RSS/Atom'), + Protocol::MAIL => L10n::t('Email'), + Protocol::DIASPORA => L10n::t('Diaspora'), + Protocol::ZOT => L10n::t('Zot!'), + Protocol::LINKEDIN => L10n::t('LinkedIn'), + Protocol::XMPP => L10n::t('XMPP/IM'), + Protocol::MYSPACE => L10n::t('MySpace'), + Protocol::GPLUS => L10n::t('Google+'), + Protocol::PUMPIO => L10n::t('pump.io'), + Protocol::TWITTER => L10n::t('Twitter'), + Protocol::DIASPORA2 => L10n::t('Diaspora Connector'), + Protocol::STATUSNET => L10n::t('GNU Social Connector'), + Protocol::ACTIVITYPUB => L10n::t('ActivityPub'), + Protocol::PNUT => L10n::t('pnut'), ]; Addon::callHooks('network_to_name', $nets); @@ -99,7 +100,7 @@ class ContactSelector $networkname = str_replace($search, $replace, $s); - if ((in_array($s, [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) && ($profile != "")) { + if ((in_array($s, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) && ($profile != "")) { $r = DBA::fetchFirst("SELECT `gserver`.`platform` FROM `gcontact` INNER JOIN `gserver` ON `gserver`.`nurl` = `gcontact`.`server_url` WHERE `gcontact`.`nurl` = ? AND `platform` != ''", normalise_link($profile)); diff --git a/src/Content/Widget.php b/src/Content/Widget.php index f245f0d95e..faba55b7a8 100644 --- a/src/Content/Widget.php +++ b/src/Content/Widget.php @@ -142,10 +142,7 @@ class Widget $nets = array(); while ($rr = DBA::fetch($r)) { - /// @TODO If 'network' is not there, this triggers an E_NOTICE - if ($rr['network']) { - $nets[] = array('ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network']), 'selected' => (($selected == $rr['network']) ? 'selected' : '' )); - } + $nets[] = array('ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network']), 'selected' => (($selected == $rr['network']) ? 'selected' : '' )); } DBA::close($r); diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 1bbc0228a8..ab804ab7f1 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -775,7 +775,7 @@ class Contact extends BaseObject } if ((empty($profile["addr"]) || empty($profile["name"])) && (defaults($profile, "gid", 0) != 0) - && in_array($profile["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]) + && in_array($profile["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]) ) { Worker::add(PRIORITY_LOW, "UpdateGContact", $profile["gid"]); } @@ -1088,7 +1088,7 @@ class Contact extends BaseObject } // Last try in gcontact for unsupported networks - if (!in_array($data["network"], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::PUMPIO, Protocol::MAIL, Protocol::FEED])) { + if (!in_array($data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::PUMPIO, Protocol::MAIL, Protocol::FEED])) { if ($uid != 0) { return 0; } @@ -1327,7 +1327,7 @@ class Contact extends BaseObject return ''; } - if (in_array($r[0]["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { + if (in_array($r[0]["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = ? AND NOT `item`.`global`))"; } else { $sql = "`item`.`uid` = ?"; diff --git a/src/Model/Conversation.php b/src/Model/Conversation.php index 0692a73412..ba50dc25e4 100644 --- a/src/Model/Conversation.php +++ b/src/Model/Conversation.php @@ -22,6 +22,7 @@ class Conversation const PARCEL_DIASPORA = 2; const PARCEL_SALMON = 3; const PARCEL_FEED = 4; // Deprecated + const PARCEL_ACTIVITYPUB = 5; const PARCEL_SPLIT_CONVERSATION = 6; const PARCEL_TWITTER = 67; @@ -34,7 +35,7 @@ class Conversation public static function insert(array $arr) { if (in_array(defaults($arr, 'network', Protocol::PHANTOM), - [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::TWITTER]) && !empty($arr['uri'])) { + [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::TWITTER]) && !empty($arr['uri'])) { $conversation = ['item-uri' => $arr['uri'], 'received' => DateTimeFormat::utcNow()]; if (isset($arr['parent-uri']) && ($arr['parent-uri'] != $arr['uri'])) { @@ -70,7 +71,8 @@ class Conversation unset($old_conv['source']); } // Update structure data all the time but the source only when its from a better protocol. - if (isset($conversation['protocol']) && isset($conversation['source']) && ($old_conv['protocol'] < $conversation['protocol']) && ($old_conv['protocol'] != 0)) { + if (isset($conversation['protocol']) && isset($conversation['source']) && ($old_conv['protocol'] < $conversation['protocol']) + && ($old_conv['protocol'] != 0) && ($old_conv['protocol'] != self::PARCEL_ACTIVITYPUB)) { unset($conversation['protocol']); unset($conversation['source']); } diff --git a/src/Model/Item.php b/src/Model/Item.php index d10a211a59..9406df2ac8 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -176,7 +176,7 @@ class Item extends BaseObject // We can always comment on posts from these networks if (array_key_exists('writable', $row) && - in_array($row['internal-network'], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { + in_array($row['internal-network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { $row['writable'] = true; } @@ -1352,7 +1352,7 @@ class Item extends BaseObject * We have to check several networks since Friendica posts could be repeated * via OStatus (maybe Diasporsa as well) */ - if (in_array($item['network'], [Protocol::DIASPORA, Protocol::DFRN, Protocol::OSTATUS, ""])) { + if (in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DIASPORA, Protocol::DFRN, Protocol::OSTATUS, ""])) { $condition = ["`uri` = ? AND `uid` = ? AND `network` IN (?, ?, ?)", trim($item['uri']), $item['uid'], Protocol::DIASPORA, Protocol::DFRN, Protocol::OSTATUS]; @@ -2053,7 +2053,7 @@ class Item extends BaseObject // Only distribute public items from native networks $condition = ['id' => $itemid, 'uid' => 0, - 'network' => [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""], + 'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""], 'visible' => true, 'deleted' => false, 'moderated' => false, 'private' => false]; $item = self::selectFirst(self::ITEM_FIELDLIST, ['id' => $itemid]); if (!DBA::isResult($item)) { @@ -2175,7 +2175,7 @@ class Item extends BaseObject } // is it an entry from a connector? Only add an entry for natively connected networks - if (!in_array($item["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { + if (!in_array($item["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { return; } diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index bb0d9ef040..dafc418f69 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -32,6 +32,10 @@ class Inbox extends BaseModule $tempfile = tempnam(get_temppath(), $filename); file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $postdata])); - System::httpExit(200); + logger('Incoming message stored under ' . $tempfile); + + ActivityPub::processInbox($postdata, $_SERVER); + + System::httpExit(202); } } diff --git a/src/Network/Probe.php b/src/Network/Probe.php index d48617f43b..2cf91486bb 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -331,6 +331,8 @@ class Probe if ($network != Protocol::ACTIVITYPUB) { $data = self::detect($uri, $network, $uid); + } else { + $data = null; } if (in_array(defaults($data, 'network', ''), ['', Protocol::PHANTOM])) { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 12c849d5b4..71e554d8e5 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -10,11 +10,14 @@ use Friendica\BaseObject; use Friendica\Util\Network; use Friendica\Util\HTTPSignature; use Friendica\Core\Protocol; +use Friendica\Model\Conversation; +use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\User; use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; +use Friendica\Content\Text\HTML; use Friendica\Network\Probe; /** @@ -474,6 +477,25 @@ class ActivityPub return $profile; } + public static function processInbox($body, $header) + { + logger('Incoming message', LOGGER_DEBUG); + + if (!self::verifySignature($body, $header)) { + logger('Invalid signature, message will be discarded.', LOGGER_DEBUG); + return; + } + + $activity = json_decode($body, true); + + if (!is_array($activity)) { + logger('Invalid body.', LOGGER_DEBUG); + return; + } + + self::processActivity($activity, $body); + } + public static function fetchOutbox($url) { $data = self::fetchContent($url); @@ -493,26 +515,31 @@ class ActivityPub } foreach ($items as $activity) { - self::processActivity($activity, $url); + self::processActivity($activity); } } - function processActivity($activity, $url) + function processActivity($activity, $body = '') { if (empty($activity['type'])) { + logger('Empty type', LOGGER_DEBUG); return; } if (empty($activity['object'])) { + logger('Empty object', LOGGER_DEBUG); return; } if (empty($activity['actor'])) { + logger('Empty actor', LOGGER_DEBUG); return; + } $actor = self::processElement($activity, 'actor', 'id'); if (empty($actor)) { + logger('Empty actor - 2', LOGGER_DEBUG); return; } @@ -521,11 +548,13 @@ class ActivityPub } elseif (!empty($activity['object']['id'])) { $object_url = $activity['object']['id']; } else { + logger('No object found', LOGGER_DEBUG); return; } $receivers = self::getReceivers($activity); if (empty($receivers)) { + logger('No receivers found', LOGGER_DEBUG); return; } @@ -545,6 +574,7 @@ class ActivityPub // To-Do? unset($activity['context']); unset($activity['location']); + unset($activity['signature']); // handled unset($activity['to']); @@ -559,17 +589,19 @@ class ActivityPub unset($activity['instrument']); unset($activity['inReplyTo']); +/* if (!empty($activity)) { echo "Activity\n"; print_r($activity); die($url."\n"); } - +*/ $activity = $structure; // ---------------------------------- - $item = self::fetchObject($object_url, $url); + $item = self::fetchObject($object_url, $activity['object']); if (empty($item)) { + logger("Object data couldn't be processed", LOGGER_DEBUG); return; } @@ -579,28 +611,32 @@ class ActivityPub $item['receiver'] = array_merge($item['receiver'], $receivers); + logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); + switch ($activity['type']) { case 'Create': case 'Update': - self::createItem($item); + self::createItem($item, $body); break; case 'Announce': - self::announceItem($item); + self::announceItem($item, $body); break; case 'Like': case 'Dislike': - self::activityItem($item); + self::activityItem($item, $body); break; case 'Follow': break; default: - echo "Unknown activity: ".$activity['type']."\n"; + logger('Unknown activity: ' . $activity['type'], LOGGER_DEBUG); +/* echo "Unknown activity: ".$activity['type']."\n"; print_r($item); die(); +*/ break; } } @@ -626,11 +662,11 @@ class ActivityPub } $condition = ['self' => true, 'nurl' => normalise_link($receiver)]; - $contact = DBA::selectFirst('contact', ['id'], $condition); + $contact = DBA::selectFirst('contact', ['uid'], $condition); if (!DBA::isResult($contact)) { continue; } - $receivers[$receiver] = $contact['id']; + $receivers[$receiver] = $contact['uid']; } } return $receivers; @@ -653,67 +689,78 @@ class ActivityPub if (!empty($activity['instrument'])) { $item['service'] = self::processElement($activity, 'instrument', 'name', 'Service'); } - +/* // Remove all "null" fields foreach ($item as $field => $content) { if (is_null($content)) { unset($item[$field]); } } - +*/ return $item; } - private static function fetchObject($object_url, $url) + private static function fetchObject($object_url, $object = []) { $data = self::fetchContent($object_url); if (empty($data)) { - return false; + $data = $object; + if (empty($data)) { + logger('Empty content'); + return false; + } else { + logger('Using provided object'); + } } if (empty($data['type'])) { + logger('Empty type'); return false; } else { $type = $data['type']; + logger('Type ' . $type); } if (in_array($type, ['Note', 'Article', 'Video'])) { - $common = self::processCommonData($data, $url); + $common = self::processCommonData($data); } switch ($type) { case 'Note': - return array_merge($common, self::processNote($data, $url)); + return array_merge($common, self::processNote($data)); case 'Article': - return array_merge($common, self::processArticle($data, $url)); + return array_merge($common, self::processArticle($data)); case 'Video': - return array_merge($common, self::processVideo($data, $url)); + return array_merge($common, self::processVideo($data)); case 'Announce': if (empty($data['object'])) { return false; } - return self::fetchObject($data['object'], $url); + return self::fetchObject($data['object']); case 'Person': case 'Tombstone': break; default: - echo "Unknown object type: ".$data['type']."\n"; + logger('Unknown object type: ' . $data['type'], LOGGER_DEBUG); +/* echo "Unknown object type: ".$data['type']."\n"; print_r($data); die($url."\n"); +*/ break; } } - private static function processCommonData(&$object, $url) + private static function processCommonData(&$object) { if (empty($object['id']) || empty($object['attributedTo'])) { return false; } $item = []; + $item['type'] = $object['type']; $item['uri'] = $object['id']; if (!empty($object['inReplyTo'])) { @@ -789,7 +836,7 @@ class ActivityPub return $item; } - private static function processNote($object, $url) + private static function processNote($object) { $item = []; @@ -810,33 +857,33 @@ class ActivityPub unset($object['quoteUrl']); unset($object['statusnetConversationId']); - if (empty($object)) +// if (empty($object)) return $item; - +/* echo "Unknown Note\n"; print_r($object); print_r($item); die($url."\n"); - +*/ return []; } - private static function processArticle($object, $url) + private static function processArticle($object) { $item = []; - if (empty($object)) +// if (empty($object)) return $item; - +/* echo "Unknown Article\n"; print_r($object); print_r($item); die($url."\n"); - +*/ return []; } - private static function processVideo($object, $url) + private static function processVideo($object) { $item = []; @@ -857,14 +904,14 @@ class ActivityPub unset($object['shares']); unset($object['comments']); - if (empty($object)) +// if (empty($object)) return $item; - +/* echo "Unknown Video\n"; print_r($object); print_r($item); die($url."\n"); - +*/ return []; } @@ -903,9 +950,55 @@ class ActivityPub return false; } - private static function createItem($item) + private static function createItem($activity, $body) { -// print_r($item); +// print_r($activity); + + $item = []; + $item['network'] = Protocol::ACTIVITYPUB; + $item['wall'] = 0; + $item['origin'] = 0; +// $item['private'] = 0; + $item['gravity'] = GRAVITY_COMMENT; + $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); + $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); + $item['uri'] = $activity['uri']; + $item['parent-uri'] = $activity['reply-to-uri']; + $item['verb'] = ACTIVITY_POST; // Todo + $item['object-type'] = ACTIVITY_OBJ_NOTE; // Todo + $item['created'] = $activity['published']; + $item['edited'] = $activity['updated']; + $item['guid'] = $activity['uuid']; + $item['title'] = HTML::toBBCode($activity['name']); + $item['content-warning'] = HTML::toBBCode($activity['summary']); + $item['body'] = HTML::toBBCode($activity['content']); + $item['location'] = $activity['location']; +// $item['attach'] = $activity['attachments']; +// $item['tag'] = self::constructTagList($activity['tags'], $activity['sensitive']); + $item['app'] = $activity['service']; + $item['plink'] = $activity['alternate-url']; + + $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB; + $item['source'] = $body; +// $item[''] = $activity['context']; + $item['conversation-uri'] = $activity['conversation']; + + foreach ($activity['receiver'] as $receiver) { + $item['uid'] = $receiver; + $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true); + + if (($receiver != 0) && empty($item['contact-id'])) { + $item['contact-id'] = Contact::getIdForURL($activity['author'], 0, true); + } + + $item_id = Item::insert($item); + logger('Storing for user ' . $item['uid'] . ': ' . $item_id); + if (!empty($item_id) && ($item['uid'] == 0)) { + Item::distribute($item_id); + } +//print_r($item); + } +// $item[''] = $activity['receiver']; } private static function announceItem($item) diff --git a/src/Protocol/PortableContact.php b/src/Protocol/PortableContact.php index 20ee77a07c..61274dc2be 100644 --- a/src/Protocol/PortableContact.php +++ b/src/Protocol/PortableContact.php @@ -333,7 +333,7 @@ class PortableContact $server_url = normalise_link(self::detectServer($profile)); } - if (!in_array($gcontacts[0]["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::FEED, Protocol::OSTATUS, ""])) { + if (!in_array($gcontacts[0]["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::FEED, Protocol::OSTATUS, ""])) { logger("Profile ".$profile.": Network type ".$gcontacts[0]["network"]." can't be checked", LOGGER_DEBUG); return false; } diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 61eaba388b..55f80c94d7 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -363,9 +363,9 @@ class Notifier } // It only makes sense to distribute answers to OStatus messages to Friendica and OStatus - but not Diaspora - $networks = [Protocol::OSTATUS, Protocol::DFRN]; + $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN]; } else { - $networks = [Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; + $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; } } else { $public_message = false; diff --git a/src/Worker/UpdateGContact.php b/src/Worker/UpdateGContact.php index f2919e4a75..67362917c3 100644 --- a/src/Worker/UpdateGContact.php +++ b/src/Worker/UpdateGContact.php @@ -29,13 +29,13 @@ class UpdateGContact return; } - if (!in_array($r[0]["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { + if (!in_array($r[0]["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { return; } $data = Probe::uri($r[0]["url"]); - if (!in_array($data["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { + if (!in_array($data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) { if ($r[0]["server_url"] != "") { PortableContact::checkServer($r[0]["server_url"], $r[0]["network"]); } From b7e15e8b3856e8ffde5aae032da3e7af6e17a7a0 Mon Sep 17 00:00:00 2001 From: Jonny Tischbein Date: Fri, 14 Sep 2018 19:35:24 +0200 Subject: [PATCH 014/261] api_friendships_destroy finally function header + NotFoundException typo --- include/api.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/include/api.php b/include/api.php index db42d63c5b..49aa4489b0 100644 --- a/include/api.php +++ b/include/api.php @@ -3629,6 +3629,15 @@ function api_direct_messages_destroy($type) /// @TODO move to top of file or somewhere better api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', true, API_METHOD_DELETE); +/** + * Unfollow Contact + * + * @brief unfollow contact + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string|array + * @see https://developer.twitter.com/en/docs/accounts-and-users/follow-search-get-users/api-reference/post-friendships-destroy.html + */ function api_friendships_destroy($type) { $a = api_user(); @@ -3651,7 +3660,7 @@ function api_friendships_destroy($type) if(!DBA::isResult($contact)) { logger("No contact found for ID" . $contact_id, LOGGER_DEBUG); - throw new NoFoundException("no contact found to given ID"); + throw new NotFoundException("no contact found to given ID"); } $url = $contact["url"]; @@ -3663,7 +3672,7 @@ function api_friendships_destroy($type) if (!DBA::isResult($contact)) { logger("Not following Contact", LOGGER_DEBUG); - throw new NoFoundException("Not following Contact"); + throw new NotFoundException("Not following Contact"); } if (!in_array($contact['network'], Protocol::NATIVE_SUPPORT)) { @@ -3679,7 +3688,7 @@ function api_friendships_destroy($type) } else { logger("No owner found", LOGGER_DEBUG); - throw new NoFoundException("Error Processing Request"); + throw new NotFoundException("Error Processing Request"); } // Sharing-only contacts get deleted as there no relationship any more From 2e7ca76e15dd8aba82c730a222f8711ed71dead3 Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 14 Sep 2018 21:10:49 +0000 Subject: [PATCH 015/261] More fields to import / like could possibly work --- src/Protocol/ActivityPub.php | 280 +++++++++++++++-------------------- 1 file changed, 123 insertions(+), 157 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 71e554d8e5..54c479b50c 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -37,7 +37,7 @@ class ActivityPub { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; - public static function transmit($content, $target, $uid) + public static function transmit($data, $target, $uid) { $owner = User::getOwnerDataById($uid); @@ -45,6 +45,8 @@ class ActivityPub return; } + $content = json_encode($data); + $host = parse_url($target, PHP_URL_HOST); $path = parse_url($target, PHP_URL_PATH); $date = date('r'); @@ -138,8 +140,6 @@ class ActivityPub $data['actor'] = $item['author-link']; $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; $data['object'] = self::createNote($item); -// print_r($data); -// print_r($item); return $data; } @@ -150,25 +150,38 @@ class ActivityPub $data['id'] = $item['plink']; //$data['context'] = $data['conversation'] = $item['parent-uri']; $data['actor'] = $item['author-link']; -// if (!$item['private']) { -// $data['to'] = []; -// $data['to'][] = '"https://pleroma.soykaf.com/users/heluecht"'; - $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; -// $data['cc'] = 'https://pleroma.soykaf.com/users/heluecht'; -// } + $data['to'] = []; + if (!$item['private']) { + $data['to'][] = '"https://pleroma.soykaf.com/users/heluecht"'; + } $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); $data['attributedTo'] = $item['author-link']; $data['name'] = BBCode::convert($item['title'], false, 7); $data['content'] = BBCode::convert($item['body'], false, 7); - //$data['summary'] = ''; - //$data['sensitive'] = false; - //$data['emoji'] = []; - //$data['tag'] = []; - //$data['attachment'] = []; + //$data['summary'] = ''; // Ignore by now + //$data['sensitive'] = false; // - Query NSFW + //$data['emoji'] = []; // Ignore by now + //$data['tag'] = []; /// @ToDo + //$data['attachment'] = []; // @ToDo return $data; } + public static function transmitActivity($activity, $target, $uid) + { + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + + $owner = User::getOwnerDataById($uid); + + $data = ['@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://pirati.ca/' . strtolower($activity) . '/' . System::createGUID(), + 'type' => $activity, + 'actor' => $owner['url'], + 'object' => $profile['url']]; + + return self::transmit($data, $profile['notify'], $uid); + } + /** * Fetches ActivityPub content from the given url * @@ -304,7 +317,7 @@ class ActivityPub $hashalg = 'sha512'; } - /// @todo addd all hashes from the rfc + /// @todo add all hashes from the rfc if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { return false; @@ -435,19 +448,7 @@ class ActivityPub } } - // Handled - unset($data['id']); - unset($data['inbox']); - unset($data['outbox']); - unset($data['preferredUsername']); - unset($data['name']); - unset($data['summary']); - unset($data['url']); - unset($data['publicKey']); - unset($data['endpoints']); - unset($data['icon']); - unset($data['uuid']); - +/* // To-Do unset($data['type']); unset($data['manuallyApprovesFollowers']); @@ -468,11 +469,6 @@ class ActivityPub unset($data['isCat']); // Misskey unset($data['kroeg:blocks']); // Kroeg unset($data['updated']); // Kroeg - -/* if (!empty($data)) { - print_r($data); - die(); - } */ return $profile; } @@ -559,6 +555,7 @@ class ActivityPub } // ---------------------------------- +/* // unhandled unset($activity['@context']); unset($activity['id']); @@ -569,40 +566,22 @@ class ActivityPub unset($activity['context_id']); unset($activity['statusnetConversationId']); - $structure = $activity; - // To-Do? unset($activity['context']); unset($activity['location']); unset($activity['signature']); - - // handled - unset($activity['to']); - unset($activity['cc']); - unset($activity['bto']); - unset($activity['bcc']); - unset($activity['type']); - unset($activity['actor']); - unset($activity['object']); - unset($activity['published']); - unset($activity['updated']); - unset($activity['instrument']); - unset($activity['inReplyTo']); - -/* - if (!empty($activity)) { - echo "Activity\n"; - print_r($activity); - die($url."\n"); - } */ - $activity = $structure; - // ---------------------------------- - $item = self::fetchObject($object_url, $activity['object']); - if (empty($item)) { - logger("Object data couldn't be processed", LOGGER_DEBUG); - return; + if (!in_array($activity['type'], ['Like', 'Dislike'])) { + $item = self::fetchObject($object_url, $activity['object']); + if (empty($item)) { + logger("Object data couldn't be processed", LOGGER_DEBUG); + return; + } + } else { + $item['object'] = $object_url; + $item['receiver'] = []; + $item['type'] = $activity['type']; } $item = self::addActivityFields($item, $activity); @@ -616,11 +595,8 @@ class ActivityPub switch ($activity['type']) { case 'Create': case 'Update': - self::createItem($item, $body); - break; - case 'Announce': - self::announceItem($item, $body); + self::createItem($item, $body); break; case 'Like': @@ -633,10 +609,6 @@ class ActivityPub default: logger('Unknown activity: ' . $activity['type'], LOGGER_DEBUG); -/* echo "Unknown activity: ".$activity['type']."\n"; - print_r($item); - die(); -*/ break; } } @@ -689,14 +661,6 @@ class ActivityPub if (!empty($activity['instrument'])) { $item['service'] = self::processElement($activity, 'instrument', 'name', 'Service'); } -/* - // Remove all "null" fields - foreach ($item as $field => $content) { - if (is_null($content)) { - unset($item[$field]); - } - } -*/ return $item; } @@ -745,10 +709,6 @@ class ActivityPub default: logger('Unknown object type: ' . $data['type'], LOGGER_DEBUG); -/* echo "Unknown object type: ".$data['type']."\n"; - print_r($data); - die($url."\n"); -*/ break; } } @@ -792,30 +752,7 @@ class ActivityPub $item['alternate-url'] = self::processElement($object, 'url', 'href'); $item['receiver'] = self::getReceivers($object); - // handled - unset($object['id']); - unset($object['inReplyTo']); - unset($object['published']); - unset($object['updated']); - unset($object['uuid']); - unset($object['attributedTo']); - unset($object['context']); - unset($object['conversation']); - unset($object['sensitive']); - unset($object['name']); - unset($object['title']); - unset($object['content']); - unset($object['summary']); - unset($object['location']); - unset($object['attachment']); - unset($object['tag']); - unset($object['instrument']); - unset($object['url']); - unset($object['to']); - unset($object['cc']); - unset($object['bto']); - unset($object['bcc']); - +/* // To-Do unset($object['source']); @@ -829,10 +766,9 @@ class ActivityPub unset($object['replies']); unset($object['icon']); - /* + // Also missing: audience, preview, endTime, startTime, generator, image - */ - +*/ return $item; } @@ -840,6 +776,7 @@ class ActivityPub { $item = []; +/* // To-Do? unset($object['emoji']); unset($object['atomUri']); @@ -856,37 +793,21 @@ class ActivityPub unset($object['shares']); unset($object['quoteUrl']); unset($object['statusnetConversationId']); - -// if (empty($object)) - return $item; -/* - echo "Unknown Note\n"; - print_r($object); - print_r($item); - die($url."\n"); */ - return []; + return $item; } private static function processArticle($object) { $item = []; -// if (empty($object)) - return $item; -/* - echo "Unknown Article\n"; - print_r($object); - print_r($item); - die($url."\n"); -*/ - return []; + return $item; } private static function processVideo($object) { $item = []; - +/* // To-Do? unset($object['category']); unset($object['licence']); @@ -903,16 +824,8 @@ class ActivityPub unset($object['dislikes']); unset($object['shares']); unset($object['comments']); - -// if (empty($object)) - return $item; -/* - echo "Unknown Video\n"; - print_r($object); - print_r($item); - die($url."\n"); */ - return []; + return $item; } private static function processElement($array, $element, $key, $type = null) @@ -950,37 +863,90 @@ class ActivityPub return false; } + private static function convertMentions($body) + { + $URLSearchString = "^\[\]"; + $body = preg_replace("/\[url\=([$URLSearchString]*)\]([#@!])(.*?)\[\/url\]/ism", '$2[url=$1]$3[/url]', $body); + + return $body; + } + + private static function constructTagList($tags, $sensitive) + { + if (empty($tags)) { + return ''; + } + + $tag_text = ''; + foreach ($tags as $tag) { + if (in_array($tag['type'], ['Mention', 'Hashtag'])) { + if (!empty($tag_text)) { + $tag_text .= ','; + } + + $tag_text .= substr($tag['name'], 0, 1) . '[url=' . $tag['href'] . ']' . substr($tag['name'], 1) . '[/url]'; + } + } + + /// @todo add nsfw for $sensitive + + return $tag_text; + } + + private static function constructAttachList($attachments, $item) + { + if (empty($attachments)) { + return $item; + } + + foreach ($attachments as $attach) { + $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/'))); + if ($filetype == 'image') { + $item['body'] .= "\n[img]".$attach['url'].'[/img]'; + } else { + if (!empty($item["attach"])) { + $item["attach"] .= ','; + } else { + $item["attach"] = ''; + } + if (!isset($attach['length'])) { + $attach['length'] = "0"; + } + $item["attach"] .= '[attach]href="'.$attach['url'].'" length="'.$attach['length'].'" type="'.$attach['mediaType'].'" title="'.defaults($attach, 'name', '').'"[/attach]'; + } + } + + return $item; + } + private static function createItem($activity, $body) { -// print_r($activity); + /// @todo What to do with $activity['context']? $item = []; $item['network'] = Protocol::ACTIVITYPUB; - $item['wall'] = 0; - $item['origin'] = 0; -// $item['private'] = 0; - $item['gravity'] = GRAVITY_COMMENT; + $item['private'] = !in_array(0, $activity['receiver']); $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); $item['uri'] = $activity['uri']; $item['parent-uri'] = $activity['reply-to-uri']; - $item['verb'] = ACTIVITY_POST; // Todo - $item['object-type'] = ACTIVITY_OBJ_NOTE; // Todo + $item['verb'] = ACTIVITY_POST; + $item['object-type'] = ACTIVITY_OBJ_NOTE; /// Todo? $item['created'] = $activity['published']; $item['edited'] = $activity['updated']; $item['guid'] = $activity['uuid']; $item['title'] = HTML::toBBCode($activity['name']); $item['content-warning'] = HTML::toBBCode($activity['summary']); - $item['body'] = HTML::toBBCode($activity['content']); + $item['body'] = self::convertMentions(HTML::toBBCode($activity['content'])); $item['location'] = $activity['location']; -// $item['attach'] = $activity['attachments']; -// $item['tag'] = self::constructTagList($activity['tags'], $activity['sensitive']); + $item['tag'] = self::constructTagList($activity['tags'], $activity['sensitive']); $item['app'] = $activity['service']; - $item['plink'] = $activity['alternate-url']; + $item['plink'] = defaults($activity, 'alternate-url', $item['uri']); + + $item = self::constructAttachList($activity['attachments'], $item); $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB; $item['source'] = $body; -// $item[''] = $activity['context']; $item['conversation-uri'] = $activity['conversation']; foreach ($activity['receiver'] as $receiver) { @@ -996,19 +962,19 @@ class ActivityPub if (!empty($item_id) && ($item['uid'] == 0)) { Item::distribute($item_id); } -//print_r($item); } -// $item[''] = $activity['receiver']; } - private static function announceItem($item) + private static function activityItem($data) { -// print_r($item); - } - - private static function activityItem($item) - { - // print_r($item); + logger('Activity "' . $data['type'] . '" for ' . $data['object']); + $items = Item::select(['id'], ['uri' => $data['object']]); + while ($item = Item::fetch($items)) { + logger('Activity ' . $data['type'] . ' for item ' . $item['id'], LOGGER_DEBUG); + Item::performLike($item['id'], strtolower($data['type'])); + } + DBA::close($item); + logger('Activity done'); } } From fb5b6e4a1409a2f8de0b393fea184a2894fd2e0e Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 07:37:34 +0000 Subject: [PATCH 016/261] New uri format for our posts that is AP compatible --- src/Model/Item.php | 8 +------- src/Protocol/Diaspora.php | 14 +++++--------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/Model/Item.php b/src/Model/Item.php index 9406df2ac8..69a3783bbd 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2329,13 +2329,7 @@ class Item extends BaseObject $guid = System::createGUID(32); } - $hostname = self::getApp()->get_hostname(); - - $user = DBA::selectFirst('user', ['nickname'], ['uid' => $uid]); - - $uri = "urn:X-dfrn:" . $hostname . ':' . $user['nickname'] . ':' . $guid; - - return $uri; + return self::getApp()->get_baseurl() . '/object/' . $guid; } /** diff --git a/src/Protocol/Diaspora.php b/src/Protocol/Diaspora.php index 7af8dbd424..11a0e5996e 100644 --- a/src/Protocol/Diaspora.php +++ b/src/Protocol/Diaspora.php @@ -1592,17 +1592,13 @@ class Diaspora if (DBA::isResult($item)) { return $item["uri"]; } elseif (!$onlyfound) { - $contact = Contact::getDetailsByAddr($author, 0); - if (!empty($contact['network'])) { - $prefix = 'urn:X-' . $contact['network'] . ':'; - } else { - // This fallback should happen most unlikely - $prefix = 'urn:X-dspr:'; - } + $person = self::personByHandle($author); - $author_parts = explode('@', $author); + $parts = parse_url($person['url']); + unset($parts['path']); + $host_url = Network::unparseURL($parts); - return $prefix . $author_parts[1] . ':' . $author_parts[0] . ':'. $guid; + return $host_url . '/object/' . $guid; } return ""; From 957c70d1c6664ff845f32246b89271e2357177c0 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 07:40:19 +0000 Subject: [PATCH 017/261] Several improvements --- src/Protocol/ActivityPub.php | 71 +++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 54c479b50c..6489ef8121 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -136,7 +136,7 @@ class ActivityPub 'toot' => 'http://joinmastodon.org/ns#']]]; $data['type'] = 'Create'; - $data['id'] = $item['plink']; + $data['id'] = $item['uri']; $data['actor'] = $item['author-link']; $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; $data['object'] = self::createNote($item); @@ -147,18 +147,31 @@ class ActivityPub { $data = []; $data['type'] = 'Note'; - $data['id'] = $item['plink']; - //$data['context'] = $data['conversation'] = $item['parent-uri']; + $data['id'] = $item['uri']; + + if ($item['uri'] != $item['thr-parent']) { + $data['inReplyTo'] = $item['thr-parent']; + } + + $conversation = DBA::selectFirst('conversation', ['conversation-uri'], ['item-uri' => $item['parent-uri']]); + if (DBA::isResult($conversation) && !empty($conversation['conversation-uri'])) { + $conversation_uri = $conversation['conversation-uri']; + } else { + $conversation_uri = $item['parent-uri']; + } + + $data['context'] = $data['conversation'] = $conversation_uri; $data['actor'] = $item['author-link']; $data['to'] = []; if (!$item['private']) { - $data['to'][] = '"https://pleroma.soykaf.com/users/heluecht"'; + $data['to'][] = 'https://www.w3.org/ns/activitystreams#Public'; } $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); $data['attributedTo'] = $item['author-link']; $data['name'] = BBCode::convert($item['title'], false, 7); $data['content'] = BBCode::convert($item['body'], false, 7); + $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; //$data['summary'] = ''; // Ignore by now //$data['sensitive'] = false; // - Query NSFW //$data['emoji'] = []; // Ignore by now @@ -548,12 +561,6 @@ class ActivityPub return; } - $receivers = self::getReceivers($activity); - if (empty($receivers)) { - logger('No receivers found', LOGGER_DEBUG); - return; - } - // ---------------------------------- /* // unhandled @@ -573,12 +580,19 @@ class ActivityPub */ if (!in_array($activity['type'], ['Like', 'Dislike'])) { + $receivers = self::getReceivers($activity); + if (empty($receivers)) { + logger('No receivers found', LOGGER_DEBUG); + return; + } + $item = self::fetchObject($object_url, $activity['object']); if (empty($item)) { logger("Object data couldn't be processed", LOGGER_DEBUG); return; } } else { + $receivers = []; $item['object'] = $object_url; $item['receiver'] = []; $item['type'] = $activity['type']; @@ -659,7 +673,7 @@ class ActivityPub } if (!empty($activity['instrument'])) { - $item['service'] = self::processElement($activity, 'instrument', 'name', 'Service'); + $item['service'] = self::processElement($activity, 'instrument', 'name', 'type', 'Service'); } return $item; } @@ -670,19 +684,28 @@ class ActivityPub if (empty($data)) { $data = $object; if (empty($data)) { - logger('Empty content'); + logger('Empty content', LOGGER_DEBUG); return false; + } elseif (is_string($data)) { + logger('No object array provided.', LOGGER_DEBUG); + $item = Item::selectFirst([], ['uri' => $data]); + if (!DBA::isResult($item)) { + logger('Object with url ' . $data . ' was not found locally.', LOGGER_DEBUG); + return false; + } + logger('Using already stored item', LOGGER_DEBUG); + $data = self::createNote($item); } else { - logger('Using provided object'); + logger('Using provided object', LOGGER_DEBUG); } } if (empty($data['type'])) { - logger('Empty type'); + logger('Empty type', LOGGER_DEBUG); return false; } else { $type = $data['type']; - logger('Type ' . $type); + logger('Type ' . $type, LOGGER_DEBUG); } if (in_array($type, ['Note', 'Article', 'Video'])) { @@ -745,10 +768,11 @@ class ActivityPub $item['name'] = defaults($object, 'name', $item['name']); $item['summary'] = defaults($object, 'summary', null); $item['content'] = defaults($object, 'content', null); - $item['location'] = self::processElement($object, 'location', 'name', 'Place'); + $item['source'] = defaults($object, 'source', null); + $item['location'] = self::processElement($object, 'location', 'name', 'type', 'Place'); $item['attachments'] = defaults($object, 'attachment', null); $item['tags'] = defaults($object, 'tag', null); - $item['service'] = self::processElement($object, 'instrument', 'name', 'Service'); + $item['service'] = self::processElement($object, 'instrument', 'name', 'type', 'Service'); $item['alternate-url'] = self::processElement($object, 'url', 'href'); $item['receiver'] = self::getReceivers($object); @@ -828,7 +852,7 @@ class ActivityPub return $item; } - private static function processElement($array, $element, $key, $type = null) + private static function processElement($array, $element, $key, $type = null, $type_value = null) { if (empty($array)) { return false; @@ -842,7 +866,7 @@ class ActivityPub return $array[$element]; } - if (is_null($type)) { + if (is_null($type_value)) { if (!empty($array[$element][$key])) { return $array[$element][$key]; } @@ -854,7 +878,7 @@ class ActivityPub return false; } - if (!empty($array[$element][$key]) && !empty($array[$element]['type']) && ($array[$element]['type'] == $type)) { + if (!empty($array[$element][$key]) && !empty($array[$element][$type]) && ($array[$element][$type] == $type_value)) { return $array[$element][$key]; } @@ -945,6 +969,11 @@ class ActivityPub $item = self::constructAttachList($activity['attachments'], $item); + $source = self::processElement($activity, 'source', 'content', 'mediaType', 'text/bbcode'); + if (!empty($source)) { + $item['body'] = $source; + } + $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB; $item['source'] = $body; $item['conversation-uri'] = $activity['conversation']; @@ -974,7 +1003,7 @@ class ActivityPub Item::performLike($item['id'], strtolower($data['type'])); } DBA::close($item); - logger('Activity done'); + logger('Activity done', LOGGER_DEBUG); } } From a0942963c9f3881c61c05b4c6b9943f6ebf4d686 Mon Sep 17 00:00:00 2001 From: Jonny Tischbein Date: Sat, 15 Sep 2018 11:06:55 +0200 Subject: [PATCH 018/261] api_friendships_detroy uid - api_user instead of local_uer, empty check for --- include/api.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/api.php b/include/api.php index 49aa4489b0..242f4bc3bb 100644 --- a/include/api.php +++ b/include/api.php @@ -3640,17 +3640,15 @@ api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', */ function api_friendships_destroy($type) { - $a = api_user(); + $uid = api_user(); - if ($a === false) { + if ($uid === false) { throw new ForbiddenException(); } - $uid = local_user(); - $contact_id = defaults($_REQUEST, 'user_id'); - if ($contact_id == null) { + if (empty($contact_id)) { logger("No user_id specified", LOGGER_DEBUG); throw new BadRequestException("no user_id specified"); } From 35854a0ad1b05704f9b573aab0a142daa2ebc7ec Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 10:13:41 +0000 Subject: [PATCH 019/261] Adding "(AP)" to the server name when posted via AP --- src/Content/ContactSelector.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Content/ContactSelector.php b/src/Content/ContactSelector.php index dc158cfa5f..6a701f25f3 100644 --- a/src/Content/ContactSelector.php +++ b/src/Content/ContactSelector.php @@ -107,6 +107,10 @@ class ContactSelector if (DBA::isResult($r)) { $networkname = $r['platform']; + + if ($s == Protocol::ACTIVITYPUB) { + $networkname .= ' (AP)'; + } } } From 96f575fe281b3629389c7d7a5a84751cf8000675 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 10:14:56 +0000 Subject: [PATCH 020/261] Processing of personal inbox enabled --- src/Module/Inbox.php | 15 +++++++++++-- src/Protocol/ActivityPub.php | 42 +++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index dafc418f69..d47419a415 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -7,6 +7,7 @@ namespace Friendica\Module; use Friendica\BaseModule; use Friendica\Protocol\ActivityPub; use Friendica\Core\System; +use Friendica\Database\DBA; /** * ActivityPub Inbox @@ -30,11 +31,21 @@ class Inbox extends BaseModule } $tempfile = tempnam(get_temppath(), $filename); - file_put_contents($tempfile, json_encode(['header' => $_SERVER, 'body' => $postdata])); + file_put_contents($tempfile, json_encode(['argv' => $a->argv, 'header' => $_SERVER, 'body' => $postdata])); logger('Incoming message stored under ' . $tempfile); - ActivityPub::processInbox($postdata, $_SERVER); + if (!empty($a->argv[1])) { + $user = DBA::selectFirst('user', ['uid'], ['nickname' => $a->argv[1]]); + if (!DBA::isResult($user)) { + System::httpExit(404); + } + $uid = $user['uid']; + } else { + $uid = 0; + } + + ActivityPub::processInbox($postdata, $_SERVER, $uid); System::httpExit(202); } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 6489ef8121..e80568487b 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -32,6 +32,20 @@ use Friendica\Network\Probe; * * Digest: https://tools.ietf.org/html/rfc5843 * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 + * + * To-do: + * + * Receiver: + * - Activities: Follow, Accept, Update + * - Object Types: Person, Tombstome + * + * Transmitter: + * - Activities: Like, Dislike, Follow, Accept, Update + * - Object Tyoes: Article, Announce, Person, Tombstone + * + * General: + * - Message distribution + * - Endpoints: Outbox, Object, Follower, Following */ class ActivityPub { @@ -187,7 +201,7 @@ class ActivityPub $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://pirati.ca/' . strtolower($activity) . '/' . System::createGUID(), + 'id' => 'https://pirati.ca/activity/' . System::createGUID(), 'type' => $activity, 'actor' => $owner['url'], 'object' => $profile['url']]; @@ -486,9 +500,9 @@ class ActivityPub return $profile; } - public static function processInbox($body, $header) + public static function processInbox($body, $header, $uid) { - logger('Incoming message', LOGGER_DEBUG); + logger('Incoming message for user ' . $uid, LOGGER_DEBUG); if (!self::verifySignature($body, $header)) { logger('Invalid signature, message will be discarded.', LOGGER_DEBUG); @@ -502,7 +516,7 @@ class ActivityPub return; } - self::processActivity($activity, $body); + self::processActivity($activity, $body, $uid); } public static function fetchOutbox($url) @@ -528,7 +542,7 @@ class ActivityPub } } - function processActivity($activity, $body = '') + function processActivity($activity, $body = '', $uid = null) { if (empty($activity['type'])) { logger('Empty type', LOGGER_DEBUG); @@ -578,21 +592,25 @@ class ActivityPub unset($activity['location']); unset($activity['signature']); */ + // Fetch all receivers from to, cc, bto and bcc + $receivers = self::getReceivers($activity); + + // When it is a delivery to a personal inbox we add that user to the receivers + if (!empty($uid)) { + $owner = User::getOwnerDataById($uid); + $additional = [$owner['url'] => $uid]; + $receivers = array_merge($receivers, $additional); + } + + logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); if (!in_array($activity['type'], ['Like', 'Dislike'])) { - $receivers = self::getReceivers($activity); - if (empty($receivers)) { - logger('No receivers found', LOGGER_DEBUG); - return; - } - $item = self::fetchObject($object_url, $activity['object']); if (empty($item)) { logger("Object data couldn't be processed", LOGGER_DEBUG); return; } } else { - $receivers = []; $item['object'] = $object_url; $item['receiver'] = []; $item['type'] = $activity['type']; From 6a8ebc8639507404be07bb7d6fd5a37189e46563 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 18:54:45 +0000 Subject: [PATCH 021/261] Contact follow and unfollow workd partially --- mod/dfrn_confirm.php | 40 +++----- mod/follow.php | 3 +- src/Model/Contact.php | 43 +++++---- src/Model/Item.php | 67 +++++++++++++ src/Protocol/ActivityPub.php | 177 +++++++++++++++++++++++++++++++---- 5 files changed, 268 insertions(+), 62 deletions(-) diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 41b5e0ef54..2a872dd414 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -28,6 +28,7 @@ use Friendica\Model\Group; use Friendica\Model\User; use Friendica\Network\Probe; use Friendica\Protocol\Diaspora; +use Friendica\Protocol\ActivityPub; use Friendica\Util\Crypto; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -335,10 +336,17 @@ function dfrn_confirm_post(App $a, $handsfree = null) intval($contact_id) ); } else { + if ($network == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $uid); + $pending = true; + } else { + $pending = false; + } + // $network !== Protocol::DFRN $network = defaults($contact, 'network', Protocol::OSTATUS); - $arr = Probe::uri($contact['url']); + $arr = Probe::uri($contact['url'], $network); $notify = defaults($contact, 'notify' , $arr['notify']); $poll = defaults($contact, 'poll' , $arr['poll']); @@ -362,30 +370,12 @@ function dfrn_confirm_post(App $a, $handsfree = null) DBA::delete('intro', ['id' => $intro_id]); - $r = q("UPDATE `contact` SET `name-date` = '%s', - `uri-date` = '%s', - `addr` = '%s', - `notify` = '%s', - `poll` = '%s', - `blocked` = 0, - `pending` = 0, - `network` = '%s', - `writable` = %d, - `hidden` = %d, - `rel` = %d - WHERE `id` = %d - ", - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape(DateTimeFormat::utcNow()), - DBA::escape($addr), - DBA::escape($notify), - DBA::escape($poll), - DBA::escape($network), - intval($writable), - intval($hidden), - intval($new_relation), - intval($contact_id) - ); + $fields = ['name-date' => DateTimeFormat::utcNow(), + 'uri-date' => DateTimeFormat::utcNow(), 'addr' => $addr, + 'notify' => $notify, 'poll' => $poll, 'blocked' => false, + 'pending' => $pending, 'network' => $network, + 'writable' => $writable, 'hidden' => $hidden, 'rel' => $new_relation]; + DBA::update('contact', $fields, ['id' => $contact_id]); } if (!DBA::isResult($r)) { diff --git a/mod/follow.php b/mod/follow.php index 627ab52033..65028a70e0 100644 --- a/mod/follow.php +++ b/mod/follow.php @@ -31,7 +31,8 @@ function follow_post(App $a) // This is just a precaution if maybe this page is called somewhere directly via POST $_SESSION['fastlane'] = $url; - $result = Contact::createFromProbe($uid, $url, true); + $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); +// $result = Contact::createFromProbe($uid, $url, true); if ($result['success'] == false) { if ($result['message']) { diff --git a/src/Model/Contact.php b/src/Model/Contact.php index ab804ab7f1..2bcb86327b 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -16,6 +16,7 @@ use Friendica\Database\DBA; use Friendica\Model\Profile; use Friendica\Network\Probe; use Friendica\Object\Image; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\DFRN; use Friendica\Protocol\OStatus; @@ -555,6 +556,8 @@ class Contact extends BaseObject } } elseif ($contact['network'] == Protocol::DIASPORA) { Diaspora::sendUnshare($user, $contact); + } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Undo', $contact['url'], '', $user['uid']); } } @@ -1054,7 +1057,6 @@ class Contact extends BaseObject if (!x($contact, 'avatar')) { $update_contact = true; } - if (!$update_contact || $no_update) { return $contact_id; } @@ -1495,10 +1497,11 @@ class Contact extends BaseObject } /** - * @param integer $id contact id + * @param integer $id contact id + * @param string $network Optional network we are probing for * @return boolean */ - public static function updateFromProbe($id) + public static function updateFromProbe($id, $network = '') { /* Warning: Never ever fetch the public key via Probe::uri and write it into the contacts. @@ -1511,10 +1514,10 @@ class Contact extends BaseObject return false; } - $ret = Probe::uri($contact["url"]); + $ret = Probe::uri($contact["url"], $network); // If Probe::uri fails the network code will be different - if ($ret["network"] != $contact["network"]) { + if (($ret["network"] != $contact["network"]) && ($ret["network"] != $network)) { return false; } @@ -1537,14 +1540,15 @@ class Contact extends BaseObject DBA::update( 'contact', [ - 'url' => $ret['url'], - 'nurl' => normalise_link($ret['url']), - 'addr' => $ret['addr'], - 'alias' => $ret['alias'], - 'batch' => $ret['batch'], - 'notify' => $ret['notify'], - 'poll' => $ret['poll'], - 'poco' => $ret['poco'] + 'url' => $ret['url'], + 'nurl' => normalise_link($ret['url']), + 'network' => $ret['network'], + 'addr' => $ret['addr'], + 'alias' => $ret['alias'], + 'batch' => $ret['batch'], + 'notify' => $ret['notify'], + 'poll' => $ret['poll'], + 'poco' => $ret['poco'] ], ['id' => $id] ); @@ -1686,7 +1690,7 @@ class Contact extends BaseObject $hidden = (($ret['network'] === Protocol::MAIL) ? 1 : 0); - if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA])) { + if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { $writeable = 1; } @@ -1766,6 +1770,9 @@ class Contact extends BaseObject } elseif ($contact['network'] == Protocol::DIASPORA) { $ret = Diaspora::sendShare($a->user, $contact); logger('share returns: ' . $ret); + } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { + $ret = ActivityPub::transmitActivity('Follow', $contact['url'], $uid); + logger('Follow returns: ' . $ret); } } @@ -1814,7 +1821,7 @@ class Contact extends BaseObject return $contact; } - public static function addRelationship($importer, $contact, $datarray, $item, $sharing = false) { + public static function addRelationship($importer, $contact, $datarray, $item = '', $sharing = false) { // Should always be set if (empty($datarray['author-id'])) { return; @@ -1839,13 +1846,17 @@ class Contact extends BaseObject DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true], ['id' => $contact['id'], 'uid' => $importer['uid']]); } + + if ($contact['network'] == Protocol::ACTIVITYPUB) { + ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $importer['uid']); + } + // send email notification to owner? } else { if (DBA::exists('contact', ['nurl' => normalise_link($url), 'uid' => $importer['uid'], 'pending' => true])) { logger('ignoring duplicated connection request from pending contact ' . $url); return; } - // create contact record q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`, `blocked`, `readonly`, `pending`, `writable`) diff --git a/src/Model/Item.php b/src/Model/Item.php index 69a3783bbd..68ebc690e8 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2071,6 +2071,47 @@ class Item extends BaseObject $users = []; + $owner = DBA::selectFirst('contact', ['url', 'nurl', 'alias'], ['id' => $parent['owner-id']]); + if (!DBA::isResult($owner)) { + return; + } + + $condition = ['nurl' => $owner['nurl'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + + $condition = ['alias' => $owner['url'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + + if (!empty($owner['alias'])) { + $condition = ['url' => $owner['alias'], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] == 0) { + continue; + } + + $users[$contact['uid']] = $contact['uid']; + } + DBA::close($contacts); + } +/* + $condition = ["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; @@ -2080,6 +2121,32 @@ class Item extends BaseObject $users[$contact['uid']] = $contact['uid']; } + DBA::close($contacts); + + // And the same with the alias in the user contacts + $condition = ["`alias` IN (SELECT `url` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", + $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; + + $contacts = DBA::select('contact', ['uid'], $condition); + + while ($contact = DBA::fetch($contacts)) { + $users[$contact['uid']] = $contact['uid']; + } + + DBA::close($contacts); + + // And vice versa + $condition = ["`url` IN (SELECT `alias` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", + $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; + + $contacts = DBA::select('contact', ['uid'], $condition); + + while ($contact = DBA::fetch($contacts)) { + $users[$contact['uid']] = $contact['uid']; + } + + DBA::close($contacts); +*/ $origin_uid = 0; if ($item['uri'] != $item['parent-uri']) { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index e80568487b..78d5d44daf 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -33,14 +33,17 @@ use Friendica\Network\Probe; * Digest: https://tools.ietf.org/html/rfc5843 * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 * + * Part of the code for HTTP signing is taken from the Osada project. + * + * * To-do: * * Receiver: - * - Activities: Follow, Accept, Update + * - Activities: Undo, Update * - Object Types: Person, Tombstome * * Transmitter: - * - Activities: Like, Dislike, Follow, Accept, Update + * - Activities: Like, Dislike, Update, Undo * - Object Tyoes: Article, Announce, Person, Tombstone * * General: @@ -77,7 +80,7 @@ class ActivityPub Network::post($target, $content, $headers); $return_code = BaseObject::getApp()->get_curl_code(); - echo $return_code."\n"; + logger('Transmit to ' . $target . ' returned ' . $return_code); } /** @@ -206,6 +209,28 @@ class ActivityPub 'actor' => $owner['url'], 'object' => $profile['url']]; + logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); + return self::transmit($data, $profile['notify'], $uid); + } + + public static function transmitContactActivity($activity, $target, $id, $uid) + { + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + + if (empty($id)) { + $id = 'https://pirati.ca/activity/' . System::createGUID(); + } + + $owner = User::getOwnerDataById($uid); + $data = ['@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'type' => $activity, + 'actor' => $owner['url'], + 'object' => ['id' => $id, 'type' => 'Follow', + 'actor' => $owner['url'], + 'object' => $profile['url']]]; + + logger('Sending ' . $activity . ' to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); } @@ -604,14 +629,24 @@ class ActivityPub logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); - if (!in_array($activity['type'], ['Like', 'Dislike'])) { + logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); + + // Fetch the content only on activities where this matters + if (in_array($activity['type'], ['Create', 'Update', 'Announce'])) { $item = self::fetchObject($object_url, $activity['object']); if (empty($item)) { logger("Object data couldn't be processed", LOGGER_DEBUG); return; } } else { - $item['object'] = $object_url; + if (in_array($activity['type'], ['Accept'])) { + $item['object'] = self::processElement($activity, 'object', 'actor', 'type', 'Follow'); + } elseif (in_array($activity['type'], ['Undo'])) { + $item['object'] = self::processElement($activity, 'object', 'object', 'type', 'Follow'); + } else { + $item['object'] = $object_url; + } + $item['id'] = $activity['id']; $item['receiver'] = []; $item['type'] = $activity['type']; } @@ -622,8 +657,6 @@ class ActivityPub $item['receiver'] = array_merge($item['receiver'], $receivers); - logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); - switch ($activity['type']) { case 'Create': case 'Update': @@ -632,11 +665,25 @@ class ActivityPub break; case 'Like': + self::likeItem($item, $body); + break; + case 'Dislike': - self::activityItem($item, $body); + break; + + case 'Delete': break; case 'Follow': + self::followUser($item); + break; + + case 'Accept': + self::acceptFollowUser($item); + break; + + case 'Undo': + self::undoFollowUser($item); break; default: @@ -962,18 +1009,42 @@ class ActivityPub } private static function createItem($activity, $body) + { + $item = []; + $item['verb'] = ACTIVITY_POST; + $item['parent-uri'] = $activity['reply-to-uri']; + + if ($activity['reply-to-uri'] == $activity['uri']) { + $item['gravity'] = GRAVITY_PARENT; + $item['object-type'] = ACTIVITY_OBJ_NOTE; + } else { + $item['gravity'] = GRAVITY_COMMENT; + $item['object-type'] = ACTIVITY_OBJ_COMMENT; + } + + self::postItem($activity, $item, $body); + } + + private static function likeItem($activity, $body) + { + $item = []; + $item['verb'] = ACTIVITY_LIKE; + $item['parent-uri'] = $activity['object']; + $item['gravity'] = GRAVITY_ACTIVITY; + $item['object-type'] = ACTIVITY_OBJ_NOTE; + + self::postItem($activity, $item, $body); + } + + private static function postItem($activity, $item, $body) { /// @todo What to do with $activity['context']? - $item = []; $item['network'] = Protocol::ACTIVITYPUB; $item['private'] = !in_array(0, $activity['receiver']); $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); $item['uri'] = $activity['uri']; - $item['parent-uri'] = $activity['reply-to-uri']; - $item['verb'] = ACTIVITY_POST; - $item['object-type'] = ACTIVITY_OBJ_NOTE; /// Todo? $item['created'] = $activity['published']; $item['edited'] = $activity['updated']; $item['guid'] = $activity['uuid']; @@ -1012,16 +1083,82 @@ class ActivityPub } } - private static function activityItem($data) + private static function followUser($activity) { - logger('Activity "' . $data['type'] . '" for ' . $data['object']); - $items = Item::select(['id'], ['uri' => $data['object']]); - while ($item = Item::fetch($items)) { - logger('Activity ' . $data['type'] . ' for item ' . $item['id'], LOGGER_DEBUG); - Item::performLike($item['id'], strtolower($data['type'])); + if (empty($activity['receiver'][$activity['object']])) { + return; } - DBA::close($item); - logger('Activity done', LOGGER_DEBUG); + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (!empty($cid)) { + $contact = DBA::selectFirst('contact', [], ['id' => $cid]); + } else { + $contact = false; + } + + $item = ['author-id' => Contact::getIdForURL($activity['owner'])]; + + Contact::addRelationship($owner, $contact, $item); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + return; + } + + $contact = DBA::selectFirst('contact', ['network'], ['id' => $cid]); + if ($contact['network'] != Protocol::ACTIVITYPUB) { + Contact::updateFromProbe($cid, Protocol::ACTIVITYPUB); + } + + DBA::update('contact', ['hub-verify' => $activity['id']], ['id' => $cid]); + logger('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); } + private static function acceptFollowUser($activity) + { + if (empty($activity['receiver'][$activity['object']])) { + return; + } + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG); + return; + } + + $fields = ['pending' => false]; + $condition = ['id' => $cid, 'pending' => true]; + DBA::update('comtact', $fields, $condition); + logger('Accept contact request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG); + } + + private static function undoFollowUser($activity) + { + if (empty($activity['receiver'][$activity['object']])) { + return; + } + + $uid = $activity['receiver'][$activity['object']]; + $owner = User::getOwnerDataById($uid); + + $cid = Contact::getIdForURL($activity['owner'], $uid); + if (empty($cid)) { + logger('No contact found for ' . $activity['owner'], LOGGER_DEBUG); + return; + } + + $contact = DBA::selectFirst('contact', [], ['id' => $cid]); + if (!DBA::isResult($contact)) { + return; + } + + Contact::removeFollower($owner, $contact); + logger('Undo following request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG); + } } From 86bd28370525a4fb6d4a55ea48cb509ef58ce5ff Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 20:31:05 +0000 Subject: [PATCH 022/261] The whole contact handling does work now, yeah ... --- mod/dfrn_confirm.php | 2 +- src/Model/Contact.php | 6 +++--- src/Protocol/ActivityPub.php | 27 ++++++++++++++++++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 2a872dd414..4abaf978fb 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -337,7 +337,7 @@ function dfrn_confirm_post(App $a, $handsfree = null) ); } else { if ($network == Protocol::ACTIVITYPUB) { - ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $uid); + ActivityPub::transmitContactAccept($contact['url'], $contact['hub-verify'], $uid); $pending = true; } else { $pending = false; diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 2bcb86327b..1e61c0d10d 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -557,7 +557,7 @@ class Contact extends BaseObject } elseif ($contact['network'] == Protocol::DIASPORA) { Diaspora::sendUnshare($user, $contact); } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { - ActivityPub::transmitContactActivity('Undo', $contact['url'], '', $user['uid']); + ActivityPub::transmitContactUndo($contact['url'], '', $user['uid']); } } @@ -1834,7 +1834,7 @@ class Contact extends BaseObject return; } - $url = $pub_contact['url']; + $url = defaults($datarray, 'author-link', $pub_contact['url']); $name = $pub_contact['name']; $photo = $pub_contact['photo']; $nick = $pub_contact['nick']; @@ -1848,7 +1848,7 @@ class Contact extends BaseObject } if ($contact['network'] == Protocol::ACTIVITYPUB) { - ActivityPub::transmitContactActivity('Accept', $contact['url'], $contact['hub-verify'], $importer['uid']); + ActivityPub::transmitContactAccept($contact['url'], $contact['hub-verify'], $importer['uid']); } // send email notification to owner? diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 78d5d44daf..73d5231d72 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -213,7 +213,24 @@ class ActivityPub return self::transmit($data, $profile['notify'], $uid); } - public static function transmitContactActivity($activity, $target, $id, $uid) + public static function transmitContactAccept($target, $id, $uid) + { + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + + $owner = User::getOwnerDataById($uid); + $data = ['@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'type' => 'Accept', + 'actor' => $owner['url'], + 'object' => ['id' => $id, 'type' => 'Follow', + 'actor' => $profile['url'], + 'object' => $owner['url']]]; + + logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); + return self::transmit($data, $profile['notify'], $uid); + } + + public static function transmitContactUndo($target, $id, $uid) { $profile = Probe::uri($target, Protocol::ACTIVITYPUB); @@ -224,13 +241,13 @@ class ActivityPub $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', 'id' => 'https://pirati.ca/activity/' . System::createGUID(), - 'type' => $activity, + 'type' => 'Undo', 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', 'actor' => $owner['url'], 'object' => $profile['url']]]; - logger('Sending ' . $activity . ' to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); + logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); } @@ -1099,10 +1116,10 @@ class ActivityPub $contact = false; } - $item = ['author-id' => Contact::getIdForURL($activity['owner'])]; + $item = ['author-id' => Contact::getIdForURL($activity['owner']), + 'author-link' => $activity['owner']]; Contact::addRelationship($owner, $contact, $item); - $cid = Contact::getIdForURL($activity['owner'], $uid); if (empty($cid)) { return; From f5104f3e46d27e6535bd8a07a98ead5b0c6755a8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 20:48:24 +0000 Subject: [PATCH 023/261] Removed harcoded host names --- src/Protocol/ActivityPub.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 73d5231d72..df6b0342be 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -39,11 +39,11 @@ use Friendica\Network\Probe; * To-do: * * Receiver: - * - Activities: Undo, Update + * - Activities: Dislike, Update, Delete * - Object Types: Person, Tombstome * * Transmitter: - * - Activities: Like, Dislike, Update, Undo + * - Activities: Like, Dislike, Update, Delete * - Object Tyoes: Article, Announce, Person, Tombstone * * General: @@ -204,7 +204,7 @@ class ActivityPub $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'id' => System::baseUrl() . '/activity/' . System::createGUID(), 'type' => $activity, 'actor' => $owner['url'], 'object' => $profile['url']]; @@ -219,7 +219,7 @@ class ActivityPub $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'id' => System::baseUrl() . '/activity/' . System::createGUID(), 'type' => 'Accept', 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', @@ -235,12 +235,12 @@ class ActivityPub $profile = Probe::uri($target, Protocol::ACTIVITYPUB); if (empty($id)) { - $id = 'https://pirati.ca/activity/' . System::createGUID(); + $id = System::baseUrl() . '/activity/' . System::createGUID(); } $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => 'https://pirati.ca/activity/' . System::createGUID(), + 'id' => System::baseUrl() . '/activity/' . System::createGUID(), 'type' => 'Undo', 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', From 2eabe45a8e23512e31517ae8660f0576c111e32c Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 22:25:58 +0000 Subject: [PATCH 024/261] Contact reject does work now as well --- mod/dfrn_confirm.php | 6 +++++- src/Model/Contact.php | 6 +++++- src/Protocol/ActivityPub.php | 35 ++++++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/mod/dfrn_confirm.php b/mod/dfrn_confirm.php index 4abaf978fb..90d3d8990c 100644 --- a/mod/dfrn_confirm.php +++ b/mod/dfrn_confirm.php @@ -356,7 +356,7 @@ function dfrn_confirm_post(App $a, $handsfree = null) $new_relation = $contact['rel']; $writable = $contact['writable']; - if ($network === Protocol::DIASPORA) { + if (in_array($network, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) { if ($duplex) { $new_relation = Contact::FRIEND; } else { @@ -393,6 +393,10 @@ function dfrn_confirm_post(App $a, $handsfree = null) Group::addMember(User::getDefaultGroup($uid, $contact["network"]), $contact['id']); + if ($network == Protocol::ACTIVITYPUB && $duplex) { + ActivityPub::transmitActivity('Follow', $contact['url'], $uid); + } + // Let's send our user to the contact editor in case they want to // do anything special with this new friend. if ($handsfree === null) { diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 1e61c0d10d..ae42030952 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -557,7 +557,11 @@ class Contact extends BaseObject } elseif ($contact['network'] == Protocol::DIASPORA) { Diaspora::sendUnshare($user, $contact); } elseif ($contact['network'] == Protocol::ACTIVITYPUB) { - ActivityPub::transmitContactUndo($contact['url'], '', $user['uid']); + ActivityPub::transmitContactUndo($contact['url'], $user['uid']); + + if ($dissolve) { + ActivityPub::transmitContactReject($contact['url'], $contact['hub-verify'], $user['uid']); + } } } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index df6b0342be..19407b825b 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -230,17 +230,32 @@ class ActivityPub return self::transmit($data, $profile['notify'], $uid); } - public static function transmitContactUndo($target, $id, $uid) + public static function transmitContactReject($target, $id, $uid) { $profile = Probe::uri($target, Protocol::ACTIVITYPUB); - if (empty($id)) { - $id = System::baseUrl() . '/activity/' . System::createGUID(); - } - $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', 'id' => System::baseUrl() . '/activity/' . System::createGUID(), + 'type' => 'Reject', + 'actor' => $owner['url'], + 'object' => ['id' => $id, 'type' => 'Follow', + 'actor' => $profile['url'], + 'object' => $owner['url']]]; + + logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); + return self::transmit($data, $profile['notify'], $uid); + } + + public static function transmitContactUndo($target, $uid) + { + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + + $id = System::baseUrl() . '/activity/' . System::createGUID(); + + $owner = User::getOwnerDataById($uid); + $data = ['@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $id, 'type' => 'Undo', 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', @@ -1150,8 +1165,14 @@ class ActivityPub } $fields = ['pending' => false]; - $condition = ['id' => $cid, 'pending' => true]; - DBA::update('comtact', $fields, $condition); + + $contact = DBA::selectFirst('contact', ['rel'], ['id' => $cid]); + if ($contact['rel'] == Contact::FOLLOWER) { + $fields['rel'] = Contact::FRIEND; + } + + $condition = ['id' => $cid]; + DBA::update('contact', $fields, $condition); logger('Accept contact request from contact ' . $cid . ' for user ' . $uid, LOGGER_DEBUG); } From a7f5c1f4c63136901a72269ec0fdfb1f6db3e4e5 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 15 Sep 2018 23:06:03 +0000 Subject: [PATCH 025/261] Likes work now --- src/Protocol/ActivityPub.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 19407b825b..85d21d7cba 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -34,7 +34,7 @@ use Friendica\Network\Probe; * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 * * Part of the code for HTTP signing is taken from the Osada project. - * + * https://framagit.org/macgirvin/osada * * To-do: * @@ -49,6 +49,7 @@ use Friendica\Network\Probe; * General: * - Message distribution * - Endpoints: Outbox, Object, Follower, Following + * - General cleanup */ class ActivityPub { @@ -676,6 +677,19 @@ class ActivityPub } elseif (in_array($activity['type'], ['Undo'])) { $item['object'] = self::processElement($activity, 'object', 'object', 'type', 'Follow'); } else { + $item['uri'] = $activity['id']; + $item['author'] = $activity['actor']; + $item['updated'] = $item['published'] = $activity['published']; + $item['uuid'] = ''; + $item['name'] = $activity['type']; + $item['summary'] = ''; + $item['content'] = ''; + $item['location'] = ''; + $item['tags'] = []; + $item['sensitive'] = false; + $item['service'] = ''; + $item['attachments'] = []; + $item['conversation'] = ''; $item['object'] = $object_url; } $item['id'] = $activity['id']; From 1f98414bdd271ea4a6b0938f09bec6f881c450dc Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Sep 2018 05:49:13 +0000 Subject: [PATCH 026/261] Some code cleanup --- src/Protocol/ActivityPub.php | 340 +++++++++++++++-------------------- 1 file changed, 149 insertions(+), 191 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 85d21d7cba..3047853b11 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -131,7 +131,7 @@ class ActivityPub 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; $data['summary'] = $contact['about']; $data['url'] = $contact['url']; - $data['manuallyApprovesFollowers'] = false; + $data['manuallyApprovesFollowers'] = false; /// @todo $data['publicKey'] = ['id' => $contact['url'] . '#main-key', 'owner' => $contact['url'], 'publicKeyPem' => $user['pubkey']]; @@ -533,28 +533,18 @@ class ActivityPub } } -/* // To-Do - unset($data['type']); - unset($data['manuallyApprovesFollowers']); + // type, manuallyApprovesFollowers // Unhandled - unset($data['@context']); - unset($data['tag']); - unset($data['attachment']); - unset($data['image']); - unset($data['nomadicLocations']); - unset($data['signature']); - unset($data['following']); - unset($data['followers']); - unset($data['featured']); - unset($data['movedTo']); - unset($data['liked']); - unset($data['sharedInbox']); // Misskey - unset($data['isCat']); // Misskey - unset($data['kroeg:blocks']); // Kroeg - unset($data['updated']); // Kroeg -*/ + // @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked + + // Unhandled from Misskey + // sharedInbox, isCat + + // Unhandled from Kroeg + // kroeg:blocks, updated + return $profile; } @@ -600,7 +590,72 @@ class ActivityPub } } - function processActivity($activity, $body = '', $uid = null) + private static function prepareObjectData($activity, $uid) + { + $actor = self::processElement($activity, 'actor', 'id'); + if (empty($actor)) { + logger('Empty actor', LOGGER_DEBUG); + return []; + } + + // Fetch all receivers from to, cc, bto and bcc + $receivers = self::getReceivers($activity); + + // When it is a delivery to a personal inbox we add that user to the receivers + if (!empty($uid)) { + $owner = User::getOwnerDataById($uid); + $additional = [$owner['url'] => $uid]; + $receivers = array_merge($receivers, $additional); + } + + logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); + + if (is_string($activity['object'])) { + $object_url = $activity['object']; + } elseif (!empty($activity['object']['id'])) { + $object_url = $activity['object']['id']; + } else { + logger('No object found', LOGGER_DEBUG); + return []; + } + + // Fetch the content only on activities where this matters + if (in_array($activity['type'], ['Create', 'Update', 'Announce'])) { + $object_data = self::fetchObject($object_url, $activity['object']); + if (empty($object_data)) { + logger("Object data couldn't be processed", LOGGER_DEBUG); + return []; + } + } elseif ($activity['type'] == 'Accept') { + $object_data = []; + $object_data['object_type'] = self::processElement($activity, 'object', 'type'); + $object_data['object'] = self::processElement($activity, 'object', 'actor'); + } elseif ($activity['type'] == 'Undo') { + $object_data = []; + $object_data['object_type'] = self::processElement($activity, 'object', 'type'); + $object_data['object'] = self::processElement($activity, 'object', 'object'); + } elseif (in_array($activity['type'], ['Like', 'Dislike'])) { + // 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::processCommonData($activity); + $object_data['name'] = $activity['type']; + $object_data['author'] = $activity['actor']; + $object_data['object'] = $object_url; + } elseif ($activity['type'] == 'Follow') { + $object_data['id'] = $activity['id']; + $object_data['object'] = $object_url; + } + + $object_data = self::addActivityFields($object_data, $activity); + + $object_data['type'] = $activity['type']; + $object_data['owner'] = $actor; + $object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers); + + return $object_data; + } + + private static function processActivity($activity, $body = '', $uid = null) { if (empty($activity['type'])) { logger('Empty type', LOGGER_DEBUG); @@ -618,118 +673,53 @@ class ActivityPub } - $actor = self::processElement($activity, 'actor', 'id'); - if (empty($actor)) { - logger('Empty actor - 2', LOGGER_DEBUG); - return; - } - - if (is_string($activity['object'])) { - $object_url = $activity['object']; - } elseif (!empty($activity['object']['id'])) { - $object_url = $activity['object']['id']; - } else { - logger('No object found', LOGGER_DEBUG); - return; - } - - // ---------------------------------- -/* - // unhandled - unset($activity['@context']); - unset($activity['id']); - // Non standard - unset($activity['title']); - unset($activity['atomUri']); - unset($activity['context_id']); - unset($activity['statusnetConversationId']); + // title, atomUri, context_id, statusnetConversationId // To-Do? - unset($activity['context']); - unset($activity['location']); - unset($activity['signature']); -*/ - // Fetch all receivers from to, cc, bto and bcc - $receivers = self::getReceivers($activity); - - // When it is a delivery to a personal inbox we add that user to the receivers - if (!empty($uid)) { - $owner = User::getOwnerDataById($uid); - $additional = [$owner['url'] => $uid]; - $receivers = array_merge($receivers, $additional); - } - - logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); + // context, location, signature; logger('Processing activity: ' . $activity['type'], LOGGER_DEBUG); - // Fetch the content only on activities where this matters - if (in_array($activity['type'], ['Create', 'Update', 'Announce'])) { - $item = self::fetchObject($object_url, $activity['object']); - if (empty($item)) { - logger("Object data couldn't be processed", LOGGER_DEBUG); - return; - } - } else { - if (in_array($activity['type'], ['Accept'])) { - $item['object'] = self::processElement($activity, 'object', 'actor', 'type', 'Follow'); - } elseif (in_array($activity['type'], ['Undo'])) { - $item['object'] = self::processElement($activity, 'object', 'object', 'type', 'Follow'); - } else { - $item['uri'] = $activity['id']; - $item['author'] = $activity['actor']; - $item['updated'] = $item['published'] = $activity['published']; - $item['uuid'] = ''; - $item['name'] = $activity['type']; - $item['summary'] = ''; - $item['content'] = ''; - $item['location'] = ''; - $item['tags'] = []; - $item['sensitive'] = false; - $item['service'] = ''; - $item['attachments'] = []; - $item['conversation'] = ''; - $item['object'] = $object_url; - } - $item['id'] = $activity['id']; - $item['receiver'] = []; - $item['type'] = $activity['type']; + $object_data = self::prepareObjectData($activity, $uid); + if (empty($object_data)) { + logger('No object data found', LOGGER_DEBUG); + return; } - $item = self::addActivityFields($item, $activity); - - $item['owner'] = $actor; - - $item['receiver'] = array_merge($item['receiver'], $receivers); - switch ($activity['type']) { case 'Create': - case 'Update': case 'Announce': - self::createItem($item, $body); + self::createItem($object_data, $body); break; case 'Like': - self::likeItem($item, $body); + self::likeItem($object_data, $body); break; case 'Dislike': break; + case 'Update': + break; + case 'Delete': break; case 'Follow': - self::followUser($item); + self::followUser($object_data); break; case 'Accept': - self::acceptFollowUser($item); + if ($object_data['object_type'] == 'Follow') { + self::acceptFollowUser($object_data); + } break; case 'Undo': - self::undoFollowUser($item); + if ($object_data['object_type'] == 'Follow') { + self::undoFollowUser($object_data); + } break; default: @@ -769,24 +759,24 @@ class ActivityPub return $receivers; } - private static function addActivityFields($item, $activity) + private static function addActivityFields($object_data, $activity) { - if (!empty($activity['published']) && empty($item['published'])) { - $item['published'] = $activity['published']; + if (!empty($activity['published']) && empty($object_data['published'])) { + $object_data['published'] = $activity['published']; } - if (!empty($activity['updated']) && empty($item['updated'])) { - $item['updated'] = $activity['updated']; + if (!empty($activity['updated']) && empty($object_data['updated'])) { + $object_data['updated'] = $activity['updated']; } - if (!empty($activity['inReplyTo']) && empty($item['parent-uri'])) { - $item['parent-uri'] = self::processElement($activity, 'inReplyTo', 'id'); + if (!empty($activity['inReplyTo']) && empty($object_data['parent-uri'])) { + $object_data['parent-uri'] = self::processElement($activity, 'inReplyTo', 'id'); } if (!empty($activity['instrument'])) { - $item['service'] = self::processElement($activity, 'instrument', 'name', 'type', 'Service'); + $object_data['service'] = self::processElement($activity, 'instrument', 'name', 'type', 'Service'); } - return $item; + return $object_data; } private static function fetchObject($object_url, $object = []) @@ -849,118 +839,86 @@ class ActivityPub private static function processCommonData(&$object) { - if (empty($object['id']) || empty($object['attributedTo'])) { + if (empty($object['id'])) { return false; } - $item = []; - $item['type'] = $object['type']; - $item['uri'] = $object['id']; + $object_data = []; + $object_data['type'] = $object['type']; + $object_data['uri'] = $object['id']; if (!empty($object['inReplyTo'])) { - $item['reply-to-uri'] = self::processElement($object, 'inReplyTo', 'id'); + $object_data['reply-to-uri'] = self::processElement($object, 'inReplyTo', 'id'); } else { - $item['reply-to-uri'] = $item['uri']; + $object_data['reply-to-uri'] = $object_data['uri']; } - $item['published'] = defaults($object, 'published', null); - $item['updated'] = defaults($object, 'updated', $item['published']); + $object_data['published'] = defaults($object, 'published', null); + $object_data['updated'] = defaults($object, 'updated', $object_data['published']); - if (empty($item['published']) && !empty($item['updated'])) { - $item['published'] = $item['updated']; + if (empty($object_data['published']) && !empty($object_data['updated'])) { + $object_data['published'] = $object_data['updated']; } - $item['uuid'] = defaults($object, 'uuid', null); - $item['owner'] = $item['author'] = self::processElement($object, 'attributedTo', 'id'); - $item['context'] = defaults($object, 'context', null); - $item['conversation'] = defaults($object, 'conversation', null); - $item['sensitive'] = defaults($object, 'sensitive', null); - $item['name'] = defaults($object, 'title', null); - $item['name'] = defaults($object, 'name', $item['name']); - $item['summary'] = defaults($object, 'summary', null); - $item['content'] = defaults($object, 'content', null); - $item['source'] = defaults($object, 'source', null); - $item['location'] = self::processElement($object, 'location', 'name', 'type', 'Place'); - $item['attachments'] = defaults($object, 'attachment', null); - $item['tags'] = defaults($object, 'tag', null); - $item['service'] = self::processElement($object, 'instrument', 'name', 'type', 'Service'); - $item['alternate-url'] = self::processElement($object, 'url', 'href'); - $item['receiver'] = self::getReceivers($object); - -/* - // To-Do - unset($object['source']); + $object_data['uuid'] = defaults($object, 'uuid', null); + $object_data['owner'] = $object_data['author'] = self::processElement($object, 'attributedTo', 'id'); + $object_data['context'] = defaults($object, 'context', null); + $object_data['conversation'] = defaults($object, 'conversation', null); + $object_data['sensitive'] = defaults($object, 'sensitive', null); + $object_data['name'] = defaults($object, 'title', null); + $object_data['name'] = defaults($object, 'name', $object_data['name']); + $object_data['summary'] = defaults($object, 'summary', null); + $object_data['content'] = defaults($object, 'content', null); + $object_data['source'] = defaults($object, 'source', null); + $object_data['location'] = self::processElement($object, 'location', 'name', 'type', 'Place'); + $object_data['attachments'] = defaults($object, 'attachment', null); + $object_data['tags'] = defaults($object, 'tag', null); + $object_data['service'] = self::processElement($object, 'instrument', 'name', 'type', 'Service'); + $object_data['alternate-url'] = self::processElement($object, 'url', 'href'); + $object_data['receiver'] = self::getReceivers($object); // Unhandled - unset($object['@context']); - unset($object['type']); - unset($object['actor']); - unset($object['signature']); - unset($object['mediaType']); - unset($object['duration']); - unset($object['replies']); - unset($object['icon']); + // @context, type, actor, signature, mediaType, duration, replies, icon - // Also missing: - audience, preview, endTime, startTime, generator, image -*/ - return $item; + // Also missing: (Defined in the standard, but currently unused) + // audience, preview, endTime, startTime, generator, image + + return $object_data; } private static function processNote($object) { - $item = []; + $object_data = []; -/* // To-Do? - unset($object['emoji']); - unset($object['atomUri']); - unset($object['inReplyToAtomUri']); + // emoji, atomUri, inReplyToAtomUri // Unhandled - unset($object['contentMap']); - unset($object['announcement_count']); - unset($object['announcements']); - unset($object['context_id']); - unset($object['likes']); - unset($object['like_count']); - unset($object['inReplyToStatusId']); - unset($object['shares']); - unset($object['quoteUrl']); - unset($object['statusnetConversationId']); -*/ - return $item; + // contentMap, announcement_count, announcements, context_id, likes, like_count + // inReplyToStatusId, shares, quoteUrl, statusnetConversationId + + return $object_data; } private static function processArticle($object) { - $item = []; + $object_data = []; - return $item; + return $object_data; } private static function processVideo($object) { - $item = []; -/* + $object_data = []; + // To-Do? - unset($object['category']); - unset($object['licence']); - unset($object['language']); - unset($object['commentsEnabled']); + // category, licence, language, commentsEnabled // Unhandled - unset($object['views']); - unset($object['waitTranscoding']); - unset($object['state']); - unset($object['support']); - unset($object['subtitleLanguage']); - unset($object['likes']); - unset($object['dislikes']); - unset($object['shares']); - unset($object['comments']); -*/ - return $item; + // views, waitTranscoding, state, support, subtitleLanguage + // likes, dislikes, shares, comments + + return $object_data; } private static function processElement($array, $element, $key, $type = null, $type_value = null) From 6f3b2b65866a841e5e47b59dfa686d9c7d74f58d Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Sep 2018 09:06:09 +0000 Subject: [PATCH 027/261] Handling of unlisted posts, better uid detection --- mod/follow.php | 4 +- src/Model/Contact.php | 24 ++++------- src/Model/Item.php | 37 +--------------- src/Protocol/ActivityPub.php | 84 ++++++++++++++++++++++++++---------- 4 files changed, 73 insertions(+), 76 deletions(-) diff --git a/mod/follow.php b/mod/follow.php index 65028a70e0..ad1dd349cc 100644 --- a/mod/follow.php +++ b/mod/follow.php @@ -31,8 +31,8 @@ function follow_post(App $a) // This is just a precaution if maybe this page is called somewhere directly via POST $_SESSION['fastlane'] = $url; - $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); -// $result = Contact::createFromProbe($uid, $url, true); +// $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); + $result = Contact::createFromProbe($uid, $url, true); if ($result['success'] == false) { if ($result['message']) { diff --git a/src/Model/Contact.php b/src/Model/Contact.php index ae42030952..b6b7081626 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -1322,33 +1322,27 @@ class Contact extends BaseObject require_once 'include/conversation.php'; - // There are no posts with "uid = 0" with connector networks - // This speeds up the query a lot - $r = q("SELECT `network`, `id` AS `author-id`, `contact-type` FROM `contact` - WHERE `contact`.`nurl` = '%s' AND `contact`.`uid` = 0", - DBA::escape(normalise_link($contact_url)) - ); + $cid = Self::getIdForURL($contact_url); - if (!DBA::isResult($r)) { + $contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]); + if (!DBA::isResult($contact)) { return ''; } - if (in_array($r[0]["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { + if (in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) { $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = ? AND NOT `item`.`global`))"; } else { $sql = "`item`.`uid` = ?"; } - $author_id = intval($r[0]["author-id"]); - - $contact = ($r[0]["contact-type"] == self::ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id'); + $contact_field = ($contact["contact-type"] == self::ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id'); if ($thread_mode) { - $condition = ["`$contact` = ? AND `gravity` = ? AND " . $sql, - $author_id, GRAVITY_PARENT, local_user()]; + $condition = ["`$contact_field` = ? AND `gravity` = ? AND " . $sql, + $cid, GRAVITY_PARENT, local_user()]; } else { - $condition = ["`$contact` = ? AND `gravity` IN (?, ?) AND " . $sql, - $author_id, GRAVITY_PARENT, GRAVITY_COMMENT, local_user()]; + $condition = ["`$contact_field` = ? AND `gravity` IN (?, ?) AND " . $sql, + $cid, GRAVITY_PARENT, GRAVITY_COMMENT, local_user()]; } $params = ['order' => ['created' => true], diff --git a/src/Model/Item.php b/src/Model/Item.php index 68ebc690e8..b9cc6c2b9a 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2071,6 +2071,7 @@ class Item extends BaseObject $users = []; + /// @todo add a field "pcid" in the contact table that referrs to the public contact id. $owner = DBA::selectFirst('contact', ['url', 'nurl', 'alias'], ['id' => $parent['owner-id']]); if (!DBA::isResult($owner)) { return; @@ -2110,43 +2111,7 @@ class Item extends BaseObject } DBA::close($contacts); } -/* - $condition = ["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", - $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; - - $contacts = DBA::select('contact', ['uid'], $condition); - - while ($contact = DBA::fetch($contacts)) { - $users[$contact['uid']] = $contact['uid']; - } - - DBA::close($contacts); - - // And the same with the alias in the user contacts - $condition = ["`alias` IN (SELECT `url` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", - $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; - - $contacts = DBA::select('contact', ['uid'], $condition); - - while ($contact = DBA::fetch($contacts)) { - $users[$contact['uid']] = $contact['uid']; - } - - DBA::close($contacts); - - // And vice versa - $condition = ["`url` IN (SELECT `alias` FROM `contact` WHERE `id` = ?) AND `uid` != 0 AND NOT `blocked` AND `rel` IN (?, ?)", - $parent['owner-id'], Contact::SHARING, Contact::FRIEND]; - - $contacts = DBA::select('contact', ['uid'], $condition); - - while ($contact = DBA::fetch($contacts)) { - $users[$contact['uid']] = $contact['uid']; - } - - DBA::close($contacts); -*/ $origin_uid = 0; if ($item['uri'] != $item['parent-uri']) { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 3047853b11..2b05ff68c7 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -65,17 +65,20 @@ class ActivityPub $content = json_encode($data); + // Header data that is about to be signed. + /// @todo Add "digest" $host = parse_url($target, PHP_URL_HOST); $path = parse_url($target, PHP_URL_PATH); $date = date('r'); + $content_length = strlen($content); - $headers = ['Host: ' . $host, 'Date: ' . $date]; + $headers = ['Host: ' . $host, 'Date: ' . $date, 'Content-Length: ' . $content_length]; - $signed_data = "(request-target): post " . $path . "\nhost: " . $host . "\ndate: " . $date; + $signed_data = "(request-target): post " . $path . "\nhost: " . $host . "\ndate: " . $date . "\ncontent-length: " . $content_length; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",headers="(request-target) host date",signature="' . $signature . '"'; + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",headers="(request-target) host date content-length",signature="' . $signature . '"'; $headers[] = 'Content-Type: application/activity+json'; Network::post($target, $content, $headers); @@ -102,7 +105,7 @@ class ActivityPub return []; } - $fields = ['locality', 'region', 'country-name']; + $fields = ['locality', 'region', 'country-name', 'page-flags']; $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]); if (!DBA::isResult($profile)) { return []; @@ -131,7 +134,7 @@ class ActivityPub 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; $data['summary'] = $contact['about']; $data['url'] = $contact['url']; - $data['manuallyApprovesFollowers'] = false; /// @todo + $data['manuallyApprovesFollowers'] = in_array($profile['page-flags'], [Contact::PAGE_NORMAL, Contact::PAGE_PRVGROUP]); $data['publicKey'] = ['id' => $contact['url'] . '#main-key', 'owner' => $contact['url'], 'publicKeyPem' => $user['pubkey']]; @@ -392,7 +395,7 @@ class ActivityPub return false; } - // Check the digest if it was part of the signed data + // Check the digest when it is part of the signed data if (in_array('digest', $sig_block['headers'])) { $digest = explode('=', $headers['digest'], 2); if ($digest[0] === 'SHA-256') { @@ -409,7 +412,7 @@ class ActivityPub } } - // Check the content-length if it was part of the signed data + // Check the content-length when it is part of the signed data if (in_array('content-length', $sig_block['headers'])) { if (strlen($content) != $headers['content-length']) { return false; @@ -599,7 +602,7 @@ class ActivityPub } // Fetch all receivers from to, cc, bto and bcc - $receivers = self::getReceivers($activity); + $receivers = self::getReceivers($activity, $actor); // When it is a delivery to a personal inbox we add that user to the receivers if (!empty($uid)) { @@ -728,10 +731,13 @@ class ActivityPub } } - private static function getReceivers($activity) + private static function getReceivers($activity, $actor) { $receivers = []; + $data = self::fetchContent($actor); + $followers = defaults($data, 'followers', ''); + $elements = ['to', 'cc', 'bto', 'bcc']; foreach ($elements as $element) { if (empty($activity[$element])) { @@ -744,8 +750,25 @@ class ActivityPub } foreach ($activity[$element] as $receiver) { - if ($receiver == self::PUBLIC) { - $receivers[$receiver] = 0; + // Mastodon puts public only in "cc" not in "to" when the post should not be listed + if (($receiver == self::PUBLIC) && ($element == 'to')) { + $receivers['uid:0'] = 0; + } + + if (($receiver == self::PUBLIC)) { + $receivers['uid:-1'] = -1; + } + + if (in_array($receiver, [$followers, self::PUBLIC])) { + $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] != 0) { + $receivers['uid:' . $contact['uid']] = $contact['uid']; + } + } + DBA::close($contacts); + continue; } $condition = ['self' => true, 'nurl' => normalise_link($receiver)]; @@ -753,7 +776,7 @@ class ActivityPub if (!DBA::isResult($contact)) { continue; } - $receivers[$receiver] = $contact['uid']; + $receivers['cid:' . $contact['uid']] = $contact['uid']; } } return $receivers; @@ -875,7 +898,7 @@ class ActivityPub $object_data['tags'] = defaults($object, 'tag', null); $object_data['service'] = self::processElement($object, 'instrument', 'name', 'type', 'Service'); $object_data['alternate-url'] = self::processElement($object, 'url', 'href'); - $object_data['receiver'] = self::getReceivers($object); + $object_data['receiver'] = self::getReceivers($object, $object_data['owner']); // Unhandled // @context, type, actor, signature, mediaType, duration, replies, icon @@ -1045,7 +1068,11 @@ class ActivityPub /// @todo What to do with $activity['context']? $item['network'] = Protocol::ACTIVITYPUB; - $item['private'] = !in_array(0, $activity['receiver']); + $item['private'] = !in_array(-1, $activity['receiver']); + if (in_array(-1, $activity['receiver'])) { + $item['private'] = 2; + } + $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); $item['uri'] = $activity['uri']; @@ -1072,6 +1099,10 @@ class ActivityPub $item['conversation-uri'] = $activity['conversation']; foreach ($activity['receiver'] as $receiver) { + if ($receiver < 0) { + continue; + } + $item['uid'] = $receiver; $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true); @@ -1081,19 +1112,26 @@ class ActivityPub $item_id = Item::insert($item); logger('Storing for user ' . $item['uid'] . ': ' . $item_id); - if (!empty($item_id) && ($item['uid'] == 0)) { - Item::distribute($item_id); - } + } + } + + private static function getUserOfObject($object) + { + $self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($object), 'self' => true]); + if (!DBA::isResult(§self)) { + return false; + } else { + return $self['uid']; } } private static function followUser($activity) { - if (empty($activity['receiver'][$activity['object']])) { + $uid = self::getUserOfObject[$activity['object']]; + if (empty($uid)) { return; } - $uid = $activity['receiver'][$activity['object']]; $owner = User::getOwnerDataById($uid); $cid = Contact::getIdForURL($activity['owner'], $uid); @@ -1123,11 +1161,11 @@ class ActivityPub private static function acceptFollowUser($activity) { - if (empty($activity['receiver'][$activity['object']])) { + $uid = self::getUserOfObject[$activity['object']]; + if (empty($uid)) { return; } - $uid = $activity['receiver'][$activity['object']]; $owner = User::getOwnerDataById($uid); $cid = Contact::getIdForURL($activity['owner'], $uid); @@ -1150,11 +1188,11 @@ class ActivityPub private static function undoFollowUser($activity) { - if (empty($activity['receiver'][$activity['object']])) { + $uid = self::getUserOfObject[$activity['object']]; + if (empty($uid)) { return; } - $uid = $activity['receiver'][$activity['object']]; $owner = User::getOwnerDataById($uid); $cid = Contact::getIdForURL($activity['owner'], $uid); From 629cca19631d918a98063a8f1f8d9fa46b0c3d81 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Sep 2018 13:04:00 +0000 Subject: [PATCH 028/261] Added function to fetch missing data, code bugfixing --- src/Protocol/ActivityPub.php | 105 +++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 2b05ff68c7..12477bad6b 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -279,7 +279,6 @@ class ActivityPub public static function fetchContent($url) { $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json']); - if (!$ret['success'] || empty($ret['body'])) { return; } @@ -613,6 +612,8 @@ class ActivityPub logger('Receivers: ' . json_encode($receivers), LOGGER_DEBUG); + $public = in_array(0, $receivers); + if (is_string($activity['object'])) { $object_url = $activity['object']; } elseif (!empty($activity['object']['id'])) { @@ -647,6 +648,8 @@ class ActivityPub } elseif ($activity['type'] == 'Follow') { $object_data['id'] = $activity['id']; $object_data['object'] = $object_url; + } else { + $object_data = []; } $object_data = self::addActivityFields($object_data, $activity); @@ -738,6 +741,8 @@ class ActivityPub $data = self::fetchContent($actor); $followers = defaults($data, 'followers', ''); + logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG); + $elements = ['to', 'cc', 'bto', 'bcc']; foreach ($elements as $element) { if (empty($activity[$element])) { @@ -750,17 +755,23 @@ class ActivityPub } foreach ($activity[$element] as $receiver) { - // Mastodon puts public only in "cc" not in "to" when the post should not be listed - if (($receiver == self::PUBLIC) && ($element == 'to')) { + if ($receiver == self::PUBLIC) { $receivers['uid:0'] = 0; - } - if (($receiver == self::PUBLIC)) { - $receivers['uid:-1'] = -1; + // This will most likely catch all OStatus connections to Mastodon + $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $contacts = DBA::select('contact', ['uid'], $condition); + while ($contact = DBA::fetch($contacts)) { + if ($contact['uid'] != 0) { + $receivers['uid:' . $contact['uid']] = $contact['uid']; + } + } + DBA::close($contacts); } if (in_array($receiver, [$followers, self::PUBLIC])) { - $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND]]; + $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], + 'network' => Protocol::ACTIVITYPUB]; $contacts = DBA::select('contact', ['uid'], $condition); while ($contact = DBA::fetch($contacts)) { if ($contact['uid'] != 0) { @@ -802,26 +813,27 @@ class ActivityPub return $object_data; } - private static function fetchObject($object_url, $object = []) + private static function fetchObject($object_url, $object = [], $public = true) { - $data = self::fetchContent($object_url); - if (empty($data)) { - $data = $object; + if ($public) { + $data = self::fetchContent($object_url); if (empty($data)) { - logger('Empty content', LOGGER_DEBUG); - return false; - } elseif (is_string($data)) { - logger('No object array provided.', LOGGER_DEBUG); - $item = Item::selectFirst([], ['uri' => $data]); - if (!DBA::isResult($item)) { - logger('Object with url ' . $data . ' was not found locally.', LOGGER_DEBUG); - return false; - } - logger('Using already stored item', LOGGER_DEBUG); - $data = self::createNote($item); - } else { - logger('Using provided object', LOGGER_DEBUG); + logger('Empty content for ' . $object_url . ', check if content is available locally.', LOGGER_DEBUG); + $data = $object_url; } + } else { + logger('Using original object for url ' . $object_url, LOGGER_DEBUG); + $data = $object; + } + + if (is_string($data)) { + $item = Item::selectFirst([], ['uri' => $data]); + if (!DBA::isResult($item)) { + logger('Object with url ' . $data . ' was not found locally.', LOGGER_DEBUG); + return false; + } + logger('Using already stored item for url ' . $object_url, LOGGER_DEBUG); + $data = self::createNote($item); } if (empty($data['type'])) { @@ -1049,6 +1061,11 @@ class ActivityPub $item['object-type'] = ACTIVITY_OBJ_COMMENT; } + if (($activity['uri'] != $activity['reply-to-uri']) && !Item::exists(['uri' => $activity['reply-to-uri']])) { + logger('Parent ' . $activity['reply-to-uri'] . ' not found. Try to refetch it.'); + self::fetchMissingActivity($activity['reply-to-uri']); + } + self::postItem($activity, $item, $body); } @@ -1068,11 +1085,7 @@ class ActivityPub /// @todo What to do with $activity['context']? $item['network'] = Protocol::ACTIVITYPUB; - $item['private'] = !in_array(-1, $activity['receiver']); - if (in_array(-1, $activity['receiver'])) { - $item['private'] = 2; - } - + $item['private'] = !in_array(0, $activity['receiver']); $item['author-id'] = Contact::getIdForURL($activity['author'], 0, true); $item['owner-id'] = Contact::getIdForURL($activity['owner'], 0, true); $item['uri'] = $activity['uri']; @@ -1099,10 +1112,6 @@ class ActivityPub $item['conversation-uri'] = $activity['conversation']; foreach ($activity['receiver'] as $receiver) { - if ($receiver < 0) { - continue; - } - $item['uid'] = $receiver; $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, true); @@ -1115,10 +1124,32 @@ class ActivityPub } } + private static function fetchMissingActivity($url) + { + $object = ActivityPub::fetchContent($url); + if (empty($object)) { + logger('Activity ' . $url . ' was not fetchable, aborting.'); + return; + } + + $activity = []; + $activity['@context'] = $object['@context']; + unset($object['@context']); + $activity['id'] = $object['id']; + $activity['to'] = defaults($object, 'to', []); + $activity['cc'] = defaults($object, 'cc', []); + $activity['actor'] = $object['attributedTo']; + $activity['object'] = $object; + $activity['published'] = $object['published']; + $activity['type'] = 'Create'; + self::processActivity($activity); + logger('Activity ' . $url . ' had been fetched and processed.'); + } + private static function getUserOfObject($object) { $self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($object), 'self' => true]); - if (!DBA::isResult(§self)) { + if (!DBA::isResult($self)) { return false; } else { return $self['uid']; @@ -1127,7 +1158,7 @@ class ActivityPub private static function followUser($activity) { - $uid = self::getUserOfObject[$activity['object']]; + $uid = self::getUserOfObject($activity['object']); if (empty($uid)) { return; } @@ -1161,7 +1192,7 @@ class ActivityPub private static function acceptFollowUser($activity) { - $uid = self::getUserOfObject[$activity['object']]; + $uid = self::getUserOfObject($activity['object']); if (empty($uid)) { return; } @@ -1188,7 +1219,7 @@ class ActivityPub private static function undoFollowUser($activity) { - $uid = self::getUserOfObject[$activity['object']]; + $uid = self::getUserOfObject($activity['object']); if (empty($uid)) { return; } From 9821f173a42e676507fbead7418c885c7118de04 Mon Sep 17 00:00:00 2001 From: Jonny Tischbein Date: Sun, 16 Sep 2018 19:36:25 +0200 Subject: [PATCH 029/261] fix response --- include/api.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/api.php b/include/api.php index 242f4bc3bb..8588b2cf89 100644 --- a/include/api.php +++ b/include/api.php @@ -3696,8 +3696,14 @@ function api_friendships_destroy($type) DBA::update('contact', ['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); } - $answer = ['result' => 'ok', 'user_id' => $contact_id, 'contact' => 'contact deleted']; - return api_format_data("friendships-destroy", $type, ['result' => $answer]); + // "uid" and "self" are only needed for some internal stuff, so remove it from here + unset($contact["uid"]); + unset($contact["self"]); + + // Set screen_name since Twidere requests it + $contact["screen_name"] = $contact["nick"]; + + return api_format_data("friendships-destroy", $type, ['user' => $contact]); } api_register_func('api/friendships/destroy', 'api_friendships_destroy', true, API_METHOD_POST); From e4d28629e43f829877336f4207edd304069f9ed8 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Sep 2018 17:47:00 +0000 Subject: [PATCH 030/261] First posting tests --- mod/follow.php | 4 +- src/Protocol/ActivityPub.php | 37 +++++++++------- src/Worker/Delivery.php | 82 ++++++++++++++++++++++++++++++++++++ src/Worker/Notifier.php | 2 +- 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/mod/follow.php b/mod/follow.php index ad1dd349cc..65028a70e0 100644 --- a/mod/follow.php +++ b/mod/follow.php @@ -31,8 +31,8 @@ function follow_post(App $a) // This is just a precaution if maybe this page is called somewhere directly via POST $_SESSION['fastlane'] = $url; -// $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); - $result = Contact::createFromProbe($uid, $url, true); + $result = Contact::createFromProbe($uid, $url, true, Protocol::ACTIVITYPUB); +// $result = Contact::createFromProbe($uid, $url, true); if ($result['success'] == false) { if ($result['message']) { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 12477bad6b..ad27535ae6 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -95,8 +95,7 @@ class ActivityPub */ public static function profile($uid) { - $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application']; - + $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application', 'page-flags']; $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false, 'account_removed' => false, 'verified' => true]; $fields = ['guid', 'nickname', 'pubkey', 'account-type']; @@ -105,7 +104,7 @@ class ActivityPub return []; } - $fields = ['locality', 'region', 'country-name', 'page-flags']; + $fields = ['locality', 'region', 'country-name']; $profile = DBA::selectFirst('profile', $fields, ['uid' => $uid, 'is-default' => true]); if (!DBA::isResult($profile)) { return []; @@ -119,6 +118,7 @@ class ActivityPub $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ['uuid' => 'http://schema.org/identifier', 'sensitive' => 'as:sensitive', + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'vcard' => 'http://www.w3.org/2006/vcard/ns#']]]; $data['id'] = $contact['url']; @@ -130,7 +130,7 @@ class ActivityPub $data['outbox'] = System::baseUrl() . '/outbox/' . $user['nickname']; $data['preferredUsername'] = $user['nickname']; $data['name'] = $contact['name']; - $data['vcard:hasAddress'] = ['@type' => 'Home', 'vcard:country-name' => $profile['country-name'], + $data['vcard:hasAddress'] = ['@type' => 'vcard:Home', 'vcard:country-name' => $profile['country-name'], 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; $data['summary'] = $contact['about']; $data['url'] = $contact['url']; @@ -183,9 +183,8 @@ class ActivityPub $data['context'] = $data['conversation'] = $conversation_uri; $data['actor'] = $item['author-link']; - $data['to'] = []; if (!$item['private']) { - $data['to'][] = 'https://www.w3.org/ns/activitystreams#Public'; + $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; } $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); @@ -606,7 +605,7 @@ class ActivityPub // When it is a delivery to a personal inbox we add that user to the receivers if (!empty($uid)) { $owner = User::getOwnerDataById($uid); - $additional = [$owner['url'] => $uid]; + $additional = ['uid:' . $uid => $uid]; $receivers = array_merge($receivers, $additional); } @@ -738,10 +737,15 @@ class ActivityPub { $receivers = []; - $data = self::fetchContent($actor); - $followers = defaults($data, 'followers', ''); + if (!empty($actor)) { + $data = self::fetchContent($actor); + $followers = defaults($data, 'followers', ''); - logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG); + logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG); + } else { + logger('Empty actor', LOGGER_DEBUG); + $followers = ''; + } $elements = ['to', 'cc', 'bto', 'bcc']; foreach ($elements as $element) { @@ -757,7 +761,9 @@ class ActivityPub foreach ($activity[$element] as $receiver) { if ($receiver == self::PUBLIC) { $receivers['uid:0'] = 0; + } + if (($receiver == self::PUBLIC) && !empty($actor)) { // This will most likely catch all OStatus connections to Mastodon $condition = ['alias' => [$actor, normalise_link($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND]]; $contacts = DBA::select('contact', ['uid'], $condition); @@ -769,7 +775,7 @@ class ActivityPub DBA::close($contacts); } - if (in_array($receiver, [$followers, self::PUBLIC])) { + if (in_array($receiver, [$followers, self::PUBLIC]) && !empty($actor)) { $condition = ['nurl' => normalise_link($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB]; $contacts = DBA::select('contact', ['uid'], $condition); @@ -787,7 +793,7 @@ class ActivityPub if (!DBA::isResult($contact)) { continue; } - $receivers['cid:' . $contact['uid']] = $contact['uid']; + $receivers['uid:' . $contact['uid']] = $contact['uid']; } } return $receivers; @@ -820,6 +826,7 @@ class ActivityPub if (empty($data)) { logger('Empty content for ' . $object_url . ', check if content is available locally.', LOGGER_DEBUG); $data = $object_url; + $data = $object; } } else { logger('Using original object for url ' . $object_url, LOGGER_DEBUG); @@ -1063,7 +1070,7 @@ class ActivityPub if (($activity['uri'] != $activity['reply-to-uri']) && !Item::exists(['uri' => $activity['reply-to-uri']])) { logger('Parent ' . $activity['reply-to-uri'] . ' not found. Try to refetch it.'); - self::fetchMissingActivity($activity['reply-to-uri']); + self::fetchMissingActivity($activity['reply-to-uri'], $activity); } self::postItem($activity, $item, $body); @@ -1124,7 +1131,7 @@ class ActivityPub } } - private static function fetchMissingActivity($url) + private static function fetchMissingActivity($url, $child) { $object = ActivityPub::fetchContent($url); if (empty($object)) { @@ -1138,7 +1145,7 @@ class ActivityPub $activity['id'] = $object['id']; $activity['to'] = defaults($object, 'to', []); $activity['cc'] = defaults($object, 'cc', []); - $activity['actor'] = $object['attributedTo']; + $activity['actor'] = $activity['author']; $activity['object'] = $object; $activity['published'] = $object['published']; $activity['type'] = 'Create'; diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index e0a5c09c27..ef3a34649d 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -15,6 +15,7 @@ use Friendica\Model\Item; use Friendica\Model\Queue; use Friendica\Model\User; use Friendica\Protocol\DFRN; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Email; @@ -165,6 +166,10 @@ class Delivery extends BaseObject switch ($contact['network']) { + case Protocol::ACTIVITYPUB: + self::deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); + break; + case Protocol::DFRN: self::deliverDFRN($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); break; @@ -383,6 +388,83 @@ class Delivery extends BaseObject logger('Unknown mode ' . $cmd . ' for ' . $loc); } + /** + * @brief Deliver content via ActivityPub +q * + * @param string $cmd Command + * @param array $contact Contact record of the receiver + * @param array $owner Owner record of the sender + * @param array $items Item record of the content and the parent + * @param array $target_item Item record of the content + * @param boolean $public_message Is the content public? + * @param boolean $top_level Is it a thread starter? + * @param boolean $followup Is it an answer to a remote post? + */ + private static function deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup) + { + // We don't treat Forum posts as "wall-to-wall" to be able to post them via ActivityPub + $walltowall = $top_level && ($owner['id'] != $items[0]['contact-id']) & ($owner['account-type'] != Contact::ACCOUNT_TYPE_COMMUNITY); + + if ($public_message) { + $loc = 'public batch ' . $contact['batch']; + } else { + $loc = $contact['addr']; + } + + logger('Deliver ' . $target_item["guid"] . ' via ActivityPub to ' . $loc); + +// if (Config::get('system', 'dfrn_only') || !Config::get('system', 'diaspora_enabled')) { +// return; +// } + if ($cmd == self::MAIL) { +// ActivityPub::sendMail($target_item, $owner, $contact); + return; + } + + if ($cmd == self::SUGGESTION) { + return; + } +// if (!$contact['pubkey'] && !$public_message) { +// logger('No public key, no delivery.'); +// return; +// } + if (($target_item['deleted']) && (($target_item['uri'] === $target_item['parent-uri']) || $followup)) { + // top-level retraction + logger('ActivityPub retract: ' . $loc); +// ActivityPub::sendRetraction($target_item, $owner, $contact, $public_message); + return; + } elseif ($cmd == self::RELOCATION) { +// ActivityPub::sendAccountMigration($owner, $contact, $owner['uid']); + return; + } elseif ($followup) { + // send comments and likes to owner to relay + logger('ActivityPub followup: ' . $loc); + $data = ActivityPub::createActivityFromItem($target_item['id']); + $content = json_encode($data); + ActivityPub::transmit($content, $contact['notify'], $owner['uid']); +// ActivityPub::sendFollowup($target_item, $owner, $contact, $public_message); + return; + } elseif ($target_item['uri'] !== $target_item['parent-uri']) { + // we are the relay - send comments, likes and relayable_retractions to our conversants + logger('ActivityPub relay: ' . $loc); + $data = ActivityPub::createActivityFromItem($target_item['id']); + $content = json_encode($data); + ActivityPub::transmit($content, $contact['notify'], $owner['uid']); +// ActivityPub::sendRelay($target_item, $owner, $contact, $public_message); + return; + } elseif ($top_level && !$walltowall) { + // currently no workable solution for sending walltowall + logger('ActivityPub status: ' . $loc); + $data = ActivityPub::createActivityFromItem($target_item['id']); + $content = json_encode($data); + ActivityPub::transmit($content, $contact['notify'], $owner['uid']); +// ActivityPub::sendStatus($target_item, $owner, $contact, $public_message); + return; + } + + logger('Unknown mode ' . $cmd . ' for ' . $loc); + } + /** * @brief Deliver content via mail * diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 55f80c94d7..6a37186186 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -448,7 +448,7 @@ class Notifier } } - $condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false, + $condition = ['network' => [Protocol::DFRN, Protocol::ACTIVITYPUB], 'uid' => $owner['uid'], 'blocked' => false, 'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]; $r2 = DBA::toArray(DBA::select('contact', ['id', 'name', 'network'], $condition)); From 699a4140f9a4bf31d406b684e58ea3b8ef3417c2 Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 16 Sep 2018 20:12:48 +0000 Subject: [PATCH 031/261] Now sending does finally work - and some AP standards are improved as well --- mod/display.php | 2 +- mod/profile.php | 2 +- src/Module/Inbox.php | 2 +- src/Protocol/ActivityPub.php | 32 +++++++++++++++++++++----------- src/Worker/Delivery.php | 9 +++------ 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/mod/display.php b/mod/display.php index 21e28d5617..047f752c98 100644 --- a/mod/display.php +++ b/mod/display.php @@ -77,7 +77,7 @@ function display_init(App $a) displayShowFeed($item["id"], false); } - if (stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json')) { + if (ActivityPub::isRequest()) { $wall_item = Item::selectFirst(['id', 'uid'], ['guid' => $item['guid'], 'wall' => true]); if ($wall_item['uid'] == 180) { $data = ActivityPub::createActivityFromItem($wall_item['id']); diff --git a/mod/profile.php b/mod/profile.php index fd23964e41..a284e10f2d 100644 --- a/mod/profile.php +++ b/mod/profile.php @@ -50,7 +50,7 @@ function profile_init(App $a) DFRN::autoRedir($a, $which); } - if (stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json')) { + if (ActivityPub::isRequest()) { $user = DBA::selectFirst('user', ['uid'], ['nickname' => $which]); if ($user['uid'] == 180) { $data = ActivityPub::profile($user['uid']); diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index d47419a415..21dd77151c 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -47,6 +47,6 @@ class Inbox extends BaseModule ActivityPub::processInbox($postdata, $_SERVER, $uid); - System::httpExit(202); + System::httpExit(201); } } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index ad27535ae6..5b5d16e85a 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -55,6 +55,12 @@ class ActivityPub { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; + public static function isRequest() + { + return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') || + stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json'); + } + public static function transmit($data, $target, $uid) { $owner = User::getOwnerDataById($uid); @@ -66,19 +72,19 @@ class ActivityPub $content = json_encode($data); // Header data that is about to be signed. - /// @todo Add "digest" $host = parse_url($target, PHP_URL_HOST); $path = parse_url($target, PHP_URL_PATH); - $date = date('r'); + $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); $content_length = strlen($content); - $headers = ['Host: ' . $host, 'Date: ' . $date, 'Content-Length: ' . $content_length]; + $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; - $signed_data = "(request-target): post " . $path . "\nhost: " . $host . "\ndate: " . $date . "\ncontent-length: " . $content_length; + $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",headers="(request-target) host date content-length",signature="' . $signature . '"'; + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"'; + $headers[] = 'Content-Type: application/activity+json'; Network::post($target, $content, $headers); @@ -157,7 +163,7 @@ class ActivityPub 'toot' => 'http://joinmastodon.org/ns#']]]; $data['type'] = 'Create'; - $data['id'] = $item['uri']; + $data['id'] = $item['uri'] . '/activity'; $data['actor'] = $item['author-link']; $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; $data['object'] = self::createNote($item); @@ -210,7 +216,8 @@ class ActivityPub 'id' => System::baseUrl() . '/activity/' . System::createGUID(), 'type' => $activity, 'actor' => $owner['url'], - 'object' => $profile['url']]; + 'object' => $profile['url'], + 'to' => $profile['url']]; logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); @@ -227,7 +234,8 @@ class ActivityPub 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', 'actor' => $profile['url'], - 'object' => $owner['url']]]; + 'object' => $owner['url']], + 'to' => $profile['url']]; logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); @@ -244,7 +252,8 @@ class ActivityPub 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', 'actor' => $profile['url'], - 'object' => $owner['url']]]; + 'object' => $owner['url']], + 'to' => $profile['url']]; logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); @@ -263,7 +272,8 @@ class ActivityPub 'actor' => $owner['url'], 'object' => ['id' => $id, 'type' => 'Follow', 'actor' => $owner['url'], - 'object' => $profile['url']]]; + 'object' => $profile['url']], + 'to' => $profile['url']]; logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); return self::transmit($data, $profile['notify'], $uid); @@ -277,7 +287,7 @@ class ActivityPub */ public static function fetchContent($url) { - $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json']); + $ret = Network::curl($url, false, $redirects, ['accept_content' => 'application/activity+json, application/ld+json']); if (!$ret['success'] || empty($ret['body'])) { return; } diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index ef3a34649d..8ee00af630 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -440,24 +440,21 @@ q * // send comments and likes to owner to relay logger('ActivityPub followup: ' . $loc); $data = ActivityPub::createActivityFromItem($target_item['id']); - $content = json_encode($data); - ActivityPub::transmit($content, $contact['notify'], $owner['uid']); + ActivityPub::transmit($data, $contact['notify'], $owner['uid']); // ActivityPub::sendFollowup($target_item, $owner, $contact, $public_message); return; } elseif ($target_item['uri'] !== $target_item['parent-uri']) { // we are the relay - send comments, likes and relayable_retractions to our conversants logger('ActivityPub relay: ' . $loc); $data = ActivityPub::createActivityFromItem($target_item['id']); - $content = json_encode($data); - ActivityPub::transmit($content, $contact['notify'], $owner['uid']); + ActivityPub::transmit($data, $contact['notify'], $owner['uid']); // ActivityPub::sendRelay($target_item, $owner, $contact, $public_message); return; } elseif ($top_level && !$walltowall) { // currently no workable solution for sending walltowall logger('ActivityPub status: ' . $loc); $data = ActivityPub::createActivityFromItem($target_item['id']); - $content = json_encode($data); - ActivityPub::transmit($content, $contact['notify'], $owner['uid']); + ActivityPub::transmit($data, $contact['notify'], $owner['uid']); // ActivityPub::sendStatus($target_item, $owner, $contact, $public_message); return; } From 91d1b4de5d1b08bd240ea3d96b3a84e5caecfd6c Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 17 Sep 2018 05:51:05 +0000 Subject: [PATCH 032/261] We now use the conversation data with AP --- config/dbstructure.json | 2 +- src/Model/Conversation.php | 8 ++++---- src/Module/Inbox.php | 2 +- src/Protocol/ActivityPub.php | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/config/dbstructure.json b/config/dbstructure.json index c467ba6bc7..8767c0db16 100644 --- a/config/dbstructure.json +++ b/config/dbstructure.json @@ -215,7 +215,7 @@ "reply-to-uri": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "URI to which this item is a reply"}, "conversation-uri": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "GNU Social conversation URI"}, "conversation-href": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "GNU Social conversation link"}, - "protocol": {"type": "tinyint unsigned", "not null": "1", "default": "0", "comment": "The protocol of the item"}, + "protocol": {"type": "tinyint unsigned", "not null": "1", "default": "255", "comment": "The protocol of the item"}, "source": {"type": "mediumtext", "comment": "Original source"}, "received": {"type": "datetime", "not null": "1", "default": "0001-01-01 00:00:00", "comment": "Receiving date"} }, diff --git a/src/Model/Conversation.php b/src/Model/Conversation.php index ba50dc25e4..be1eaf2295 100644 --- a/src/Model/Conversation.php +++ b/src/Model/Conversation.php @@ -17,14 +17,14 @@ class Conversation * These constants represent the parcel format used to transport a conversation independently of the message protocol. * It currently is stored in the "protocol" field for legacy reasons. */ - const PARCEL_UNKNOWN = 0; + const PARCEL_ACTIVITYPUB = 0; const PARCEL_DFRN = 1; const PARCEL_DIASPORA = 2; const PARCEL_SALMON = 3; const PARCEL_FEED = 4; // Deprecated - const PARCEL_ACTIVITYPUB = 5; const PARCEL_SPLIT_CONVERSATION = 6; const PARCEL_TWITTER = 67; + const PARCEL_UNKNOWN = 255; /** * @brief Store the conversation data @@ -71,8 +71,8 @@ class Conversation unset($old_conv['source']); } // Update structure data all the time but the source only when its from a better protocol. - if (isset($conversation['protocol']) && isset($conversation['source']) && ($old_conv['protocol'] < $conversation['protocol']) - && ($old_conv['protocol'] != 0) && ($old_conv['protocol'] != self::PARCEL_ACTIVITYPUB)) { + if (empty($conversation['source']) || (!empty($old_conv['source']) && + ($old_conv['protocol'] < defaults($conversation, 'protocol', PARCEL_UNKNOWN)))) { unset($conversation['protocol']); unset($conversation['source']); } diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index 21dd77151c..d47419a415 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -47,6 +47,6 @@ class Inbox extends BaseModule ActivityPub::processInbox($postdata, $_SERVER, $uid); - System::httpExit(201); + System::httpExit(202); } } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 5b5d16e85a..9fb22b2bd9 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -32,6 +32,7 @@ use Friendica\Network\Probe; * * Digest: https://tools.ietf.org/html/rfc5843 * https://tools.ietf.org/html/draft-cavage-http-signatures-10#ref-15 + * https://github.com/digitalbazaar/php-json-ld * * Part of the code for HTTP signing is taken from the Osada project. * https://framagit.org/macgirvin/osada @@ -156,6 +157,19 @@ class ActivityPub { $item = Item::selectFirst([], ['id' => $item_id]); + if (!DBA::isResult($item)) { + return false; + } + + $condition = ['item-uri' => $item['uri'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB]; + $conversation = DBA::selectFirst('conversation', ['source'], $condition); + if (DBA::isResult($conversation)) { + $data = json_decode($conversation['source']); + if (!empty($data)) { + return $data; + } + } + $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', @@ -1029,6 +1043,11 @@ class ActivityPub $tag_text .= ','; } + if (empty($tag['href'])) { + //$tag['href'] + logger('Blubb!'); + } + $tag_text .= substr($tag['name'], 0, 1) . '[url=' . $tag['href'] . ']' . substr($tag['name'], 1) . '[/url]'; } } From f772ece86f4de915c006fd44409ed6d2244c0465 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 17 Sep 2018 21:13:08 +0000 Subject: [PATCH 033/261] New delivery module for ap --- mod/display.php | 2 +- src/Model/Item.php | 2 +- src/Model/Term.php | 11 +++ src/Protocol/ActivityPub.php | 135 +++++++++++++++++++++++++++++++++-- src/Worker/APDelivery.php | 28 ++++++++ src/Worker/Delivery.php | 79 -------------------- src/Worker/Notifier.php | 15 +++- 7 files changed, 183 insertions(+), 89 deletions(-) create mode 100644 src/Worker/APDelivery.php diff --git a/mod/display.php b/mod/display.php index 047f752c98..1b4508c18d 100644 --- a/mod/display.php +++ b/mod/display.php @@ -80,7 +80,7 @@ function display_init(App $a) if (ActivityPub::isRequest()) { $wall_item = Item::selectFirst(['id', 'uid'], ['guid' => $item['guid'], 'wall' => true]); if ($wall_item['uid'] == 180) { - $data = ActivityPub::createActivityFromItem($wall_item['id']); + $data = ActivityPub::createObjectFromItemID($wall_item['id']); echo json_encode($data); exit(); } diff --git a/src/Model/Item.php b/src/Model/Item.php index b9cc6c2b9a..e590c0c5c5 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -2851,7 +2851,7 @@ class Item extends BaseObject } // returns an array of contact-ids that are allowed to see this object - private static function enumeratePermissions($obj) + public static function enumeratePermissions($obj) { $allow_people = expand_acl($obj['allow_cid']); $allow_groups = Group::expand(expand_acl($obj['allow_gid'])); diff --git a/src/Model/Term.php b/src/Model/Term.php index a81241cb42..2623125329 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -33,6 +33,17 @@ class Term return $tag_text; } + public static function tagArrayFromItemId($itemid) + { + $condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => [TERM_HASHTAG, TERM_MENTION]]; + $tags = DBA::select('term', ['type', 'term', 'url'], $condition); + if (!DBA::isResult($tags)) { + return []; + } + + return DBA::toArray($tags); + } + public static function fileTextFromItemId($itemid) { $file_text = ''; diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 9fb22b2bd9..c148e26c69 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -13,6 +13,7 @@ use Friendica\Core\Protocol; use Friendica\Model\Conversation; use Friendica\Model\Contact; use Friendica\Model\Item; +use Friendica\Model\Term; use Friendica\Model\User; use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; @@ -153,6 +154,111 @@ class ActivityPub return $data; } + public static function createPermissionBlockForItem($item) + { + $data = ['to' => [], 'cc' => []]; + + $terms = Term::tagArrayFromItemId($item['id']); + + if (!$item['private']) { + $data['to'][] = self::PUBLIC; + $data['cc'][] = System::baseUrl() . '/followers/' . $item['author-nick']; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $data['cc'][] = $profile['url']; + } + } + } else { + $receiver_list = Item::enumeratePermissions($item); + + $mentioned = []; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $cid = Contact::getIdForURL($term['url'], $item['uid']); + if (!empty($cid) && in_array($cid, $receiver_list)) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); + $data['to'][] = $contact['url']; + } + } + + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); + $data['cc'][] = $contact['url']; + } + + if (empty($data['to'])) { + $data['to'] = $data['cc']; + unset($data['cc']); + } + } + + return $data; + } + + public static function fetchTargetInboxes($item) + { + $inboxes = []; + + $terms = Term::tagArrayFromItemId($item['id']); + if (!$item['private']) { + $contacts = DBA::select('contact', ['notify', 'batch'], ['uid' => $item['uid'], 'network' => Protocol::ACTIVITYPUB]); + while ($contact = DBA::fetch($contacts)) { + $contact = defaults($contact, 'batch', $contact['notify']); + $inboxes[$contact] = $contact; + } + DBA::close($contacts); + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } else { + $receiver_list = Item::enumeratePermissions($item); + + $mentioned = []; + + foreach ($terms as $term) { + if ($term['type'] != TERM_MENTION) { + continue; + } + $cid = Contact::getIdForURL($term['url'], $item['uid']); + if (!empty($cid) && in_array($cid, $receiver_list)) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); + $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } + + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); + $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); + if ($profile['network'] == Protocol::ACTIVITYPUB) { + $target = defaults($profile, 'batch', $profile['notify']); + $inboxes[$target] = $target; + } + } + } + + return $inboxes; + } + public static function createActivityFromItem($item_id) { $item = Item::selectFirst([], ['id' => $item_id]); @@ -177,13 +283,34 @@ class ActivityPub 'toot' => 'http://joinmastodon.org/ns#']]]; $data['type'] = 'Create'; - $data['id'] = $item['uri'] . '/activity'; + $data['id'] = $item['uri'] . '#activity'; $data['actor'] = $item['author-link']; - $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; + $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); + $data['object'] = self::createNote($item); return $data; } + public static function createObjectFromItemID($item_id) + { + $item = Item::selectFirst([], ['id' => $item_id]); + + if (!DBA::isResult($item)) { + return false; + } + + $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', + ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', + 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', + 'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', + 'toot' => 'http://joinmastodon.org/ns#']]]; + + $data = array_merge($data, self::createNote($item)); + + + return $data; + } + public static function createNote($item) { $data = []; @@ -203,9 +330,7 @@ class ActivityPub $data['context'] = $data['conversation'] = $conversation_uri; $data['actor'] = $item['author-link']; - if (!$item['private']) { - $data['to'] = 'https://www.w3.org/ns/activitystreams#Public'; - } + $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); $data['attributedTo'] = $item['author-link']; diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php new file mode 100644 index 0000000000..b7e881c7a3 --- /dev/null +++ b/src/Worker/APDelivery.php @@ -0,0 +1,28 @@ + $item_id]); + $data = ActivityPub::createActivityFromItem($item_id); + ActivityPub::transmit($data, $inbox, $item['uid']); + } + + return; + } +} diff --git a/src/Worker/Delivery.php b/src/Worker/Delivery.php index 8ee00af630..e0a5c09c27 100644 --- a/src/Worker/Delivery.php +++ b/src/Worker/Delivery.php @@ -15,7 +15,6 @@ use Friendica\Model\Item; use Friendica\Model\Queue; use Friendica\Model\User; use Friendica\Protocol\DFRN; -use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\Email; @@ -166,10 +165,6 @@ class Delivery extends BaseObject switch ($contact['network']) { - case Protocol::ACTIVITYPUB: - self::deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); - break; - case Protocol::DFRN: self::deliverDFRN($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup); break; @@ -388,80 +383,6 @@ class Delivery extends BaseObject logger('Unknown mode ' . $cmd . ' for ' . $loc); } - /** - * @brief Deliver content via ActivityPub -q * - * @param string $cmd Command - * @param array $contact Contact record of the receiver - * @param array $owner Owner record of the sender - * @param array $items Item record of the content and the parent - * @param array $target_item Item record of the content - * @param boolean $public_message Is the content public? - * @param boolean $top_level Is it a thread starter? - * @param boolean $followup Is it an answer to a remote post? - */ - private static function deliverActivityPub($cmd, $contact, $owner, $items, $target_item, $public_message, $top_level, $followup) - { - // We don't treat Forum posts as "wall-to-wall" to be able to post them via ActivityPub - $walltowall = $top_level && ($owner['id'] != $items[0]['contact-id']) & ($owner['account-type'] != Contact::ACCOUNT_TYPE_COMMUNITY); - - if ($public_message) { - $loc = 'public batch ' . $contact['batch']; - } else { - $loc = $contact['addr']; - } - - logger('Deliver ' . $target_item["guid"] . ' via ActivityPub to ' . $loc); - -// if (Config::get('system', 'dfrn_only') || !Config::get('system', 'diaspora_enabled')) { -// return; -// } - if ($cmd == self::MAIL) { -// ActivityPub::sendMail($target_item, $owner, $contact); - return; - } - - if ($cmd == self::SUGGESTION) { - return; - } -// if (!$contact['pubkey'] && !$public_message) { -// logger('No public key, no delivery.'); -// return; -// } - if (($target_item['deleted']) && (($target_item['uri'] === $target_item['parent-uri']) || $followup)) { - // top-level retraction - logger('ActivityPub retract: ' . $loc); -// ActivityPub::sendRetraction($target_item, $owner, $contact, $public_message); - return; - } elseif ($cmd == self::RELOCATION) { -// ActivityPub::sendAccountMigration($owner, $contact, $owner['uid']); - return; - } elseif ($followup) { - // send comments and likes to owner to relay - logger('ActivityPub followup: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendFollowup($target_item, $owner, $contact, $public_message); - return; - } elseif ($target_item['uri'] !== $target_item['parent-uri']) { - // we are the relay - send comments, likes and relayable_retractions to our conversants - logger('ActivityPub relay: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendRelay($target_item, $owner, $contact, $public_message); - return; - } elseif ($top_level && !$walltowall) { - // currently no workable solution for sending walltowall - logger('ActivityPub status: ' . $loc); - $data = ActivityPub::createActivityFromItem($target_item['id']); - ActivityPub::transmit($data, $contact['notify'], $owner['uid']); -// ActivityPub::sendStatus($target_item, $owner, $contact, $public_message); - return; - } - - logger('Unknown mode ' . $cmd . ' for ' . $loc); - } - /** * @brief Deliver content via mail * diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index 6a37186186..693a9e343d 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -16,6 +16,7 @@ use Friendica\Model\Item; use Friendica\Model\PushSubscriber; use Friendica\Model\User; use Friendica\Network\Probe; +use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Diaspora; use Friendica\Protocol\OStatus; use Friendica\Protocol\Salmon; @@ -363,9 +364,9 @@ class Notifier } // It only makes sense to distribute answers to OStatus messages to Friendica and OStatus - but not Diaspora - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN]; + $networks = [Protocol::OSTATUS, Protocol::DFRN]; } else { - $networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; + $networks = [Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL]; } } else { $public_message = false; @@ -413,6 +414,14 @@ class Notifier } } + $inboxes = ActivityPub::fetchTargetInboxes($target_item); + foreach ($inboxes as $inbox) { + logger('Deliver ' . $item_id .' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG); + + Worker::add(['priority' => $a->queue['priority'], 'created' => $a->queue['created'], 'dont_fork' => true], + 'APDelivery', $cmd, $item_id, $inbox); + } + // send salmon slaps to mentioned remote tags (@foo@example.com) in OStatus posts // They are especially used for notifications to OStatus users that don't follow us. if (!Config::get('system', 'dfrn_only') && count($url_recipients) && ($public_message || $push_notify) && $normal_mode) { @@ -448,7 +457,7 @@ class Notifier } } - $condition = ['network' => [Protocol::DFRN, Protocol::ACTIVITYPUB], 'uid' => $owner['uid'], 'blocked' => false, + $condition = ['network' => Protocol::DFRN, 'uid' => $owner['uid'], 'blocked' => false, 'pending' => false, 'archive' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]; $r2 = DBA::toArray(DBA::select('contact', ['id', 'name', 'network'], $condition)); From 464650d4d79de8fed6ecbb05049eb38792c3b71a Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 18 Sep 2018 07:28:35 +0000 Subject: [PATCH 034/261] Public posts to Pleroma do work now, limited posts to Hubzilla and Mastodon as well --- src/Protocol/ActivityPub.php | 74 +++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index c148e26c69..b4646ad54f 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -174,6 +174,7 @@ class ActivityPub } } } else { + $data['to'][] = System::baseUrl() . '/followers/' . $item['author-nick']; $receiver_list = Item::enumeratePermissions($item); $mentioned = []; @@ -209,7 +210,8 @@ class ActivityPub $terms = Term::tagArrayFromItemId($item['id']); if (!$item['private']) { - $contacts = DBA::select('contact', ['notify', 'batch'], ['uid' => $item['uid'], 'network' => Protocol::ACTIVITYPUB]); + $contacts = DBA::select('contact', ['notify', 'batch'], ['uid' => $item['uid'], + 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB]); while ($contact = DBA::fetch($contacts)) { $contact = defaults($contact, 'batch', $contact['notify']); $inboxes[$contact] = $contact; @@ -240,7 +242,8 @@ class ActivityPub $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); if ($profile['network'] == Protocol::ACTIVITYPUB) { - $target = defaults($profile, 'batch', $profile['notify']); + //$target = defaults($profile, 'batch', $profile['notify']); + $target = $profile['notify']; $inboxes[$target] = $target; } } @@ -250,12 +253,22 @@ class ActivityPub $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); if ($profile['network'] == Protocol::ACTIVITYPUB) { - $target = defaults($profile, 'batch', $profile['notify']); + //$target = defaults($profile, 'batch', $profile['notify']); + $target = $profile['notify']; $inboxes[$target] = $target; } } } + $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + if (!empty($profile['batch'])) { + unset($inboxes[$profile['batch']]); + } + + if (!empty($profile['notify'])) { + unset($inboxes[$profile['notify']]); + } + return $inboxes; } @@ -285,6 +298,13 @@ class ActivityPub $data['type'] = 'Create'; $data['id'] = $item['uri'] . '#activity'; $data['actor'] = $item['author-link']; + + $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); + + if ($item["created"] != $item["edited"]) { + $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); + } + $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); $data['object'] = self::createNote($item); @@ -311,6 +331,38 @@ class ActivityPub return $data; } + private static function createTagList($item) + { + $tags = []; + + $receiver_list = Item::enumeratePermissions($item); + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url', 'addr'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); + if (!empty($contact['addr'])) { + $mention = '@' . $contact['addr']; + } else { + $mention = '@' . $term['url']; + } + $tags[] = ['type' => 'Mention', 'href' => $contact['url'], 'name' => $mention]; + } + + $terms = Term::tagArrayFromItemId($item['id']); + foreach ($terms as $term) { + if ($term['type'] == TERM_MENTION) { + $contact = Contact::getDetailsByURL($term['url']); + if (!empty($contact['addr'])) { + $mention = '@' . $contact['addr']; + } else { + $mention = '@' . $term['url']; + } + + $tags[] = ['type' => 'Mention', 'href' => $term['url'], 'name' => $mention]; + } + } + + return $tags; + } + public static function createNote($item) { $data = []; @@ -332,16 +384,20 @@ class ActivityPub $data['actor'] = $item['author-link']; $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); $data['published'] = DateTimeFormat::utc($item["created"]."+00:00", DateTimeFormat::ATOM); - $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); + + if ($item["created"] != $item["edited"]) { + $data['updated'] = DateTimeFormat::utc($item["edited"]."+00:00", DateTimeFormat::ATOM); + } + $data['attributedTo'] = $item['author-link']; $data['name'] = BBCode::convert($item['title'], false, 7); $data['content'] = BBCode::convert($item['body'], false, 7); $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; - //$data['summary'] = ''; // Ignore by now - //$data['sensitive'] = false; // - Query NSFW - //$data['emoji'] = []; // Ignore by now - //$data['tag'] = []; /// @ToDo - //$data['attachment'] = []; // @ToDo + $data['summary'] = ''; // Ignore by now + $data['sensitive'] = false; // - Query NSFW + $data['emoji'] = []; // Ignore by now + $data['tag'] = self::createTagList($item); + $data['attachment'] = []; // @ToDo return $data; } From cb073bea613faaee591267cb7eca376ecc0e129f Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 18 Sep 2018 19:36:27 +0000 Subject: [PATCH 035/261] Private posts with Mastodon don't work - but this is expected --- src/Protocol/ActivityPub.php | 44 ++++++++++++------------------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index b4646ad54f..47f81f8bf4 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -125,9 +125,8 @@ class ActivityPub } $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', - ['uuid' => 'http://schema.org/identifier', 'sensitive' => 'as:sensitive', - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'vcard' => 'http://www.w3.org/2006/vcard/ns#']]]; + ['vcard' => 'http://www.w3.org/2006/vcard/ns#', 'uuid' => 'http://schema.org/identifier', + 'sensitive' => 'as:sensitive', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers']]]; $data['id'] = $contact['url']; $data['uuid'] = $user['guid']; @@ -174,7 +173,7 @@ class ActivityPub } } } else { - $data['to'][] = System::baseUrl() . '/followers/' . $item['author-nick']; + //$data['cc'][] = System::baseUrl() . '/followers/' . $item['author-nick']; $receiver_list = Item::enumeratePermissions($item); $mentioned = []; @@ -197,7 +196,7 @@ class ActivityPub if (empty($data['to'])) { $data['to'] = $data['cc']; - unset($data['cc']); + $data['cc'] = []; } } @@ -242,8 +241,7 @@ class ActivityPub $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); if ($profile['network'] == Protocol::ACTIVITYPUB) { - //$target = defaults($profile, 'batch', $profile['notify']); - $target = $profile['notify']; + $target = defaults($profile, 'batch', $profile['notify']); $inboxes[$target] = $target; } } @@ -253,8 +251,7 @@ class ActivityPub $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); if ($profile['network'] == Protocol::ACTIVITYPUB) { - //$target = defaults($profile, 'batch', $profile['notify']); - $target = $profile['notify']; + $target = defaults($profile, 'batch', $profile['notify']); $inboxes[$target] = $target; } } @@ -290,10 +287,10 @@ class ActivityPub } $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', - ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', - 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', - 'toot' => 'http://joinmastodon.org/ns#']]]; + ['ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', + 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', + 'conversation' => 'ostatus:conversation', + 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri']]]; $data['type'] = 'Create'; $data['id'] = $item['uri'] . '#activity'; @@ -320,10 +317,10 @@ class ActivityPub } $data = ['@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', - ['Emoji' => 'toot:Emoji', 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', - 'conversation' => 'ostatus:conversation', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', - 'toot' => 'http://joinmastodon.org/ns#']]]; + ['ostatus' => 'http://ostatus.org#', 'sensitive' => 'as:sensitive', + 'Hashtag' => 'as:Hashtag', 'atomUri' => 'ostatus:atomUri', + 'conversation' => 'ostatus:conversation', + 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri']]]; $data = array_merge($data, self::createNote($item)); @@ -335,17 +332,6 @@ class ActivityPub { $tags = []; - $receiver_list = Item::enumeratePermissions($item); - foreach ($receiver_list as $receiver) { - $contact = DBA::selectFirst('contact', ['url', 'addr'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); - if (!empty($contact['addr'])) { - $mention = '@' . $contact['addr']; - } else { - $mention = '@' . $term['url']; - } - $tags[] = ['type' => 'Mention', 'href' => $contact['url'], 'name' => $mention]; - } - $terms = Term::tagArrayFromItemId($item['id']); foreach ($terms as $term) { if ($term['type'] == TERM_MENTION) { @@ -395,7 +381,7 @@ class ActivityPub $data['source'] = ['content' => $item['body'], 'mediaType' => "text/bbcode"]; $data['summary'] = ''; // Ignore by now $data['sensitive'] = false; // - Query NSFW - $data['emoji'] = []; // Ignore by now + //$data['emoji'] = []; // Ignore by now $data['tag'] = self::createTagList($item); $data['attachment'] = []; // @ToDo return $data; From 5de4afecf138726dc483ebf90e4fe354002389bc Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 18 Sep 2018 22:09:27 +0000 Subject: [PATCH 036/261] Table for AP contacts, JSON-LD parser included --- composer.json | 3 +- composer.lock | 58 ++++++++++++++++++++++++--- config/dbstructure.json | 28 +++++++++++++ src/Protocol/ActivityPub.php | 78 +++++++++++++++++++++++------------- 4 files changed, 133 insertions(+), 34 deletions(-) diff --git a/composer.json b/composer.json index 04e4b655da..329c22664f 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "npm-asset/jgrowl": "^1.4", "npm-asset/fullcalendar": "^3.0.1", "npm-asset/cropperjs": "1.2.2", - "npm-asset/imagesloaded": "4.1.4" + "npm-asset/imagesloaded": "4.1.4", + "digitalbazaar/json-ld": "^0.4.7" }, "repositories": [ { diff --git a/composer.lock b/composer.lock index 76c20a1b93..a564990347 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "d62c3e3d6971ee63a862a22ff3cd3768", + "content-hash": "7bfbddde186f6599a2f2012bb13cbbd8", "packages": [ { "name": "asika/simple-console", @@ -149,6 +149,52 @@ }, "type": "bower-asset-library" }, + { + "name": "digitalbazaar/json-ld", + "version": "0.4.7", + "source": { + "type": "git", + "url": "https://github.com/digitalbazaar/php-json-ld.git", + "reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/digitalbazaar/php-json-ld/zipball/dc1bd23f0ee2efd27ccf636d32d2738dabcee182", + "reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "jsonld.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Digital Bazaar, Inc.", + "email": "support@digitalbazaar.com" + } + ], + "description": "A JSON-LD Processor and API implementation in PHP.", + "homepage": "https://github.com/digitalbazaar/php-json-ld", + "keywords": [ + "JSON-LD", + "Linked Data", + "RDF", + "Semantic Web", + "json", + "jsonld" + ], + "time": "2016-04-25T04:17:52+00:00" + }, { "name": "divineomega/password_exposed", "version": "v2.5.1", @@ -3145,7 +3191,7 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "homepage": "http://www.github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", @@ -3247,7 +3293,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", + "homepage": "http://www.github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -3315,7 +3361,7 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://github.com/sebastianbergmann/exporter", + "homepage": "http://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" @@ -3367,7 +3413,7 @@ } ], "description": "Snapshotting of global state", - "homepage": "https://github.com/sebastianbergmann/global-state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], @@ -3469,7 +3515,7 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "time": "2016-11-19T07:33:16+00:00" }, { diff --git a/config/dbstructure.json b/config/dbstructure.json index 8767c0db16..8f67686156 100644 --- a/config/dbstructure.json +++ b/config/dbstructure.json @@ -15,6 +15,34 @@ "name": ["UNIQUE", "name"] } }, + "apcontact": { + "comment": "ActivityPub compatible contacts - used in the ActivityPub implementation", + "fields": { + "url": {"type": "varbinary(255)", "not null": "1", "primary": "1", "comment": "URL of the contact"}, + "uuid": {"type": "varchar(255)", "comment": ""}, + "type": {"type": "varchar(20)", "not null": "1", "comment": ""}, + "following": {"type": "varchar(255)", "comment": ""}, + "followers": {"type": "varchar(255)", "comment": ""}, + "inbox": {"type": "varchar(255)", "not null": "1", "comment": ""}, + "outbox": {"type": "varchar(255)", "comment": ""}, + "sharedinbox": {"type": "varchar(255)", "comment": ""}, + "nick": {"type": "varchar(255)", "not null": "1", "default": "", "comment": ""}, + "name": {"type": "varchar(255)", "comment": ""}, + "about": {"type": "text", "comment": ""}, + "photo": {"type": "varchar(255)", "comment": ""}, + "addr": {"type": "varchar(255)", "comment": ""}, + "alias": {"type": "varchar(255)", "comment": ""}, + "pubkey": {"type": "text", "comment": ""}, + "baseurl": {"type": "varchar(255)", "comment": "baseurl of the ap contact"}, + "updated": {"type": "datetime", "not null": "1", "default": "0001-01-01 00:00:00", "comment": ""} + + }, + "indexes": { + "PRIMARY": ["url"], + "addr": ["addr(32)"], + "url": ["followers(190)"] + } + }, "attach": { "comment": "file attachments", "fields": { diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 47f81f8bf4..ffde0eff00 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -684,39 +684,63 @@ class ActivityPub return false; } - $profile = ['network' => Protocol::ACTIVITYPUB]; - $profile['nick'] = $data['preferredUsername']; - $profile['name'] = defaults($data, 'name', $profile['nick']); - $profile['guid'] = defaults($data, 'uuid', null); - $profile['url'] = $data['id']; + $apcontact = []; + $apcontact['url'] = $data['id']; + $apcontact['uuid'] = defaults($data, 'uuid', null); + $apcontact['type'] = defaults($data, 'type', null); + $apcontact['following'] = defaults($data, 'following', null); + $apcontact['followers'] = defaults($data, 'followers', null); + $apcontact['inbox'] = defaults($data, 'inbox', null); + $apcontact['outbox'] = defaults($data, 'outbox', null); + $apcontact['sharedinbox'] = self::processElement($data, 'endpoints', 'sharedInbox'); + $apcontact['nick'] = defaults($data, 'preferredUsername', null); + $apcontact['name'] = defaults($data, 'name', $apcontact['nick']); + $apcontact['about'] = defaults($data, 'summary', ''); + $apcontact['photo'] = self::processElement($data, 'icon', 'url'); + $apcontact['alias'] = self::processElement($data, 'url', 'href'); - $parts = parse_url($profile['url']); + $parts = parse_url($apcontact['url']); unset($parts['scheme']); unset($parts['path']); - $profile['addr'] = $profile['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); - $profile['alias'] = self::processElement($data, 'url', 'href'); - $profile['photo'] = self::processElement($data, 'icon', 'url'); + $apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); + + $apcontact['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); + + // Check if the address is resolvable + if (self::addrToUrl($apcontact['addr']) == $apcontact['url']) { + $parts = parse_url($apcontact['url']); + unset($parts['path']); + $apcontact['baseurl'] = Network::unparseURL($parts); + } else { + $apcontact['addr'] = null; + } + + if ($apcontact['url'] == $apcontact['alias']) { + $apcontact['alias'] = null; + } + + $apcontact['updated'] = DateTimeFormat::utcNow(); + + DBA::update('apcontact', $apcontact, ['url' => $url], true); + + // Array that is compatible to Probe::uri + $profile = ['network' => Protocol::ACTIVITYPUB]; + $profile['nick'] = $apcontact['nick']; + $profile['name'] = $apcontact['name']; + $profile['guid'] = $apcontact['uuid']; + $profile['url'] = $apcontact['url']; + $profile['addr'] = $apcontact['addr']; + $profile['alias'] = $apcontact['alias']; + $profile['photo'] = $apcontact['photo']; // $profile['community'] // $profile['keywords'] // $profile['location'] - $profile['about'] = defaults($data, 'summary', ''); - $profile['batch'] = self::processElement($data, 'endpoints', 'sharedInbox'); - $profile['notify'] = $data['inbox']; - $profile['poll'] = $data['outbox']; - $profile['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); - - // Check if the address is resolvable - if (self::addrToUrl($profile['addr']) == $profile['url']) { - $parts = parse_url($profile['url']); - unset($parts['path']); - $profile['baseurl'] = Network::unparseURL($parts); - } else { - unset($profile['addr']); - } - - if ($profile['url'] == $profile['alias']) { - unset($profile['alias']); - } + $profile['about'] = $apcontact['about']; + $profile['batch'] = $apcontact['sharedinbox']; + $profile['notify'] = $apcontact['inbox']; + $profile['poll'] = $apcontact['outbox']; + $profile['pubkey'] = $apcontact['pubkey']; + $profile['baseurl'] = $apcontact['baseurl']; // Remove all "null" fields foreach ($profile as $field => $content) { From 7dd6fb3b3c65ffee02e37419b2feed044d52e5aa Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 19 Sep 2018 22:51:51 -0400 Subject: [PATCH 037/261] Rewrite JS hooks - Use event listeners instead of homebrew hooks - Remove view/js/addon-hooks.js and its references - Update Addon docs --- doc/Addons.md | 16 +++--------- view/js/addon-hooks.js | 41 ------------------------------ view/js/main.js | 8 +++--- view/templates/head.tpl | 1 - view/theme/frio/templates/head.tpl | 1 - 5 files changed, 8 insertions(+), 59 deletions(-) delete mode 100644 view/js/addon-hooks.js diff --git a/doc/Addons.md b/doc/Addons.md index 2465db7307..ec413c6ac8 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -103,19 +103,11 @@ function _template_vars($a, &$arr) Register your addon hooks in file `addon/*addon_name*/*addon_name*.js`. ```js -Addon_registerHook(type, hookfnstr); +document.addEventListener(name, callback); ``` -*type* is the name of the hook and corresponds to a known Friendica JavaScript hook. -*hookfnstr* is the name of your JavaScript function to execute. - -No arguments are provided to your JavaScript callback function. Example: - -```javascript -function myhook_function() { - -} -``` +*name* is the name of the hook and corresponds to a known Friendica JavaScript hook. +*callback* is a JavaScript function to execute. ## Modules @@ -704,4 +696,4 @@ Here is a complete list of all hook callbacks with file locations (as of 01-Apr- ### view/js/main.js - callAddonHooks("postprocess_liveupdate"); + document.dispatchEvent(new Event('postprocess_liveupdate')); diff --git a/view/js/addon-hooks.js b/view/js/addon-hooks.js deleted file mode 100644 index 3e1cb4849e..0000000000 --- a/view/js/addon-hooks.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @file addon-hooks.js - * @brief Provide a way for add-ons to register a JavaScript hook - */ - -var addon_hooks = {}; - -/** - * @brief Register a JavaScript hook to be called from other Javascript files - * @pre the .js file from which the hook will be called is included in the document response - * @param type which type of hook i.e. where should it be called along with other hooks of the same type - * @param hookfnstr name of the JavaScript function name that needs to be called - */ -function Addon_registerHook(type, hookfnstr) -{ - if (!addon_hooks.hasOwnProperty(type)) { - addon_hooks[type] = []; - } - - addon_hooks[type].push(hookfnstr); -} - -/** - * @brief Call all registered hooks of a certain type, i.e. at the same point of the JavaScript code execution - * @param typeOfHook string indicating which type of hooks to be called among the registered hooks - */ -function callAddonHooks(typeOfHook) -{ - if (typeof addon_hooks !== 'undefined') { - var myTypeOfHooks = addon_hooks[typeOfHook]; - if (typeof myTypeOfHooks !== 'undefined') { - for (addon_hook_idx = 0; addon_hook_idx < myTypeOfHooks.length; addon_hook_idx++) { - var hookfnstr = myTypeOfHooks[addon_hook_idx]; - var hookfn = window[hookfnstr]; - if (typeof hookfn === "function") { - hookfn(); - } - } - } - } -} diff --git a/view/js/main.js b/view/js/main.js index 4788d90a83..ae9cb23d8e 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -478,14 +478,12 @@ function liveUpdate(src) { $('.wall-item-body', data).imagesLoaded(function() { updateConvItems(data); + document.dispatchEvent(new Event('postprocess_liveupdate')); + // Update the scroll position. $(window).scrollTop($(window).scrollTop() + $("section").height() - orgHeight); }); - - callAddonHooks("postprocess_liveupdate"); - }); - } function imgbright(node) { @@ -735,6 +733,8 @@ function loadScrollContent() { } else { $("#scroll-end").fadeIn('normal'); } + + document.dispatchEvent(new Event('postprocess_liveupdate')); }); } diff --git a/view/templates/head.tpl b/view/templates/head.tpl index aadbfcd8ee..eef985bc6f 100644 --- a/view/templates/head.tpl +++ b/view/templates/head.tpl @@ -45,7 +45,6 @@ - {{if is_array($addon_hooks)}} {{foreach $addon_hooks as $addon_hook}} diff --git a/view/theme/frio/templates/head.tpl b/view/theme/frio/templates/head.tpl index 7d6cadea94..0cc5bf4af8 100644 --- a/view/theme/frio/templates/head.tpl +++ b/view/theme/frio/templates/head.tpl @@ -69,7 +69,6 @@ - {{if is_array($addon_hooks)}} {{foreach $addon_hooks as $addon_hook}} From f20bed67a95160f5a6539dde49f82a8ac53161ad Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 05:00:49 +0000 Subject: [PATCH 038/261] Table "apcontact" is now in use / added functionality to handle JSON-LD --- src/Network/Probe.php | 2 +- src/Protocol/ActivityPub.php | 178 +++++++++++++++++++++++++---------- 2 files changed, 128 insertions(+), 52 deletions(-) diff --git a/src/Network/Probe.php b/src/Network/Probe.php index 0e9219c5a6..c0c627bfe9 100644 --- a/src/Network/Probe.php +++ b/src/Network/Probe.php @@ -336,7 +336,7 @@ class Probe } if (in_array(defaults($data, 'network', ''), ['', Protocol::PHANTOM])) { - $ap_profile = ActivityPub::fetchProfile($uri); + $ap_profile = ActivityPub::probeProfile($uri); if (!empty($ap_profile) && ($ap_profile['network'] == Protocol::ACTIVITYPUB)) { $data = $ap_profile; } diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index ffde0eff00..4a2394a24b 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -19,7 +19,8 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; -use Friendica\Network\Probe; +use Friendica\Core\Cache; +use digitalbazaar\jsonld; /** * @brief ActivityPub Protocol class @@ -57,6 +58,51 @@ class ActivityPub { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; +public static function jsonld_document_loader($url) +{ + $recursion = 0; + + $x = debug_backtrace(); + if ($x) { + foreach ($x as $n) { + if ($n['function'] === __FUNCTION__) { + $recursion ++; + } + } + } + + if ($recursion > 5) { + logger('jsonld bomb detected at: ' . $url); + exit(); + } + + $result = Cache::get('jsonld_document_loader:' . $url); + if (!is_null($result)) { + return $result; + } + + $data = jsonld_default_document_loader($url); + Cache::set('jsonld_document_loader:' . $url, $data, CACHE_DAY); + return $data; +} + + public static function compactJsonLD($json) + { + jsonld_set_document_loader('Friendica\Protocol\ActivityPub::jsonld_document_loader'); + + $context = (object)['as' => 'https://www.w3.org/ns/activitystreams', + 'w3sec' => 'https://w3id.org/security', + 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], + 'vcard' => (object)['@id' => 'http://www.w3.org/2006/vcard/ns#', '@type' => '@id'], + 'uuid' => (object)['@id' => 'http://schema.org/identifier', '@type' => '@id']]; + + $jsonobj = json_decode(json_encode($json)); + + $compacted = jsonld_compact($jsonobj, $context); + + return json_decode(json_encode($compacted), true); + } + public static function isRequest() { return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') || @@ -167,8 +213,8 @@ class ActivityPub if ($term['type'] != TERM_MENTION) { continue; } - $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); - if ($profile['network'] == Protocol::ACTIVITYPUB) { + $profile = self::fetchprofile($term['url']); + if (!empty($profile)) { $data['cc'][] = $profile['url']; } } @@ -221,9 +267,9 @@ class ActivityPub if ($term['type'] != TERM_MENTION) { continue; } - $profile = Probe::uri($term['url'], Protocol::ACTIVITYPUB); - if ($profile['network'] == Protocol::ACTIVITYPUB) { - $target = defaults($profile, 'batch', $profile['notify']); + $profile = self::fetchprofile($term['url']); + if (!empty($profile)) { + $target = defaults($profile, 'sharedinbox', $profile['inbox']); $inboxes[$target] = $target; } } @@ -239,9 +285,9 @@ class ActivityPub $cid = Contact::getIdForURL($term['url'], $item['uid']); if (!empty($cid) && in_array($cid, $receiver_list)) { $contact = DBA::selectFirst('contact', ['url'], ['id' => $cid, 'network' => Protocol::ACTIVITYPUB]); - $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); - if ($profile['network'] == Protocol::ACTIVITYPUB) { - $target = defaults($profile, 'batch', $profile['notify']); + $profile = self::fetchprofile($contact['url']); + if (!empty($profile['network'])) { + $target = defaults($profile, 'sharedinbox', $profile['inbox']); $inboxes[$target] = $target; } } @@ -249,21 +295,21 @@ class ActivityPub foreach ($receiver_list as $receiver) { $contact = DBA::selectFirst('contact', ['url'], ['id' => $receiver, 'network' => Protocol::ACTIVITYPUB]); - $profile = Probe::uri($contact['url'], Protocol::ACTIVITYPUB); - if ($profile['network'] == Protocol::ACTIVITYPUB) { - $target = defaults($profile, 'batch', $profile['notify']); + $profile = self::fetchprofile($contact['url']); + if (!empty($profile['network'])) { + $target = defaults($profile, 'sharedinbox', $profile['inbox']); $inboxes[$target] = $target; } } } - $profile = Probe::uri($target, Protocol::ACTIVITYPUB); - if (!empty($profile['batch'])) { - unset($inboxes[$profile['batch']]); + $profile = self::fetchprofile($item['author-link']); + if (!empty($profile['sharedinbox'])) { + unset($inboxes[$profile['sharedinbox']]); } - if (!empty($profile['notify'])) { - unset($inboxes[$profile['notify']]); + if (!empty($profile['inbox'])) { + unset($inboxes[$profile['inbox']]); } return $inboxes; @@ -389,7 +435,7 @@ class ActivityPub public static function transmitActivity($activity, $target, $uid) { - $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($target); $owner = User::getOwnerDataById($uid); @@ -401,12 +447,12 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); - return self::transmit($data, $profile['notify'], $uid); + return self::transmit($data, $profile['inbox'], $uid); } public static function transmitContactAccept($target, $id, $uid) { - $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($target); $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', @@ -419,12 +465,12 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['notify'], $uid); + return self::transmit($data, $profile['inbox'], $uid); } public static function transmitContactReject($target, $id, $uid) { - $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($target); $owner = User::getOwnerDataById($uid); $data = ['@context' => 'https://www.w3.org/ns/activitystreams', @@ -437,12 +483,12 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['notify'], $uid); + return self::transmit($data, $profile['inbox'], $uid); } public static function transmitContactUndo($target, $uid) { - $profile = Probe::uri($target, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($target); $id = System::baseUrl() . '/activity/' . System::createGUID(); @@ -457,7 +503,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['notify'], $uid); + return self::transmit($data, $profile['inbox'], $uid); } /** @@ -616,11 +662,11 @@ class ActivityPub { $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); - $profile = Probe::uri($url, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($url); if (!empty($profile)) { return $profile['pubkey']; } elseif ($url != $actor) { - $profile = Probe::uri($actor, Protocol::ACTIVITYPUB); + $profile = self::fetchprofile($actor); if (!empty($profile)) { return $profile['pubkey']; } @@ -663,14 +709,29 @@ class ActivityPub return $ret; } - /** - * Fetches a profile from the given url - * - * @param string $url profile url - * @return array - */ - public static function fetchProfile($url) + public static function fetchprofile($url, $update = false) { + if (empty($url)) { + return false; + } + + if (!$update) { + $apcontact = DBA::selectFirst('apcontact', [], ['url' => $url]); + if (DBA::isResult($apcontact)) { + return $apcontact; + } + + $apcontact = DBA::selectFirst('apcontact', [], ['alias' => $url]); + if (DBA::isResult($apcontact)) { + return $apcontact; + } + + $apcontact = DBA::selectFirst('apcontact', [], ['addr' => $url]); + if (DBA::isResult($apcontact)) { + return $apcontact; + } + } + if (empty(parse_url($url, PHP_URL_SCHEME))) { $url = self::addrToUrl($url); if (empty($url)) { @@ -704,7 +765,19 @@ class ActivityPub unset($parts['path']); $apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); - $apcontact['pubkey'] = self::processElement($data, 'publicKey', 'publicKeyPem'); + $apcontact['pubkey'] = trim(self::processElement($data, 'publicKey', 'publicKeyPem')); + + // To-Do + // manuallyApprovesFollowers + + // Unhandled + // @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked + + // Unhandled from Misskey + // sharedInbox, isCat + + // Unhandled from Kroeg + // kroeg:blocks, updated // Check if the address is resolvable if (self::addrToUrl($apcontact['addr']) == $apcontact['url']) { @@ -723,7 +796,22 @@ class ActivityPub DBA::update('apcontact', $apcontact, ['url' => $url], true); - // Array that is compatible to Probe::uri + return $apcontact; + } + + /** + * Fetches a profile from the given url into an array that is compatible to Probe::uri + * + * @param string $url profile url + * @return array + */ + public static function probeProfile($url) + { + $apcontact = self::fetchprofile($url, true); + if (empty($apcontact)) { + return false; + } + $profile = ['network' => Protocol::ACTIVITYPUB]; $profile['nick'] = $apcontact['nick']; $profile['name'] = $apcontact['name']; @@ -749,18 +837,6 @@ class ActivityPub } } - // To-Do - // type, manuallyApprovesFollowers - - // Unhandled - // @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked - - // Unhandled from Misskey - // sharedInbox, isCat - - // Unhandled from Kroeg - // kroeg:blocks, updated - return $profile; } @@ -953,8 +1029,8 @@ class ActivityPub $receivers = []; if (!empty($actor)) { - $data = self::fetchContent($actor); - $followers = defaults($data, 'followers', ''); + $profile = self::fetchprofile($actor); + $followers = defaults($profile, 'followers', ''); logger('Actor: ' . $actor . ' - Followers: ' . $followers, LOGGER_DEBUG); } else { @@ -1365,7 +1441,7 @@ class ActivityPub $activity['id'] = $object['id']; $activity['to'] = defaults($object, 'to', []); $activity['cc'] = defaults($object, 'cc', []); - $activity['actor'] = $activity['author']; + $activity['actor'] = $child['author']; $activity['object'] = $object; $activity['published'] = $object['published']; $activity['type'] = 'Create'; From 34cb0aa406969967fa521211116564805ef6b631 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 05:30:07 +0000 Subject: [PATCH 039/261] JSON-LD stuff is now in a separate file --- src/Protocol/ActivityPub.php | 47 ------------------------- src/Util/JsonLD.php | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 src/Util/JsonLD.php diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 4a2394a24b..36cc32d64b 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -19,8 +19,6 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; -use Friendica\Core\Cache; -use digitalbazaar\jsonld; /** * @brief ActivityPub Protocol class @@ -58,51 +56,6 @@ class ActivityPub { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; -public static function jsonld_document_loader($url) -{ - $recursion = 0; - - $x = debug_backtrace(); - if ($x) { - foreach ($x as $n) { - if ($n['function'] === __FUNCTION__) { - $recursion ++; - } - } - } - - if ($recursion > 5) { - logger('jsonld bomb detected at: ' . $url); - exit(); - } - - $result = Cache::get('jsonld_document_loader:' . $url); - if (!is_null($result)) { - return $result; - } - - $data = jsonld_default_document_loader($url); - Cache::set('jsonld_document_loader:' . $url, $data, CACHE_DAY); - return $data; -} - - public static function compactJsonLD($json) - { - jsonld_set_document_loader('Friendica\Protocol\ActivityPub::jsonld_document_loader'); - - $context = (object)['as' => 'https://www.w3.org/ns/activitystreams', - 'w3sec' => 'https://w3id.org/security', - 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], - 'vcard' => (object)['@id' => 'http://www.w3.org/2006/vcard/ns#', '@type' => '@id'], - 'uuid' => (object)['@id' => 'http://schema.org/identifier', '@type' => '@id']]; - - $jsonobj = json_decode(json_encode($json)); - - $compacted = jsonld_compact($jsonobj, $context); - - return json_decode(json_encode($compacted), true); - } - public static function isRequest() { return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') || diff --git a/src/Util/JsonLD.php b/src/Util/JsonLD.php new file mode 100644 index 0000000000..e8ff5888de --- /dev/null +++ b/src/Util/JsonLD.php @@ -0,0 +1,68 @@ + 5) { + logger('jsonld bomb detected at: ' . $url); + exit(); + } + + $result = Cache::get('documentLoader:' . $url); + if (!is_null($result)) { + return $result; + } + + $data = jsonld_default_document_loader($url); + Cache::set('documentLoader:' . $url, $data, CACHE_DAY); + return $data; + } + + public static function normalize($json) + { + jsonld_set_document_loader('Friendica\Util\JsonLD::documentLoader'); + + $jsonobj = json_decode(json_encode($json)); + + return jsonld_normalize($jsonobj, array('algorithm' => 'URDNA2015', 'format' => 'application/nquads')); + } + + public static function compact($json) + { + jsonld_set_document_loader('Friendica\Util\JsonLD::documentLoader'); + + $context = (object)['as' => 'https://www.w3.org/ns/activitystreams', + 'w3sec' => 'https://w3id.org/security', + 'ostatus' => (object)['@id' => 'http://ostatus.org#', '@type' => '@id'], + 'vcard' => (object)['@id' => 'http://www.w3.org/2006/vcard/ns#', '@type' => '@id'], + 'uuid' => (object)['@id' => 'http://schema.org/identifier', '@type' => '@id']]; + + $jsonobj = json_decode(json_encode($json)); + + $compacted = jsonld_compact($jsonobj, $context); + + return json_decode(json_encode($compacted), true); + } +} From 0d51474e73dc50ac0b7159cb92fcadeafb976fc1 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 05:37:01 +0000 Subject: [PATCH 040/261] Relocated function --- src/Protocol/ActivityPub.php | 72 ++++++++++-------------------------- src/Util/JsonLD.php | 35 ++++++++++++++++++ 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 36cc32d64b..bd125aaaef 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -19,6 +19,7 @@ use Friendica\Util\DateTimeFormat; use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; +use Friendica\Util\JsonLD; /** * @brief ActivityPub Protocol class @@ -522,7 +523,7 @@ class ActivityPub return false; } - $actor = self::processElement($object, 'actor', 'id'); + $actor = JsonLD::fetchElement($object, 'actor', 'id'); $headers = []; $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; @@ -706,19 +707,19 @@ class ActivityPub $apcontact['followers'] = defaults($data, 'followers', null); $apcontact['inbox'] = defaults($data, 'inbox', null); $apcontact['outbox'] = defaults($data, 'outbox', null); - $apcontact['sharedinbox'] = self::processElement($data, 'endpoints', 'sharedInbox'); + $apcontact['sharedinbox'] = JsonLD::fetchElement($data, 'endpoints', 'sharedInbox'); $apcontact['nick'] = defaults($data, 'preferredUsername', null); $apcontact['name'] = defaults($data, 'name', $apcontact['nick']); $apcontact['about'] = defaults($data, 'summary', ''); - $apcontact['photo'] = self::processElement($data, 'icon', 'url'); - $apcontact['alias'] = self::processElement($data, 'url', 'href'); + $apcontact['photo'] = JsonLD::fetchElement($data, 'icon', 'url'); + $apcontact['alias'] = JsonLD::fetchElement($data, 'url', 'href'); $parts = parse_url($apcontact['url']); unset($parts['scheme']); unset($parts['path']); $apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts)); - $apcontact['pubkey'] = trim(self::processElement($data, 'publicKey', 'publicKeyPem')); + $apcontact['pubkey'] = trim(JsonLD::fetchElement($data, 'publicKey', 'publicKeyPem')); // To-Do // manuallyApprovesFollowers @@ -837,7 +838,7 @@ class ActivityPub private static function prepareObjectData($activity, $uid) { - $actor = self::processElement($activity, 'actor', 'id'); + $actor = JsonLD::fetchElement($activity, 'actor', 'id'); if (empty($actor)) { logger('Empty actor', LOGGER_DEBUG); return []; @@ -875,12 +876,12 @@ class ActivityPub } } elseif ($activity['type'] == 'Accept') { $object_data = []; - $object_data['object_type'] = self::processElement($activity, 'object', 'type'); - $object_data['object'] = self::processElement($activity, 'object', 'actor'); + $object_data['object_type'] = JsonLD::fetchElement($activity, 'object', 'type'); + $object_data['object'] = JsonLD::fetchElement($activity, 'object', 'actor'); } elseif ($activity['type'] == 'Undo') { $object_data = []; - $object_data['object_type'] = self::processElement($activity, 'object', 'type'); - $object_data['object'] = self::processElement($activity, 'object', 'object'); + $object_data['object_type'] = JsonLD::fetchElement($activity, 'object', 'type'); + $object_data['object'] = JsonLD::fetchElement($activity, 'object', 'object'); } elseif (in_array($activity['type'], ['Like', 'Dislike'])) { // 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. @@ -1054,11 +1055,11 @@ class ActivityPub } if (!empty($activity['inReplyTo']) && empty($object_data['parent-uri'])) { - $object_data['parent-uri'] = self::processElement($activity, 'inReplyTo', 'id'); + $object_data['parent-uri'] = JsonLD::fetchElement($activity, 'inReplyTo', 'id'); } if (!empty($activity['instrument'])) { - $object_data['service'] = self::processElement($activity, 'instrument', 'name', 'type', 'Service'); + $object_data['service'] = JsonLD::fetchElement($activity, 'instrument', 'name', 'type', 'Service'); } return $object_data; } @@ -1134,7 +1135,7 @@ class ActivityPub $object_data['uri'] = $object['id']; if (!empty($object['inReplyTo'])) { - $object_data['reply-to-uri'] = self::processElement($object, 'inReplyTo', 'id'); + $object_data['reply-to-uri'] = JsonLD::fetchElement($object, 'inReplyTo', 'id'); } else { $object_data['reply-to-uri'] = $object_data['uri']; } @@ -1147,7 +1148,7 @@ class ActivityPub } $object_data['uuid'] = defaults($object, 'uuid', null); - $object_data['owner'] = $object_data['author'] = self::processElement($object, 'attributedTo', 'id'); + $object_data['owner'] = $object_data['author'] = JsonLD::fetchElement($object, 'attributedTo', 'id'); $object_data['context'] = defaults($object, 'context', null); $object_data['conversation'] = defaults($object, 'conversation', null); $object_data['sensitive'] = defaults($object, 'sensitive', null); @@ -1156,11 +1157,11 @@ class ActivityPub $object_data['summary'] = defaults($object, 'summary', null); $object_data['content'] = defaults($object, 'content', null); $object_data['source'] = defaults($object, 'source', null); - $object_data['location'] = self::processElement($object, 'location', 'name', 'type', 'Place'); + $object_data['location'] = JsonLD::fetchElement($object, 'location', 'name', 'type', 'Place'); $object_data['attachments'] = defaults($object, 'attachment', null); $object_data['tags'] = defaults($object, 'tag', null); - $object_data['service'] = self::processElement($object, 'instrument', 'name', 'type', 'Service'); - $object_data['alternate-url'] = self::processElement($object, 'url', 'href'); + $object_data['service'] = JsonLD::fetchElement($object, 'instrument', 'name', 'type', 'Service'); + $object_data['alternate-url'] = JsonLD::fetchElement($object, 'url', 'href'); $object_data['receiver'] = self::getReceivers($object, $object_data['owner']); // Unhandled @@ -1207,41 +1208,6 @@ class ActivityPub return $object_data; } - private static function processElement($array, $element, $key, $type = null, $type_value = null) - { - if (empty($array)) { - return false; - } - - if (empty($array[$element])) { - return false; - } - - if (is_string($array[$element])) { - return $array[$element]; - } - - if (is_null($type_value)) { - if (!empty($array[$element][$key])) { - return $array[$element][$key]; - } - - if (!empty($array[$element][0][$key])) { - return $array[$element][0][$key]; - } - - return false; - } - - if (!empty($array[$element][$key]) && !empty($array[$element][$type]) && ($array[$element][$type] == $type_value)) { - return $array[$element][$key]; - } - - /// @todo Add array search - - return false; - } - private static function convertMentions($body) { $URLSearchString = "^\[\]"; @@ -1358,7 +1324,7 @@ class ActivityPub $item = self::constructAttachList($activity['attachments'], $item); - $source = self::processElement($activity, 'source', 'content', 'mediaType', 'text/bbcode'); + $source = JsonLD::fetchElement($activity, 'source', 'content', 'mediaType', 'text/bbcode'); if (!empty($source)) { $item['body'] = $source; } diff --git a/src/Util/JsonLD.php b/src/Util/JsonLD.php index e8ff5888de..8fd9f90cb4 100644 --- a/src/Util/JsonLD.php +++ b/src/Util/JsonLD.php @@ -65,4 +65,39 @@ class JsonLD return json_decode(json_encode($compacted), true); } + + public static function fetchElement($array, $element, $key, $type = null, $type_value = null) + { + if (empty($array)) { + return false; + } + + if (empty($array[$element])) { + return false; + } + + if (is_string($array[$element])) { + return $array[$element]; + } + + if (is_null($type_value)) { + if (!empty($array[$element][$key])) { + return $array[$element][$key]; + } + + if (!empty($array[$element][0][$key])) { + return $array[$element][0][$key]; + } + + return false; + } + + if (!empty($array[$element][$key]) && !empty($array[$element][$type]) && ($array[$element][$type] == $type_value)) { + return $array[$element][$key]; + } + + /// @todo Add array search + + return false; + } } From 0866fbaf8c65eedbf2d57a1b61f4fe96b2a526fd Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 09:50:03 +0000 Subject: [PATCH 041/261] Code cleaning / wrong table for flags --- src/Protocol/ActivityPub.php | 6 ++-- src/Util/HTTPSignature.php | 67 ------------------------------------ 2 files changed, 3 insertions(+), 70 deletions(-) diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index bd125aaaef..25ebedc8bd 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -103,10 +103,10 @@ class ActivityPub */ public static function profile($uid) { - $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application', 'page-flags']; + $accounttype = ['Person', 'Organization', 'Service', 'Group', 'Application']; $condition = ['uid' => $uid, 'blocked' => false, 'account_expired' => false, 'account_removed' => false, 'verified' => true]; - $fields = ['guid', 'nickname', 'pubkey', 'account-type']; + $fields = ['guid', 'nickname', 'pubkey', 'account-type', 'page-flags']; $user = DBA::selectFirst('user', $fields, $condition); if (!DBA::isResult($user)) { return []; @@ -141,7 +141,7 @@ class ActivityPub 'vcard:region' => $profile['region'], 'vcard:locality' => $profile['locality']]; $data['summary'] = $contact['about']; $data['url'] = $contact['url']; - $data['manuallyApprovesFollowers'] = in_array($profile['page-flags'], [Contact::PAGE_NORMAL, Contact::PAGE_PRVGROUP]); + $data['manuallyApprovesFollowers'] = in_array($user['page-flags'], [Contact::PAGE_NORMAL, Contact::PAGE_PRVGROUP]); $data['publicKey'] = ['id' => $contact['url'] . '#main-key', 'owner' => $contact['url'], 'publicKeyPem' => $user['pubkey']]; diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index 911de4308e..adf5d8ad27 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -18,30 +18,6 @@ use Friendica\Database\DBA; class HTTPSignature { - /** - * @brief RFC5843 - * - * Disabled until Friendica's ActivityPub implementation - * is ready. - * - * @see https://tools.ietf.org/html/rfc5843 - * - * @param string $body The value to create the digest for - * @param boolean $set (optional, default true) - * If set send a Digest HTTP header - * - * @return string The generated digest of $body - */ -// public static function generateDigest($body, $set = true) -// { -// $digest = base64_encode(hash('sha256', $body, true)); -// -// if($set) { -// header('Digest: SHA-256=' . $digest); -// } -// return $digest; -// } - // See draft-cavage-http-signatures-08 public static function verify($data, $key = '') { @@ -127,12 +103,6 @@ class HTTPSignature logger('Got keyID ' . $sig_block['keyId']); - // We don't use Activity Pub at the moment. -// if (!$key) { -// $result['signer'] = $sig_block['keyId']; -// $key = self::getActivitypubKey($sig_block['keyId']); -// } - if (!$key) { return $result; } @@ -171,43 +141,6 @@ class HTTPSignature return $result; } - /** - * Fetch the public key for Activity Pub contact. - * - * @param string|int The identifier (contact addr or contact ID). - * @return string|boolean The public key or false on failure. - */ - private static function getActivitypubKey($id) - { - if (strpos($id, 'acct:') === 0) { - $contact = DBA::selectFirst('contact', ['pubkey'], ['uid' => 0, 'addr' => str_replace('acct:', '', $id)]); - } else { - $contact = DBA::selectFirst('contact', ['pubkey'], ['id' => $id, 'network' => 'activitypub']); - } - - if (DBA::isResult($contact)) { - return $contact['pubkey']; - } - - if(function_exists('as_fetch')) { - $r = as_fetch($id); - } - - if ($r) { - $j = json_decode($r, true); - - if (array_key_exists('publicKey', $j) && array_key_exists('publicKeyPem', $j['publicKey'])) { - if ((array_key_exists('id', $j['publicKey']) && $j['publicKey']['id'] !== $id) && $j['id'] !== $id) { - return false; - } - - return $j['publicKey']['publicKeyPem']; - } - } - - return false; - } - /** * @brief * From 11310f4cf0eb66206ba758e178a892345028bf15 Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 18:16:14 +0000 Subject: [PATCH 042/261] Relocated AP signature functions, reduced magic auth functions --- src/Module/Inbox.php | 3 +- src/Module/Magic.php | 6 +- src/Module/Owa.php | 2 +- src/Protocol/ActivityPub.php | 196 +---------------------- src/Util/HTTPSignature.php | 291 +++++++++++++++++++++-------------- src/Worker/APDelivery.php | 3 +- 6 files changed, 192 insertions(+), 309 deletions(-) diff --git a/src/Module/Inbox.php b/src/Module/Inbox.php index d47419a415..49df14762e 100644 --- a/src/Module/Inbox.php +++ b/src/Module/Inbox.php @@ -8,6 +8,7 @@ use Friendica\BaseModule; use Friendica\Protocol\ActivityPub; use Friendica\Core\System; use Friendica\Database\DBA; +use Friendica\Util\HTTPSignature; /** * ActivityPub Inbox @@ -24,7 +25,7 @@ class Inbox extends BaseModule System::httpExit(400); } - if (ActivityPub::verifySignature($postdata, $_SERVER)) { + if (HTTPSignature::verifyAP($postdata, $_SERVER)) { $filename = 'signed-activitypub'; } else { $filename = 'failed-activitypub'; diff --git a/src/Module/Magic.php b/src/Module/Magic.php index cf77482e5a..0b4126e0e9 100644 --- a/src/Module/Magic.php +++ b/src/Module/Magic.php @@ -76,13 +76,9 @@ class Magic extends BaseModule // Create a header that is signed with the local users private key. $headers = HTTPSignature::createSig( - '', $headers, $user['prvkey'], - 'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->urlpath ? '/' . $a->urlpath : ''), - false, - true, - 'sha512' + 'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->urlpath ? '/' . $a->urlpath : '') ); // Try to get an authentication token from the other instance. diff --git a/src/Module/Owa.php b/src/Module/Owa.php index 1d6b1332dc..68f31c59de 100644 --- a/src/Module/Owa.php +++ b/src/Module/Owa.php @@ -54,7 +54,7 @@ class Owa extends BaseModule if (DBA::isResult($contact)) { // Try to verify the signed header with the public key of the contact record // we have found. - $verified = HTTPSignature::verify('', $contact['pubkey']); + $verified = HTTPSignature:verifyMagic($contact['pubkey']); if ($verified && $verified['header_signed'] && $verified['header_valid']) { logger('OWA header: ' . print_r($verified, true), LOGGER_DATA); diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index 25ebedc8bd..d952f4de8f 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -63,38 +63,6 @@ class ActivityPub stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json'); } - public static function transmit($data, $target, $uid) - { - $owner = User::getOwnerDataById($uid); - - if (!$owner) { - return; - } - - $content = json_encode($data); - - // Header data that is about to be signed. - $host = parse_url($target, PHP_URL_HOST); - $path = parse_url($target, PHP_URL_PATH); - $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); - $content_length = strlen($content); - - $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; - - $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; - - $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); - - $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"'; - - $headers[] = 'Content-Type: application/activity+json'; - - Network::post($target, $content, $headers); - $return_code = BaseObject::getApp()->get_curl_code(); - - logger('Transmit to ' . $target . ' returned ' . $return_code); - } - /** * Return the ActivityPub profile of the given user * @@ -401,7 +369,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactAccept($target, $id, $uid) @@ -419,7 +387,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactReject($target, $id, $uid) @@ -437,7 +405,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } public static function transmitContactUndo($target, $uid) @@ -457,7 +425,7 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return self::transmit($data, $profile['inbox'], $uid); + return HTTPSignature::transmit($data, $profile['inbox'], $uid); } /** @@ -515,154 +483,6 @@ class ActivityPub return false; } - public static function verifySignature($content, $http_headers) - { - $object = json_decode($content, true); - - if (empty($object)) { - return false; - } - - $actor = JsonLD::fetchElement($object, 'actor', 'id'); - - $headers = []; - $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; - - // First take every header - foreach ($http_headers as $k => $v) { - $field = str_replace('_', '-', strtolower($k)); - $headers[$field] = $v; - } - - // Now add every http header - foreach ($http_headers as $k => $v) { - if (strpos($k, 'HTTP_') === 0) { - $field = str_replace('_', '-', strtolower(substr($k, 5))); - $headers[$field] = $v; - } - } - - $sig_block = ActivityPub::parseSigHeader($http_headers['HTTP_SIGNATURE']); - - if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { - return false; - } - - $signed_data = ''; - foreach ($sig_block['headers'] as $h) { - if (array_key_exists($h, $headers)) { - $signed_data .= $h . ': ' . $headers[$h] . "\n"; - } - } - $signed_data = rtrim($signed_data, "\n"); - - if (empty($signed_data)) { - return false; - } - - $algorithm = null; - - if ($sig_block['algorithm'] === 'rsa-sha256') { - $algorithm = 'sha256'; - } - - if ($sig_block['algorithm'] === 'rsa-sha512') { - $algorithm = 'sha512'; - } - - if (empty($algorithm)) { - return false; - } - - $key = self::fetchKey($sig_block['keyId'], $actor); - - if (empty($key)) { - return false; - } - - if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) { - return false; - } - - // Check the digest when it is part of the signed data - if (in_array('digest', $sig_block['headers'])) { - $digest = explode('=', $headers['digest'], 2); - if ($digest[0] === 'SHA-256') { - $hashalg = 'sha256'; - } - if ($digest[0] === 'SHA-512') { - $hashalg = 'sha512'; - } - - /// @todo add all hashes from the rfc - - if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { - return false; - } - } - - // Check the content-length when it is part of the signed data - if (in_array('content-length', $sig_block['headers'])) { - if (strlen($content) != $headers['content-length']) { - return false; - } - } - - return true; - - } - - private static function fetchKey($id, $actor) - { - $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); - - $profile = self::fetchprofile($url); - if (!empty($profile)) { - return $profile['pubkey']; - } elseif ($url != $actor) { - $profile = self::fetchprofile($actor); - if (!empty($profile)) { - return $profile['pubkey']; - } - } - - return false; - } - - /** - * @brief - * - * @param string $header - * @return array associate array with - * - \e string \b keyID - * - \e string \b algorithm - * - \e array \b headers - * - \e string \b signature - */ - private static function parseSigHeader($header) - { - $ret = []; - $matches = []; - - if (preg_match('/keyId="(.*?)"/ism',$header,$matches)) { - $ret['keyId'] = $matches[1]; - } - - if (preg_match('/algorithm="(.*?)"/ism',$header,$matches)) { - $ret['algorithm'] = $matches[1]; - } - - if (preg_match('/headers="(.*?)"/ism',$header,$matches)) { - $ret['headers'] = explode(' ', $matches[1]); - } - - if (preg_match('/signature="(.*?)"/ism',$header,$matches)) { - $ret['signature'] = base64_decode(preg_replace('/\s+/','',$matches[1])); - } - - return $ret; - } - public static function fetchprofile($url, $update = false) { if (empty($url)) { @@ -798,7 +618,7 @@ class ActivityPub { logger('Incoming message for user ' . $uid, LOGGER_DEBUG); - if (!self::verifySignature($body, $header)) { + if (!HTTPSignature::verifyAP($body, $header)) { logger('Invalid signature, message will be discarded.', LOGGER_DEBUG); return; } @@ -813,7 +633,7 @@ class ActivityPub self::processActivity($activity, $body, $uid); } - public static function fetchOutbox($url) + public static function fetchOutbox($url, $uid) { $data = self::fetchContent($url); if (empty($data)) { @@ -825,14 +645,14 @@ class ActivityPub } elseif (!empty($data['first']['orderedItems'])) { $items = $data['first']['orderedItems']; } elseif (!empty($data['first'])) { - self::fetchOutbox($data['first']); + self::fetchOutbox($data['first'], $uid); return; } else { $items = []; } foreach ($items as $activity) { - self::processActivity($activity); + self::processActivity($activity, '', $uid); } } diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index adf5d8ad27..f6a5fe1fe4 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -5,8 +5,11 @@ */ namespace Friendica\Util; +use Friendica\BaseObject; use Friendica\Core\Config; use Friendica\Database\DBA; +use Friendica\Model\User; +use Friendica\Protocol\ActivityPub; /** * @brief Implements HTTP Signatures per draft-cavage-http-signatures-07. @@ -19,56 +22,36 @@ use Friendica\Database\DBA; class HTTPSignature { // See draft-cavage-http-signatures-08 - public static function verify($data, $key = '') + public static function verifyMagic($key) { - $body = $data; $headers = null; $spoofable = false; $result = [ 'signer' => '', 'header_signed' => false, - 'header_valid' => false, - 'content_signed' => false, - 'content_valid' => false + 'header_valid' => false ]; // Decide if $data arrived via controller submission or curl. - if (is_array($data) && $data['header']) { - if (!$data['success']) { - return $result; - } + $headers = []; + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI']; - $h = new HTTPHeaders($data['header']); - $headers = $h->fetch(); - $body = $data['body']; - } else { - $headers = []; - $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI']; - - foreach ($_SERVER as $k => $v) { - if (strpos($k, 'HTTP_') === 0) { - $field = str_replace('_', '-', strtolower(substr($k, 5))); - $headers[$field] = $v; - } + foreach ($_SERVER as $k => $v) { + if (strpos($k, 'HTTP_') === 0) { + $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; } } $sig_block = null; - if (array_key_exists('signature', $headers)) { - $sig_block = self::parseSigheader($headers['signature']); - } elseif (array_key_exists('authorization', $headers)) { - $sig_block = self::parseSigheader($headers['authorization']); - } + $sig_block = self::parseSigheader($headers['authorization']); if (!$sig_block) { logger('no signature provided.'); return $result; } - // Warning: This log statement includes binary data - // logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA); - $result['header_signed'] = true; $signed_headers = $sig_block['headers']; @@ -88,13 +71,7 @@ class HTTPSignature $signed_data = rtrim($signed_data, "\n"); - $algorithm = null; - if ($sig_block['algorithm'] === 'rsa-sha256') { - $algorithm = 'sha256'; - } - if ($sig_block['algorithm'] === 'rsa-sha512') { - $algorithm = 'sha512'; - } + $algorithm = 'sha512'; if ($key && function_exists($key)) { $result['signer'] = $sig_block['keyId']; @@ -119,93 +96,39 @@ class HTTPSignature $result['header_valid'] = true; } - if (in_array('digest', $signed_headers)) { - $result['content_signed'] = true; - $digest = explode('=', $headers['digest']); - - if ($digest[0] === 'SHA-256') { - $hashalg = 'sha256'; - } - if ($digest[0] === 'SHA-512') { - $hashalg = 'sha512'; - } - - // The explode operation will have stripped the '=' padding, so compare against unpadded base64. - if (rtrim(base64_encode(hash($hashalg, $body, true)), '=') === $digest[1]) { - $result['content_valid'] = true; - } - } - - logger('Content_Valid: ' . $result['content_valid']); - return $result; } /** * @brief * - * @param string $request * @param array $head * @param string $prvkey * @param string $keyid (optional, default 'Key') - * @param boolean $send_headers (optional, default false) - * If set send a HTTP header - * @param boolean $auth (optional, default false) - * @param string $alg (optional, default 'sha256') - * @param string $crypt_key (optional, default null) - * @param string $crypt_algo (optional, default 'aes256ctr') * * @return array */ - public static function createSig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr') + public static function createSig($head, $prvkey, $keyid = 'Key') { $return_headers = []; - if ($alg === 'sha256') { - $algorithm = 'rsa-sha256'; - } + $alg = 'sha512'; + $algorithm = 'rsa-sha512'; - if ($alg === 'sha512') { - $algorithm = 'rsa-sha512'; - } - - $x = self::sign($request, $head, $prvkey, $alg); + $x = self::sign($head, $prvkey, $alg); $headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm . '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"'; - if ($crypt_key) { - $x = Crypto::encapsulate($headerval, $crypt_key, $crypt_algo); - $headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"'; - } - - if ($auth) { - $sighead = 'Authorization: Signature ' . $headerval; - } else { - $sighead = 'Signature: ' . $headerval; - } + $sighead = 'Authorization: Signature ' . $headerval; if ($head) { foreach ($head as $k => $v) { - if ($send_headers) { - // This is for ActivityPub implementation. - // Since the Activity Pub implementation isn't - // ready at the moment, we comment it out. - // header($k . ': ' . $v); - } else { - $return_headers[] = $k . ': ' . $v; - } + $return_headers[] = $k . ': ' . $v; } } - if ($send_headers) { - // This is for ActivityPub implementation. - // Since the Activity Pub implementation isn't - // ready at the moment, we comment it out. - // header($sighead); - } else { - $return_headers[] = $sighead; - } + $return_headers[] = $sighead; return $return_headers; } @@ -213,35 +136,27 @@ class HTTPSignature /** * @brief * - * @param string $request * @param array $head * @param string $prvkey * @param string $alg (optional) default 'sha256' * * @return array */ - private static function sign($request, $head, $prvkey, $alg = 'sha256') + private static function sign($head, $prvkey, $alg = 'sha256') { $ret = []; $headers = ''; $fields = ''; - if ($request) { - $headers = '(request-target)' . ': ' . trim($request) . "\n"; - $fields = '(request-target)'; - } - - if ($head) { - foreach ($head as $k => $v) { - $headers .= strtolower($k) . ': ' . trim($v) . "\n"; - if ($fields) { - $fields .= ' '; - } - $fields .= strtolower($k); + foreach ($head as $k => $v) { + $headers .= strtolower($k) . ': ' . trim($v) . "\n"; + if ($fields) { + $fields .= ' '; } - // strip the trailing linefeed - $headers = rtrim($headers, "\n"); + $fields .= strtolower($k); } + // strip the trailing linefeed + $headers = rtrim($headers, "\n"); $sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg)); @@ -338,4 +253,154 @@ class HTTPSignature return ''; } + + /** + * Functions for ActivityPub + */ + + public static function transmit($data, $target, $uid) + { + $owner = User::getOwnerDataById($uid); + + if (!$owner) { + return; + } + + $content = json_encode($data); + + // Header data that is about to be signed. + $host = parse_url($target, PHP_URL_HOST); + $path = parse_url($target, PHP_URL_PATH); + $digest = 'SHA-256=' . base64_encode(hash('sha256', $content, true)); + $content_length = strlen($content); + + $headers = ['Content-Length: ' . $content_length, 'Digest: ' . $digest, 'Host: ' . $host]; + + $signed_data = "(request-target): post " . $path . "\ncontent-length: " . $content_length . "\ndigest: " . $digest . "\nhost: " . $host; + + $signature = base64_encode(Crypto::rsaSign($signed_data, $owner['uprvkey'], 'sha256')); + + $headers[] = 'Signature: keyId="' . $owner['url'] . '#main-key' . '",algorithm="rsa-sha256",headers="(request-target) content-length digest host",signature="' . $signature . '"'; + + $headers[] = 'Content-Type: application/activity+json'; + + Network::post($target, $content, $headers); + $return_code = BaseObject::getApp()->get_curl_code(); + + logger('Transmit to ' . $target . ' returned ' . $return_code); + } + + public static function verifyAP($content, $http_headers) + { + $object = json_decode($content, true); + + if (empty($object)) { + return false; + } + + $actor = JsonLD::fetchElement($object, 'actor', 'id'); + + $headers = []; + $headers['(request-target)'] = strtolower($http_headers['REQUEST_METHOD']) . ' ' . $http_headers['REQUEST_URI']; + + // First take every header + foreach ($http_headers as $k => $v) { + $field = str_replace('_', '-', strtolower($k)); + $headers[$field] = $v; + } + + // Now add every http header + foreach ($http_headers as $k => $v) { + if (strpos($k, 'HTTP_') === 0) { + $field = str_replace('_', '-', strtolower(substr($k, 5))); + $headers[$field] = $v; + } + } + + $sig_block = self::parseSigHeader($http_headers['HTTP_SIGNATURE']); + + if (empty($sig_block) || empty($sig_block['headers']) || empty($sig_block['keyId'])) { + return false; + } + + $signed_data = ''; + foreach ($sig_block['headers'] as $h) { + if (array_key_exists($h, $headers)) { + $signed_data .= $h . ': ' . $headers[$h] . "\n"; + } + } + $signed_data = rtrim($signed_data, "\n"); + + if (empty($signed_data)) { + return false; + } + + $algorithm = null; + + if ($sig_block['algorithm'] === 'rsa-sha256') { + $algorithm = 'sha256'; + } + + if ($sig_block['algorithm'] === 'rsa-sha512') { + $algorithm = 'sha512'; + } + + if (empty($algorithm)) { + return false; + } + + $key = self::fetchKey($sig_block['keyId'], $actor); + + if (empty($key)) { + return false; + } + + if (!Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm)) { + return false; + } + + // Check the digest when it is part of the signed data + if (in_array('digest', $sig_block['headers'])) { + $digest = explode('=', $headers['digest'], 2); + if ($digest[0] === 'SHA-256') { + $hashalg = 'sha256'; + } + if ($digest[0] === 'SHA-512') { + $hashalg = 'sha512'; + } + + /// @todo add all hashes from the rfc + + if (!empty($hashalg) && base64_encode(hash($hashalg, $content, true)) != $digest[1]) { + return false; + } + } + + // Check the content-length when it is part of the signed data + if (in_array('content-length', $sig_block['headers'])) { + if (strlen($content) != $headers['content-length']) { + return false; + } + } + + return true; + + } + + private static function fetchKey($id, $actor) + { + $url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id); + + $profile = ActivityPub::fetchprofile($url); + if (!empty($profile)) { + return $profile['pubkey']; + } elseif ($url != $actor) { + $profile = ActivityPub::fetchprofile($actor); + if (!empty($profile)) { + return $profile['pubkey']; + } + } + + return false; + } } diff --git a/src/Worker/APDelivery.php b/src/Worker/APDelivery.php index b7e881c7a3..f43c56a3ec 100644 --- a/src/Worker/APDelivery.php +++ b/src/Worker/APDelivery.php @@ -7,6 +7,7 @@ namespace Friendica\Worker; use Friendica\BaseObject; use Friendica\Protocol\ActivityPub; use Friendica\Model\Item; +use Friendica\Util\HTTPSignature; class APDelivery extends BaseObject { @@ -20,7 +21,7 @@ class APDelivery extends BaseObject } else { $item = Item::selectFirst(['uid'], ['id' => $item_id]); $data = ActivityPub::createActivityFromItem($item_id); - ActivityPub::transmit($data, $inbox, $item['uid']); + HTTPSignature::transmit($data, $inbox, $item['uid']); } return; From 752b5fe28464c7f1dec79132b6ef74ae71420d8b Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 20 Sep 2018 21:45:23 +0000 Subject: [PATCH 043/261] Outgoing posts are now signed --- src/Protocol/ActivityPub.php | 22 +++++++-- src/Util/LDSignature.php | 92 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 src/Util/LDSignature.php diff --git a/src/Protocol/ActivityPub.php b/src/Protocol/ActivityPub.php index d952f4de8f..6f5fdedc95 100644 --- a/src/Protocol/ActivityPub.php +++ b/src/Protocol/ActivityPub.php @@ -20,6 +20,7 @@ use Friendica\Util\Crypto; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Util\JsonLD; +use Friendica\Util\LDSignature; /** * @brief ActivityPub Protocol class @@ -273,7 +274,10 @@ class ActivityPub $data = array_merge($data, ActivityPub::createPermissionBlockForItem($item)); $data['object'] = self::createNote($item); - return $data; + + $owner = User::getOwnerDataById($item['uid']); + + return LDSignature::sign($data, $owner); } public static function createObjectFromItemID($item_id) @@ -369,7 +373,9 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending activity ' . $activity . ' to ' . $target . ' for user ' . $uid, LOGGER_DEBUG); - return HTTPSignature::transmit($data, $profile['inbox'], $uid); + + $signed = LDSignature::sign($data, $owner); + return HTTPSignature::transmit($signed, $profile['inbox'], $uid); } public static function transmitContactAccept($target, $id, $uid) @@ -387,7 +393,9 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending accept to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return HTTPSignature::transmit($data, $profile['inbox'], $uid); + + $signed = LDSignature::sign($data, $owner); + return HTTPSignature::transmit($signed, $profile['inbox'], $uid); } public static function transmitContactReject($target, $id, $uid) @@ -405,7 +413,9 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending reject to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return HTTPSignature::transmit($data, $profile['inbox'], $uid); + + $signed = LDSignature::sign($data, $owner); + return HTTPSignature::transmit($signed, $profile['inbox'], $uid); } public static function transmitContactUndo($target, $uid) @@ -425,7 +435,9 @@ class ActivityPub 'to' => $profile['url']]; logger('Sending undo to ' . $target . ' for user ' . $uid . ' with id ' . $id, LOGGER_DEBUG); - return HTTPSignature::transmit($data, $profile['inbox'], $uid); + + $signed = LDSignature::sign($data, $owner); + return HTTPSignature::transmit($signed, $profile['inbox'], $uid); } /** diff --git a/src/Util/LDSignature.php b/src/Util/LDSignature.php new file mode 100644 index 0000000000..7288b584c7 --- /dev/null +++ b/src/Util/LDSignature.php @@ -0,0 +1,92 @@ + 'RsaSignature2017', + 'nonce' => random_string(64), + 'creator' => $owner['url'] . '#main-key', + 'created' => DateTimeFormat::utcNow() + ]; + + $ohash = self::hash(self::signable_options($options)); + $dhash = self::hash(self::signable_data($data)); + $options['signatureValue'] = base64_encode(Crypto::rsaSign($ohash . $dhash, $owner['uprvkey'])); + + return array_merge($data, ['signature' => $options]); + } + + + private static function signable_data($data) + { + $newdata = []; + if (!empty($data)) { + foreach ($data as $k => $v) { + if (!in_array($k, ['signature'])) { + $newdata[$k] = $v; + } + } + } + return $newdata; + } + + + private static function signable_options($options) + { + $newopts = ['@context' => 'https://w3id.org/identity/v1']; + if (!empty($options)) { + foreach ($options as $k => $v) { + if (!in_array($k, ['type','id','signatureValue'])) { + $newopts[$k] = $v; + } + } + } + return $newopts; + } + + private static function hash($obj) + { + return hash('sha256', JsonLD::normalize($obj)); + } +} From 55f1d7b90e180b4609a4473399ea1b23912dfcfa Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:01:05 -0400 Subject: [PATCH 044/261] Add new footer hook - Add new App->footerScripts array - Add footer.tpl template - Add documentation - Rework App->init_page_end to App->initFooter --- doc/Addons.md | 6 +++++ index.php | 38 +--------------------------- src/App.php | 53 +++++++++++++++++++++++++++++++++------ view/templates/footer.tpl | 3 +++ 4 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 view/templates/footer.tpl diff --git a/doc/Addons.md b/doc/Addons.md index ec413c6ac8..b126fedc4d 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -286,6 +286,11 @@ No hook data. Called after HTML content functions have completed. `$b` is (string) HTML of content div. +### footer +Called after HTML content functions have completed. +`$b` is (string) HTML of footer div/element. +Used to load deferred Javascript files. + ### avatar_lookup Called when looking up the avatar. `$b` is an array: @@ -563,6 +568,7 @@ Here is a complete list of all hook callbacks with file locations (as of 01-Apr- ### src/App.php Addon::callHooks('load_config'); + Addon::callHooks('footer'); ### src/Model/Item.php diff --git a/index.php b/index.php index 8b0bd47251..aa7704930e 100644 --- a/index.php +++ b/index.php @@ -153,10 +153,6 @@ if (! x($_SESSION, 'authenticated')) { header('X-Account-Management-Status: none'); } -/* set up page['htmlhead'] and page['end'] for the modules to use */ -$a->page['htmlhead'] = ''; -$a->page['end'] = ''; - $_SESSION['sysmsg'] = defaults($_SESSION, 'sysmsg' , []); $_SESSION['sysmsg_info'] = defaults($_SESSION, 'sysmsg_info' , []); $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []); @@ -326,10 +322,6 @@ if (file_exists($theme_info_file)) { /* initialise content region */ -if (! x($a->page, 'content')) { - $a->page['content'] = ''; -} - if ($a->mode == App::MODE_NORMAL) { Addon::callHooks('page_content_top', $a->page['content']); } @@ -411,18 +403,7 @@ $a->init_pagehead(); * Build the page ending -- this is stuff that goes right before * the closing tag */ -$a->init_page_end(); - -// If you're just visiting, let javascript take you home -if (x($_SESSION, 'visitor_home')) { - $homebase = $_SESSION['visitor_home']; -} elseif (local_user()) { - $homebase = 'profile/' . $a->user['nickname']; -} - -if (isset($homebase)) { - $a->page['content'] .= ''; -} +$a->initFooter(); /* * now that we've been through the module content, see if the page reported @@ -444,23 +425,6 @@ if ($a->module != 'install' && $a->module != 'maintenance') { Nav::build($a); } -/* - * Add a "toggle mobile" link if we're using a mobile device - */ -if ($a->is_mobile || $a->is_tablet) { - if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) { - $link = 'toggle_mobile?address=' . curPageURL(); - } else { - $link = 'toggle_mobile?off=1&address=' . curPageURL(); - } - $a->page['footer'] = replace_macros( - get_markup_template("toggle_mobile_footer.tpl"), - [ - '$toggle_link' => $link, - '$toggle_text' => L10n::t('toggle mobile')] - ); -} - /** * Build the page - now that we have all the components */ diff --git a/src/App.php b/src/App.php index 2a5fba8541..1a26dd6507 100644 --- a/src/App.php +++ b/src/App.php @@ -96,6 +96,15 @@ class App public $force_max_items = 0; public $theme_events_in_profile = true; + public $footerScripts = []; + + public function registerFooterScript($path) + { + $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path); + + $this->footerScripts[] = $this->get_baseurl() . '/' . trim($url, '/'); + } + /** * @brief An array for all theme-controllable parameters * @@ -802,15 +811,45 @@ class App ]) . $this->page['htmlhead']; } - public function init_page_end() + public function initFooter() { - if (!isset($this->page['end'])) { - $this->page['end'] = ''; + if (!isset($this->page['footer'])) { + $this->page['footer'] = ''; } - $tpl = get_markup_template('end.tpl'); - $this->page['end'] = replace_macros($tpl, [ - '$baseurl' => $this->get_baseurl() - ]) . $this->page['end']; + + // If you're just visiting, let javascript take you home + if (!empty($_SESSION['visitor_home'])) { + $homebase = $_SESSION['visitor_home']; + } elseif (local_user()) { + $homebase = 'profile/' . $a->user['nickname']; + } + + if (isset($homebase)) { + $this->page['footer'] .= '' . "\n"; + } + + /* + * Add a "toggle mobile" link if we're using a mobile device + */ + if ($this->is_mobile || $this->is_tablet) { + if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) { + $link = 'toggle_mobile?address=' . curPageURL(); + } else { + $link = 'toggle_mobile?off=1&address=' . curPageURL(); + } + $this->page['footer'] .= replace_macros(get_markup_template("toggle_mobile_footer.tpl"), [ + '$toggle_link' => $link, + '$toggle_text' => Core\L10n::t('toggle mobile') + ]); + } + + Core\Addon::callHooks('footer', $this->page['footer']); + + $tpl = get_markup_template('footer.tpl'); + $this->page['footer'] .= replace_macros($tpl, [ + '$baseurl' => $this->get_baseurl(), + '$footerScripts' => $this->footerScripts, + ]); } public function set_curl_code($code) diff --git a/view/templates/footer.tpl b/view/templates/footer.tpl new file mode 100644 index 0000000000..1b9d700020 --- /dev/null +++ b/view/templates/footer.tpl @@ -0,0 +1,3 @@ +{{foreach $footerScripts as $scriptUrl}} + +{{/foreach}} From 1eaa523e611abb7ba0217da77a70837ea9547812 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:02:28 -0400 Subject: [PATCH 045/261] Remove unused App->page['end'] - Remove unused empty templates --- include/conversation.php | 15 --------------- include/text.php | 3 --- mod/cal.php | 5 ----- mod/contacts.php | 9 --------- mod/editpost.php | 9 --------- mod/events.php | 5 ----- mod/message.php | 20 -------------------- mod/profile_photo.php | 1 - mod/profiles.php | 3 --- mod/settings.php | 5 ----- mod/videos.php | 6 ------ mod/wallmessage.php | 7 ------- src/App.php | 1 - view/templates/contact_end.tpl | 1 - view/templates/contacts-end.tpl | 1 - view/templates/cropend.tpl | 1 - view/templates/end.tpl | 1 - view/templates/event_end.tpl | 1 - view/templates/jot-end.tpl | 1 - view/templates/message-end.tpl | 1 - view/templates/msg-end.tpl | 1 - view/templates/profed_end.tpl | 1 - view/templates/videos_end.tpl | 0 view/templates/wallmsg-end.tpl | 1 - 24 files changed, 99 deletions(-) delete mode 100644 view/templates/contact_end.tpl delete mode 100644 view/templates/contacts-end.tpl delete mode 100644 view/templates/cropend.tpl delete mode 100644 view/templates/end.tpl delete mode 100644 view/templates/event_end.tpl delete mode 100644 view/templates/jot-end.tpl delete mode 100644 view/templates/message-end.tpl delete mode 100644 view/templates/msg-end.tpl delete mode 100644 view/templates/profed_end.tpl delete mode 100644 view/templates/videos_end.tpl delete mode 100644 view/templates/wallmsg-end.tpl diff --git a/include/conversation.php b/include/conversation.php index ca01997f6a..d791fa4141 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -1091,21 +1091,6 @@ function status_editor(App $a, $x, $notes_cid = 0, $popup = false) '$delitems' => L10n::t("Delete item\x28s\x29?") ]); - $tpl = get_markup_template('jot-end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$newpost' => 'true', - '$baseurl' => System::baseUrl(true), - '$geotag' => $geotag, - '$nickname' => $x['nickname'], - '$ispublic' => L10n::t('Visible to everybody'), - '$linkurl' => L10n::t('Please enter a link URL:'), - '$vidurl' => L10n::t("Please enter a video link/URL:"), - '$audurl' => L10n::t("Please enter an audio link/URL:"), - '$term' => L10n::t('Tag term:'), - '$fileas' => L10n::t('Save to Folder:'), - '$whereareu' => L10n::t('Where are you right now?') - ]); - $jotplugins = ''; Addon::callHooks('jot_tool', $jotplugins); diff --git a/include/text.php b/include/text.php index d251824e2b..53b8061221 100644 --- a/include/text.php +++ b/include/text.php @@ -1191,9 +1191,6 @@ function prepare_body(array &$item, $attach = false, $is_preview = false) $a->page['htmlhead'] .= replace_macros(get_markup_template('videos_head.tpl'), [ '$baseurl' => System::baseUrl(), ]); - $a->page['end'] .= replace_macros(get_markup_template('videos_end.tpl'), [ - '$baseurl' => System::baseUrl(), - ]); } $url_parts = explode('/', $the_url); diff --git a/mod/cal.php b/mod/cal.php index bdedaaacfa..ae1060c47a 100644 --- a/mod/cal.php +++ b/mod/cal.php @@ -94,11 +94,6 @@ function cal_content(App $a) '$i18n' => $i18n, ]); - $etpl = get_markup_template('event_end.tpl'); - $a->page['end'] .= replace_macros($etpl, [ - '$baseurl' => System::baseUrl(), - ]); - $mode = 'view'; $y = 0; $m = 0; diff --git a/mod/contacts.php b/mod/contacts.php index 4e87697172..86d7e2ac65 100644 --- a/mod/contacts.php +++ b/mod/contacts.php @@ -112,12 +112,6 @@ function contacts_init(App $a) '$baseurl' => System::baseUrl(true), '$base' => $base ]); - - $tpl = get_markup_template("contacts-end.tpl"); - $a->page['end'] .= replace_macros($tpl, [ - '$baseurl' => System::baseUrl(true), - '$base' => $base - ]); } function contacts_batch_actions(App $a) @@ -503,9 +497,6 @@ function contacts_content(App $a, $update = 0) $a->page['htmlhead'] .= replace_macros(get_markup_template('contact_head.tpl'), [ '$baseurl' => System::baseUrl(true), ]); - $a->page['end'] .= replace_macros(get_markup_template('contact_end.tpl'), [ - '$baseurl' => System::baseUrl(true), - ]); $contact['blocked'] = Contact::isBlockedByUser($contact['id'], local_user()); $contact['readonly'] = Contact::isIgnoredByUser($contact['id'], local_user()); diff --git a/mod/editpost.php b/mod/editpost.php index 258585ef1c..b8ccff470e 100644 --- a/mod/editpost.php +++ b/mod/editpost.php @@ -51,15 +51,6 @@ function editpost_content(App $a) '$nickname' => $a->user['nickname'] ]); - $tpl = get_markup_template('jot-end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$baseurl' => System::baseUrl(), - '$ispublic' => ' ', // L10n::t('Visible to everybody'), - '$geotag' => $geotag, - '$nickname' => $a->user['nickname'] - ]); - - $tpl = get_markup_template("jot.tpl"); if (strlen($item['allow_cid']) || strlen($item['allow_gid']) || strlen($item['deny_cid']) || strlen($item['deny_gid'])) { diff --git a/mod/events.php b/mod/events.php index 91474022fc..fd658a9cef 100644 --- a/mod/events.php +++ b/mod/events.php @@ -229,11 +229,6 @@ function events_content(App $a) { '$i18n' => $i18n, ]); - $etpl = get_markup_template('event_end.tpl'); - $a->page['end'] .= replace_macros($etpl, [ - '$baseurl' => System::baseUrl(), - ]); - $o = ''; $tabs = ''; // tabs diff --git a/mod/message.php b/mod/message.php index 8c9aa657df..23d528a8aa 100644 --- a/mod/message.php +++ b/mod/message.php @@ -46,12 +46,6 @@ function message_init(App $a) '$baseurl' => System::baseUrl(true), '$base' => $base ]); - - $end_tpl = get_markup_template('message-end.tpl'); - $a->page['end'] .= replace_macros($end_tpl, [ - '$baseurl' => System::baseUrl(true), - '$base' => $base - ]); } function message_post(App $a) @@ -199,13 +193,6 @@ function message_content(App $a) '$linkurl' => L10n::t('Please enter a link URL:') ]); - $tpl = get_markup_template('msg-end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$baseurl' => System::baseUrl(true), - '$nickname' => $a->user['nickname'], - '$linkurl' => L10n::t('Please enter a link URL:') - ]); - $preselect = isset($a->argv[2]) ? [$a->argv[2]] : []; $prename = $preurl = $preid = ''; @@ -344,13 +331,6 @@ function message_content(App $a) '$linkurl' => L10n::t('Please enter a link URL:') ]); - $tpl = get_markup_template('msg-end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$baseurl' => System::baseUrl(true), - '$nickname' => $a->user['nickname'], - '$linkurl' => L10n::t('Please enter a link URL:') - ]); - $mails = []; $seen = 0; $unknown = false; diff --git a/mod/profile_photo.php b/mod/profile_photo.php index 567a7f3a25..984ebfed6f 100644 --- a/mod/profile_photo.php +++ b/mod/profile_photo.php @@ -317,7 +317,6 @@ function profile_photo_crop_ui_head(App $a, Image $image) } $a->page['htmlhead'] .= replace_macros(get_markup_template("crophead.tpl"), []); - $a->page['end'] .= replace_macros(get_markup_template("cropend.tpl"), []); $imagecrop = [ 'hash' => $hash, diff --git a/mod/profiles.php b/mod/profiles.php index d951a470d7..76491c553e 100644 --- a/mod/profiles.php +++ b/mod/profiles.php @@ -527,9 +527,6 @@ function profiles_content(App $a) { $a->page['htmlhead'] .= replace_macros(get_markup_template('profed_head.tpl'), [ '$baseurl' => System::baseUrl(true), ]); - $a->page['end'] .= replace_macros(get_markup_template('profed_end.tpl'), [ - '$baseurl' => System::baseUrl(true), - ]); $opt_tpl = get_markup_template("profile-hide-friends.tpl"); $hide_friends = replace_macros($opt_tpl,[ diff --git a/mod/settings.php b/mod/settings.php index 84bc230e30..78fa446ce0 100644 --- a/mod/settings.php +++ b/mod/settings.php @@ -982,11 +982,6 @@ function settings_content(App $a) '$theme_config' => $theme_config, ]); - $tpl = get_markup_template('settings/display_end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$theme' => ['theme', L10n::t('Display Theme:'), $theme_selected, '', $themes] - ]); - return $o; } diff --git a/mod/videos.php b/mod/videos.php index e622e17f0f..4bd0ab4f32 100644 --- a/mod/videos.php +++ b/mod/videos.php @@ -105,12 +105,6 @@ function videos_init(App $a) $a->page['htmlhead'] .= replace_macros($tpl,[ '$baseurl' => System::baseUrl(), ]); - - $tpl = get_markup_template("videos_end.tpl"); - $a->page['end'] .= replace_macros($tpl,[ - '$baseurl' => System::baseUrl(), - ]); - } return; diff --git a/mod/wallmessage.php b/mod/wallmessage.php index 5606b6feed..5e08420ecb 100644 --- a/mod/wallmessage.php +++ b/mod/wallmessage.php @@ -120,13 +120,6 @@ function wallmessage_content(App $a) { '$linkurl' => L10n::t('Please enter a link URL:') ]); - $tpl = get_markup_template('wallmsg-end.tpl'); - $a->page['end'] .= replace_macros($tpl, [ - '$baseurl' => System::baseUrl(true), - '$nickname' => $user['nickname'], - '$linkurl' => L10n::t('Please enter a link URL:') - ]); - $tpl = get_markup_template('wallmessage.tpl'); $o = replace_macros($tpl, [ '$header' => L10n::t('Send Private Message'), diff --git a/src/App.php b/src/App.php index 1a26dd6507..26bc7362f1 100644 --- a/src/App.php +++ b/src/App.php @@ -318,7 +318,6 @@ class App 'aside' => '', 'bottom' => '', 'content' => '', - 'end' => '', 'footer' => '', 'htmlhead' => '', 'nav' => '', diff --git a/view/templates/contact_end.tpl b/view/templates/contact_end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/contact_end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/contacts-end.tpl b/view/templates/contacts-end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/contacts-end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/cropend.tpl b/view/templates/cropend.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/cropend.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/end.tpl b/view/templates/end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/event_end.tpl b/view/templates/event_end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/event_end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/jot-end.tpl b/view/templates/jot-end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/jot-end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/message-end.tpl b/view/templates/message-end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/message-end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/msg-end.tpl b/view/templates/msg-end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/msg-end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/profed_end.tpl b/view/templates/profed_end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/profed_end.tpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/view/templates/videos_end.tpl b/view/templates/videos_end.tpl deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/view/templates/wallmsg-end.tpl b/view/templates/wallmsg-end.tpl deleted file mode 100644 index 8b13789179..0000000000 --- a/view/templates/wallmsg-end.tpl +++ /dev/null @@ -1 +0,0 @@ - From 73c1ebc6fd522be5ae712c058a02e105592c0f95 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:02:55 -0400 Subject: [PATCH 046/261] [frio] Move relevant code to new footer.tpl --- view/theme/frio/php/default.php | 22 +--------------------- view/theme/frio/templates/footer.tpl | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 view/theme/frio/templates/footer.tpl diff --git a/view/theme/frio/php/default.php b/view/theme/frio/php/default.php index cb68328531..d9ecb7cf0d 100644 --- a/view/theme/frio/php/default.php +++ b/view/theme/frio/php/default.php @@ -138,27 +138,7 @@ if (!isset($minimal)) {
- - - - - -
- +
diff --git a/view/theme/frio/templates/footer.tpl b/view/theme/frio/templates/footer.tpl new file mode 100644 index 0000000000..4d58ff888c --- /dev/null +++ b/view/theme/frio/templates/footer.tpl @@ -0,0 +1,23 @@ + + + + +
+ +{{foreach $footerScripts as $scriptUrl}} + +{{/foreach}} From cc73aec3baf16ed54b2a8465f16ceb968e2375d1 Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:03:23 -0400 Subject: [PATCH 047/261] Add postprocess_liveupdate JS event dispatch on post preview --- view/js/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/view/js/main.js b/view/js/main.js index ae9cb23d8e..b41750b6a5 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -667,6 +667,7 @@ function preview_post() { if (data.preview) { $("#jot-preview-content").html(data.preview); $("#jot-preview-content" + " a").click(function() {return false;}); + document.dispatchEvent(new Event('postprocess_liveupdate')); } }, "json" From 30f8fb82b632f1685b3508fe089c17e2d21bfb9b Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:05:23 -0400 Subject: [PATCH 048/261] Cleanup index.php - Removed deprecated killme() calls - Removed deprecated x() calls --- index.php | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/index.php b/index.php index aa7704930e..1adfe4ad1e 100644 --- a/index.php +++ b/index.php @@ -91,7 +91,7 @@ if (!$a->is_backend()) { * Language was set earlier, but we can over-ride it in the session. * We have to do it here because the session was just now opened. */ -if (x($_SESSION, 'authenticated') && !x($_SESSION, 'language')) { +if (!empty($_SESSION['authenticated']) && empty($_SESSION['language'])) { $_SESSION['language'] = $lang; // we haven't loaded user data yet, but we need user language if (!empty($_SESSION['uid'])) { @@ -102,7 +102,7 @@ if (x($_SESSION, 'authenticated') && !x($_SESSION, 'language')) { } } -if (x($_SESSION, 'language') && ($_SESSION['language'] !== $lang)) { +if (!empty($_SESSION['language']) && $_SESSION['language'] !== $lang) { $lang = $_SESSION['language']; L10n::loadTranslationTable($lang); } @@ -125,12 +125,12 @@ if (!empty($_GET['zrl']) && $a->mode == App::MODE_NORMAL) { logger("Invalid ZRL parameter " . $_GET['zrl'], LOGGER_DEBUG); header('HTTP/1.1 403 Forbidden'); echo "

403 Forbidden

"; - killme(); + exit(); } } } -if ((x($_GET,'owt')) && $a->mode == App::MODE_NORMAL) { +if (!empty($_GET['owt']) && $a->mode == App::MODE_NORMAL) { $token = $_GET['owt']; $a->query_string = Profile::stripQueryParam($a->query_string, 'owt'); Profile::openWebAuthInit($token); @@ -149,7 +149,7 @@ if ((x($_GET,'owt')) && $a->mode == App::MODE_NORMAL) { Login::sessionAuth(); -if (! x($_SESSION, 'authenticated')) { +if (empty($_SESSION['authenticated'])) { header('X-Account-Management-Status: none'); } @@ -291,11 +291,11 @@ if (strlen($a->module)) { if (! $a->module_loaded) { // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit. - if ((x($_SERVER, 'QUERY_STRING')) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { + if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) { killme(); } - if ((x($_SERVER, 'QUERY_STRING')) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) { + if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) { logger('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']); goaway(System::baseUrl() . $_SERVER['REQUEST_URI']); } @@ -303,11 +303,9 @@ if (strlen($a->module)) { logger('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], LOGGER_DEBUG); header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . L10n::t('Not Found')); $tpl = get_markup_template("404.tpl"); - $a->page['content'] = replace_macros( - $tpl, - [ - '$message' => L10n::t('Page not found.')] - ); + $a->page['content'] = replace_macros($tpl, [ + '$message' => L10n::t('Page not found.') + ]); } } @@ -466,7 +464,7 @@ if (isset($_GET["mode"]) && ($_GET["mode"] == "raw")) { echo substr($target->saveHTML(), 6, -8); - killme(); + exit(); } $page = $a->page; @@ -504,5 +502,3 @@ if (empty($template)) { /// @TODO Looks unsafe (remote-inclusion), is maybe not but Theme::getPathForFile() uses file_exists() but does not escape anything require_once $template; - -killme(); From 2ae6556b3293f20647edeea36b793fda8f65691f Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Thu, 20 Sep 2018 21:30:51 -0400 Subject: [PATCH 049/261] Add new 'head' hook - Add new App->registerStylesheet method - Reworked App->init_pagehead into App->initHead --- doc/Addons.md | 8 +++++++- index.php | 2 +- src/App.php | 26 +++++++++++++++++--------- view/templates/head.tpl | 4 ++++ view/theme/frio/templates/head.tpl | 4 ++++ 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/doc/Addons.md b/doc/Addons.md index b126fedc4d..b8364d9353 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -252,6 +252,11 @@ Called after conversion of bbcode to HTML. Called after tag conversion of HTML to bbcode (e.g. remote message posting) `$b` is a string converted text +### head +Called when building the `` sections. +Stylesheets should be registered using this hook. +`$b` is an HTML string of the `` tag. + ### page_header Called after building the page navigation section. `$b` is a string HTML of nav region. @@ -288,8 +293,8 @@ Called after HTML content functions have completed. ### footer Called after HTML content functions have completed. +Deferred Javascript files should be registered using this hook. `$b` is (string) HTML of footer div/element. -Used to load deferred Javascript files. ### avatar_lookup Called when looking up the avatar. `$b` is an array: @@ -568,6 +573,7 @@ Here is a complete list of all hook callbacks with file locations (as of 01-Apr- ### src/App.php Addon::callHooks('load_config'); + Addon::callHooks('head'); Addon::callHooks('footer'); ### src/Model/Item.php diff --git a/index.php b/index.php index 1adfe4ad1e..359be7dcba 100644 --- a/index.php +++ b/index.php @@ -395,7 +395,7 @@ if ($a->module_loaded) { * theme choices made by the modules can take effect. */ -$a->init_pagehead(); +$a->initHead(); /* * Build the page ending -- this is stuff that goes right before diff --git a/src/App.php b/src/App.php index 26bc7362f1..479813703c 100644 --- a/src/App.php +++ b/src/App.php @@ -96,13 +96,21 @@ class App public $force_max_items = 0; public $theme_events_in_profile = true; + public $stylesheets = []; public $footerScripts = []; + public function registerStylesheet($path) + { + $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path); + + $this->stylesheets[] = trim($url, '/'); + } + public function registerFooterScript($path) { $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path); - $this->footerScripts[] = $this->get_baseurl() . '/' . trim($url, '/'); + $this->footerScripts[] = trim($url, '/'); } /** @@ -741,7 +749,7 @@ class App $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; } - public function init_pagehead() + public function initHead() { $interval = ((local_user()) ? PConfig::get(local_user(), 'system', 'update_interval') : 40000); @@ -766,9 +774,6 @@ class App * since the code added by the modules frequently depends on it * being first */ - if (!isset($this->page['htmlhead'])) { - $this->page['htmlhead'] = ''; - } // If we're using Smarty, then doing replace_macros() will replace // any unrecognized variables with a blank string. Since we delay @@ -791,7 +796,9 @@ class App } // get data wich is needed for infinite scroll on the network page - $invinite_scroll = infinite_scroll_data($this->module); + $infinite_scroll = infinite_scroll_data($this->module); + + Core\Addon::callHooks('head', $this->page['htmlhead']); $tpl = get_markup_template('head.tpl'); $this->page['htmlhead'] = replace_macros($tpl, [ @@ -805,8 +812,9 @@ class App '$shortcut_icon' => $shortcut_icon, '$touch_icon' => $touch_icon, '$stylesheet' => $stylesheet, - '$infinite_scroll' => $invinite_scroll, + '$infinite_scroll' => $infinite_scroll, '$block_public' => intval(Config::get('system', 'block_public')), + '$stylesheets' => $this->stylesheets, ]) . $this->page['htmlhead']; } @@ -845,10 +853,10 @@ class App Core\Addon::callHooks('footer', $this->page['footer']); $tpl = get_markup_template('footer.tpl'); - $this->page['footer'] .= replace_macros($tpl, [ + $this->page['footer'] = replace_macros($tpl, [ '$baseurl' => $this->get_baseurl(), '$footerScripts' => $this->footerScripts, - ]); + ]) . $this->page['footer']; } public function set_curl_code($code) diff --git a/view/templates/head.tpl b/view/templates/head.tpl index eef985bc6f..46b7283ac2 100644 --- a/view/templates/head.tpl +++ b/view/templates/head.tpl @@ -11,6 +11,10 @@ +{{foreach $stylesheets as $stylesheetUrl}} + +{{/foreach}} + diff --git a/view/theme/frio/templates/head.tpl b/view/theme/frio/templates/head.tpl index 109127ccee..e7c3a4de88 100644 --- a/view/theme/frio/templates/head.tpl +++ b/view/theme/frio/templates/head.tpl @@ -26,21 +26,14 @@ -{{* The own style.css *}} - +{{foreach $stylesheets as $stylesheetUrl}} + +{{/foreach}} {{* own css files *}} -{{foreach $stylesheets as $stylesheetUrl}} - -{{/foreach}} - - From 489d5f41d26f6cecdfcb2b9fbffedd715351b07b Mon Sep 17 00:00:00 2001 From: Benjamin Lorteau Date: Fri, 21 Sep 2018 09:54:40 -0400 Subject: [PATCH 055/261] Add Doxygen headers for multiple function in App --- src/App.php | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/App.php b/src/App.php index 3f67dbada9..c00f898f34 100644 --- a/src/App.php +++ b/src/App.php @@ -99,6 +99,15 @@ class App public $stylesheets = []; public $footerScripts = []; + /** + * Register a stylesheet file path to be included in the tag of every page. + * Inclusion is done in App->initHead(). + * The path can be absolute or relative to the Friendica installation base folder. + * + * @see App->initHead() + * + * @param string $path + */ public function registerStylesheet($path) { $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path); @@ -106,6 +115,15 @@ class App $this->stylesheets[] = trim($url, '/'); } + /** + * Register a javascript file path to be included in the