Merge pull request #5794 from annando/ap1

ActivityPub support
This commit is contained in:
Hypolite Petovan 2018-10-02 11:24:04 -04:00 committed by GitHub
commit 505350c9fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 4014 additions and 1065 deletions

View file

@ -41,7 +41,7 @@ define('FRIENDICA_PLATFORM', 'Friendica');
define('FRIENDICA_CODENAME', 'The Tazmans Flax-lily');
define('FRIENDICA_VERSION', '2018.12-dev');
define('DFRN_PROTOCOL_VERSION', '2.23');
define('DB_UPDATE_VERSION', 1283);
define('DB_UPDATE_VERSION', 1284);
define('NEW_UPDATE_ROUTINE_VERSION', 1170);
/**

View file

@ -37,12 +37,13 @@
"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",
"friendica/json-ld": "^1.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/pear/Text_Highlighter"
"url": "https://git.friendi.ca/friendica/php-json-ld"
}
],
"autoload": {

92
composer.lock generated
View file

@ -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": "5f6a43237dc52758484cd21cd76e8ce6",
"content-hash": "ece71ff50417ed5fcc1a439ea554925c",
"packages": [
{
"name": "asika/simple-console",
@ -241,6 +241,50 @@
],
"time": "2015-08-05T01:03:42+00:00"
},
{
"name": "friendica/json-ld",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://git.friendi.ca/friendica/php-json-ld",
"reference": "a9ac64daf01cfd97e80c36a5104247d37c0ae5ef"
},
"require": {
"ext-json": "*",
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"files": [
"jsonld.php"
]
},
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Digital Bazaar, Inc.",
"email": "support@digitalbazaar.com",
"url": "http://digitalbazaar.com/"
},
{
"name": "Friendica Team",
"url": "https://friendi.ca/"
}
],
"description": "A JSON-LD Processor and API implementation in PHP.",
"homepage": "https://git.friendi.ca/friendica/php-json-ld",
"keywords": [
"JSON",
"JSON-LD",
"Linked Data",
"RDF",
"Semantic Web",
"jsonld"
],
"time": "2018-09-28T00:01:12+00:00"
},
{
"name": "fxp/composer-asset-plugin",
"version": "v1.4.2",
@ -2000,6 +2044,52 @@
}
],
"packages-dev": [
{
"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": "doctrine/instantiator",
"version": "1.0.5",

View file

@ -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": {
@ -215,7 +243,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"}
},

View file

@ -4612,7 +4612,7 @@ function post_photo_item($hash, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $f
$owner_record = DBA::selectFirst('contact', [], ['uid' => api_user(), 'self' => true]);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = intval(api_user());
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;

View file

@ -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 {

View file

@ -332,15 +332,21 @@ if ($a->module_loaded) {
$a->page['page_title'] = $a->module;
$placeholder = '';
Addon::callHooks($a->module . '_mod_init', $placeholder);
if ($a->module_class) {
Addon::callHooks($a->module . '_mod_init', $placeholder);
call_user_func([$a->module_class, 'init']);
} else if (function_exists($a->module . '_init')) {
Addon::callHooks($a->module . '_mod_init', $placeholder);
$func = $a->module . '_init';
$func($a);
}
// "rawContent" is especially meant for technical endpoints.
// This endpoint doesn't need any theme initialization or other comparable stuff.
if (!$a->error && $a->module_class) {
call_user_func([$a->module_class, 'rawContent']);
}
if (function_exists(str_replace('-', '_', $a->getCurrentTheme()) . '_init')) {
$func = str_replace('-', '_', $a->getCurrentTheme()) . '_init';
$func($a);

View file

@ -1488,7 +1488,7 @@ function admin_page_site(App $a)
'$community_page_style' => ['community_page_style', L10n::t("Community pages for visitors"), Config::get('system','community_page_style'), L10n::t("Which community pages should be available for visitors. Local users always see both pages."), $community_page_style_choices],
'$max_author_posts_community_page' => ['max_author_posts_community_page', L10n::t("Posts per user on community page"), Config::get('system','max_author_posts_community_page'), L10n::t("The maximum number of posts per user on the community page. \x28Not valid for 'Global Community'\x29")],
'$ostatus_disabled' => ['ostatus_disabled', L10n::t("Enable OStatus support"), !Config::get('system','ostatus_disabled'), L10n::t("Provide built-in OStatus \x28StatusNet, GNU Social etc.\x29 compatibility. All communications in OStatus are public, so privacy warnings will be occasionally displayed.")],
'$ostatus_full_threads' => ['ostatus_full_threads', L10n::t("Only import OStatus threads from our contacts"), Config::get('system','ostatus_full_threads'), L10n::t("Normally we import every content from our OStatus contacts. With this option we only store threads that are started by a contact that is known on our system.")],
'$ostatus_full_threads' => ['ostatus_full_threads', L10n::t("Only import OStatus/ActivityPub threads from our contacts"), Config::get('system','ostatus_full_threads'), L10n::t("Normally we import every content from our OStatus and ActivityPub contacts. With this option we only store threads that are started by a contact that is known on our system.")],
'$ostatus_not_able' => L10n::t("OStatus support can only be enabled if threading is enabled."),
'$diaspora_able' => $diaspora_able,
'$diaspora_not_able' => L10n::t("Diaspora support can't be enabled because Friendica was installed into a sub directory."),

View file

@ -533,7 +533,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 = "";
}
@ -555,7 +555,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"]));
@ -966,7 +966,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"]);
}
}
@ -990,7 +990,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"]);
}
}
@ -1071,7 +1071,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

View file

@ -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::transmitContactAccept($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']);
@ -348,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 {
@ -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)) {
@ -403,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) {

View file

@ -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]));
}
}

View file

@ -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,10 @@ function display_init(App $a)
displayShowFeed($item["id"], false);
}
if (ActivityPub::isRequest()) {
goaway(str_replace('display/', 'object/', $a->query_string));
}
if ($item["id"] != $item["parent"]) {
$item = Item::selectFirstForUser(local_user(), $fields, ['id' => $item["parent"]]);
}

View file

@ -159,7 +159,7 @@ function item_post(App $a) {
}
// Allow commenting if it is an answer to a public post
$allow_comment = local_user() && ($profile_uid == 0) && $parent && in_array($parent_item['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
$allow_comment = local_user() && ($profile_uid == 0) && $parent && in_array($parent_item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
// Now check that valid personal details have been provided
if (!can_write_wall($profile_uid) && !$allow_comment) {
@ -240,7 +240,7 @@ function item_post(App $a) {
$emailcc = notags(trim(defaults($_REQUEST, 'emailcc' , '')));
$body = escape_tags(trim(defaults($_REQUEST, 'body' , '')));
$network = notags(trim(defaults($_REQUEST, 'network' , Protocol::DFRN)));
$guid = System::createGUID(32);
$guid = System::createUUID();
$postopts = defaults($_REQUEST, 'postopts', '');
@ -343,20 +343,11 @@ function item_post(App $a) {
$tags = get_tags($body);
// Add a tag if the parent contact is from OStatus (This will notify them during delivery)
if ($parent) {
if ($thr_parent_contact['network'] == Protocol::OSTATUS) {
$contact = '@[url=' . $thr_parent_contact['url'] . ']' . $thr_parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $thr_parent_contact['url'] . ']')) {
$tags[] = $contact;
}
}
if ($parent_contact['network'] == Protocol::OSTATUS) {
$contact = '@[url=' . $parent_contact['url'] . ']' . $parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $parent_contact['url'] . ']')) {
$tags[] = $contact;
}
// Add a tag if the parent contact is from ActivityPub or OStatus (This will notify them)
if ($parent && in_array($thr_parent_contact['network'], [Protocol::OSTATUS, Protocol::ACTIVITYPUB])) {
$contact = '@[url=' . $thr_parent_contact['url'] . ']' . $thr_parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $thr_parent_contact['url'] . ']')) {
$tags[] = $contact;
}
}
@ -1026,8 +1017,7 @@ function handle_tag(App $a, &$body, &$inform, &$str_tags, $profile_uid, $tag, $n
$alias = $contact["alias"];
$newname = $contact["nick"];
if (($newname == "") || (($contact["network"] != Protocol::OSTATUS) && ($contact["network"] != Protocol::TWITTER)
&& ($contact["network"] != Protocol::STATUSNET))) {
if (($newname == "") || !in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::TWITTER, Protocol::STATUSNET])) {
$newname = $contact["name"];
}
}

View file

@ -472,7 +472,7 @@ function photos_post(App $a)
$uri = Item::newURI($page_owner_uid);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;
@ -651,7 +651,7 @@ function photos_post(App $a)
$uri = Item::newURI($page_owner_uid);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;
@ -889,7 +889,7 @@ function photos_post(App $a)
$arr['coord'] = $lat . ' ' . $lon;
}
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;

View file

@ -97,7 +97,7 @@ function poke_init(App $a)
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = (!empty($parent_uri) ? $parent_uri : $uri);

View file

@ -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,16 @@ function profile_init(App $a)
DFRN::autoRedir($a, $which);
}
if (ActivityPub::isRequest()) {
$user = DBA::selectFirst('user', ['uid'], ['nickname' => $which]);
if (DBA::isResult($user)) {
$data = ActivityPub::profile($user['uid']);
echo json_encode($data);
header('Content-Type: application/activity+json');
exit();
}
}
Profile::load($a, $which, $profile);
$blocked = !local_user() && !remote_user() && Config::get('system', 'block_public');

View file

@ -108,7 +108,7 @@ EOT;
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];

View file

@ -115,7 +115,7 @@ EOT;
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];

View file

@ -80,6 +80,7 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
['rel' => NAMESPACE_DFRN, 'href' => $profile_url],
['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']],
['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url],
['rel' => 'self', 'type' => 'application/activity+json', 'href' => $profile_url],
['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']],
['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']],
['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'],
@ -92,6 +93,7 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-dfrn+json', 'href' => System::baseUrl().'/owa']
]
];
echo json_encode($json);
killme();
}

View file

@ -21,7 +21,16 @@ abstract class BaseModule extends BaseObject
*/
public static function init()
{
}
/**
* @brief Module GET method to display raw content from technical endpoints
*
* Extend this method if the module is supposed to return communication data,
* e.g. from protocol implementations.
*/
public static function rawContent()
{
}
/**

View file

@ -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,13 +100,17 @@ 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));
if (DBA::isResult($r)) {
$networkname = $r['platform'];
if ($s == Protocol::ACTIVITYPUB) {
$networkname .= ' (AP)';
}
}
}

View file

@ -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);

View file

@ -161,6 +161,18 @@ class System extends BaseObject
killme();
}
/**
* Generates a random string in the UUID format
*
* @param bool|string $prefix A given prefix (default is empty)
* @return string a generated UUID
*/
public static function createUUID($prefix = '')
{
$guid = System::createGUID(32, $prefix);
return substr($guid, 0, 8). '-' . substr($guid, 8, 4) . '-' . substr($guid, 12, 4) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12);
}
/**
* Generates a GUID with the given parameters
*

175
src/Model/APContact.php Normal file
View file

@ -0,0 +1,175 @@
<?php
/**
* @file src/Model/APContact.php
*/
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\Network;
use Friendica\Util\JsonLD;
use Friendica\Util\DateTimeFormat;
require_once 'boot.php';
class APContact extends BaseObject
{
/**
* 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 a given url
*
* @param string $url profile url
* @param boolean $update true = always update, false = never update, null = update when not found
* @return array profile array
*/
public static function getByURL($url, $update = null)
{
if (empty($url)) {
return false;
}
if (empty($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 (!is_null($update)) {
return false;
}
}
if (empty(parse_url($url, PHP_URL_SCHEME))) {
$url = self::addrToUrl($url);
if (empty($url)) {
return false;
}
}
$data = ActivityPub::fetchContent($url);
if (empty($data) || empty($data['id']) || empty($data['inbox'])) {
return false;
}
$apcontact = [];
$apcontact['url'] = $data['id'];
$apcontact['uuid'] = defaults($data, 'diaspora:guid', 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'] = 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'] = 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(JsonLD::fetchElement($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']) {
$parts = parse_url($apcontact['url']);
unset($parts['path']);
$apcontact['baseurl'] = Network::unparseURL($parts);
} else {
$apcontact['addr'] = null;
$apcontact['baseurl'] = null;
}
if ($apcontact['url'] == $apcontact['alias']) {
$apcontact['alias'] = null;
}
$apcontact['updated'] = DateTimeFormat::utcNow();
DBA::update('apcontact', $apcontact, ['url' => $url], true);
// Update some data in the contact table with various ways to catch them all
$contact_fields = ['name' => $apcontact['name'], 'about' => $apcontact['about']];
DBA::update('contact', $contact_fields, ['nurl' => normalise_link($url)]);
$contacts = DBA::select('contact', ['uid', 'id'], ['nurl' => normalise_link($url)]);
while ($contact = DBA::fetch($contacts)) {
Contact::updateAvatar($apcontact['photo'], $contact['uid'], $contact['id']);
}
DBA::close($contacts);
// Update the gcontact table
DBA::update('gcontact', $contact_fields, ['nurl' => normalise_link($url)]);
logger('Updated profile for ' . $url, LOGGER_DEBUG);
return $apcontact;
}
}

View file

@ -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,12 @@ class Contact extends BaseObject
}
} elseif ($contact['network'] == Protocol::DIASPORA) {
Diaspora::sendUnshare($user, $contact);
} elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
ActivityPub::transmitContactUndo($contact['url'], $user['uid']);
if ($dissolve) {
ActivityPub::transmitContactReject($contact['url'], $contact['hub-verify'], $user['uid']);
}
}
}
@ -775,7 +782,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"]);
}
@ -1054,7 +1061,6 @@ class Contact extends BaseObject
if (!x($contact, 'avatar')) {
$update_contact = true;
}
if (!$update_contact || $no_update) {
return $contact_id;
}
@ -1088,7 +1094,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;
}
@ -1316,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::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],
@ -1495,10 +1495,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 +1512,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 +1538,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 +1688,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 +1768,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 +1819,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;
@ -1827,7 +1832,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'];
@ -1839,13 +1844,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::transmitContactAccept($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`)

View file

@ -17,13 +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_SPLIT_CONVERSATION = 6;
const PARCEL_TWITTER = 67;
const PARCEL_UNKNOWN = 255;
/**
* @brief Store the conversation data
@ -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 (empty($conversation['source']) || (!empty($old_conv['source']) &&
($old_conv['protocol'] < defaults($conversation, 'protocol', PARCEL_UNKNOWN)))) {
unset($conversation['protocol']);
unset($conversation['source']);
}

View file

@ -314,7 +314,7 @@ class Event extends BaseObject
Addon::callHooks('event_updated', $event['id']);
} else {
$event['guid'] = defaults($arr, 'guid', System::createGUID(32));
$event['guid'] = defaults($arr, 'guid', System::createUUID());
// New event. Store it.
DBA::insert('event', $event);

View file

@ -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;
}
@ -1081,9 +1081,8 @@ class Item extends BaseObject
DBA::delete('item-delivery-data', ['iid' => $item['id']]);
if (!empty($item['iaid']) && !self::exists(['iaid' => $item['iaid'], 'deleted' => false])) {
DBA::delete('item-activity', ['id' => $item['iaid']], ['cascade' => false]);
}
// We don't delete the item-activity here, since we need some of the data for ActivityPub
if (!empty($item['icid']) && !self::exists(['icid' => $item['icid'], 'deleted' => false])) {
DBA::delete('item-content', ['id' => $item['icid']], ['cascade' => false]);
}
@ -1205,7 +1204,7 @@ class Item extends BaseObject
} elseif (!empty($item['uri'])) {
$guid = self::guidFromUri($item['uri'], $prefix_host);
} else {
$guid = System::createGUID(32, hash('crc32', $prefix_host));
$guid = System::createUUID(hash('crc32', $prefix_host));
}
return $guid;
@ -1352,7 +1351,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];
@ -2054,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)) {
@ -2072,14 +2071,46 @@ class Item extends BaseObject
$users = [];
$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];
/// @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;
}
$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);
}
$origin_uid = 0;
@ -2176,7 +2207,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;
}
@ -2327,16 +2358,10 @@ class Item extends BaseObject
public static function newURI($uid, $guid = "")
{
if ($guid == "") {
$guid = System::createGUID(32);
$guid = System::createUUID();
}
$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;
}
/**
@ -2660,7 +2685,7 @@ class Item extends BaseObject
}
if ($contact['network'] != Protocol::FEED) {
$datarray["guid"] = System::createGUID(32);
$datarray["guid"] = System::createUUID();
unset($datarray["plink"]);
$datarray["uri"] = self::newURI($contact['uid'], $datarray["guid"]);
$datarray["parent-uri"] = $datarray["uri"];
@ -2826,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']));
@ -3089,7 +3114,7 @@ class Item extends BaseObject
$objtype = $item['resource-id'] ? ACTIVITY_OBJ_IMAGE : ACTIVITY_OBJ_NOTE ;
$new_item = [
'guid' => System::createGUID(32),
'guid' => System::createUUID(),
'uri' => self::newURI($item['uid']),
'uid' => $item['uid'],
'contact-id' => $item_contact_id,

View file

@ -46,7 +46,7 @@ class Mail
return -2;
}
$guid = System::createGUID(32);
$guid = System::createUUID();
$uri = 'urn:X-dfrn:' . System::baseUrl() . ':' . local_user() . ':' . $guid;
$convid = 0;
@ -73,7 +73,7 @@ class Mail
$recip_handle = (($contact['addr']) ? $contact['addr'] : $contact['nick'] . '@' . $recip_host);
$sender_handle = $a->user['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
$conv_guid = System::createGUID(32);
$conv_guid = System::createUUID();
$convuri = $recip_handle . ':' . $conv_guid;
$handles = $recip_handle . ';' . $sender_handle;
@ -171,7 +171,7 @@ class Mail
$subject = L10n::t('[no subject]');
}
$guid = System::createGUID(32);
$guid = System::createUUID();
$uri = 'urn:X-dfrn:' . System::baseUrl() . ':' . local_user() . ':' . $guid;
$me = Probe::uri($replyto);
@ -180,7 +180,7 @@ class Mail
return -2;
}
$conv_guid = System::createGUID(32);
$conv_guid = System::createUUID();
$recip_handle = $recipient['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);

View file

@ -28,6 +28,19 @@ require_once 'include/dba.php';
class Profile
{
/**
* @brief Returns default profile for a given user id
*
* @param integer User ID
*
* @return array Profile data
*/
public static function getByUID($uid)
{
$profile = DBA::selectFirst('profile', [], ['uid' => $uid, 'is-default' => true]);
return $profile;
}
/**
* @brief Returns a formatted location string from the given profile array
*

View file

@ -33,6 +33,17 @@ class Term
return $tag_text;
}
public static function tagArrayFromItemId($itemid, $type = [TERM_HASHTAG, TERM_MENTION])
{
$condition = ['otype' => TERM_OBJ_POST, 'oid' => $itemid, 'type' => $type];
$tags = DBA::select('term', ['type', 'term', 'url'], $condition);
if (!DBA::isResult($tags)) {
return [];
}
return DBA::toArray($tags);
}
public static function fileTextFromItemId($itemid)
{
$file_text = '';
@ -99,6 +110,18 @@ class Term
$pattern = '/\W([\#@])\[url\=(.*?)\](.*?)\[\/url\]/ism';
if (preg_match_all($pattern, $data, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if ($match[1] == '@') {
$contact = Contact::getDetailsByURL($match[2], 0);
if (!empty($contact['addr'])) {
$match[3] = $contact['addr'];
}
if (!empty($contact['url'])) {
$match[2] = $contact['url'];
}
}
$tags[$match[1] . trim($match[3], ',.:;[]/\"?!')] = $match[2];
}
}
@ -119,12 +142,22 @@ class Term
$term = substr($tag, 1);
} elseif (substr(trim($tag), 0, 1) == '@') {
$type = TERM_MENTION;
$term = substr($tag, 1);
$contact = Contact::getDetailsByURL($link, 0);
if (!empty($contact['name'])) {
$term = $contact['name'];
} else {
$term = substr($tag, 1);
}
} else { // This shouldn't happen
$type = TERM_HASHTAG;
$term = $tag;
}
if (DBA::exists('term', ['uid' => $message['uid'], 'otype' => TERM_OBJ_POST, 'oid' => $itemid, 'url' => $link])) {
continue;
}
if ($message['uid'] == 0) {
$global = true;
DBA::update('term', ['global' => true], ['otype' => TERM_OBJ_POST, 'guid' => $message['guid']]);

View file

@ -31,6 +31,23 @@ require_once 'include/text.php';
*/
class User
{
/**
* @brief Returns the user id of a given profile url
*
* @param string $profile
*
* @return integer user id
*/
public static function getIdForURL($url)
{
$self = DBA::selectFirst('contact', ['uid'], ['nurl' => normalise_link($url), 'self' => true]);
if (!DBA::isResult($self)) {
return false;
} else {
return $self['uid'];
}
}
/**
* @brief Get owner data by user id
*
@ -495,7 +512,7 @@ class User
$spubkey = $sres['pubkey'];
$insert_result = DBA::insert('user', [
'guid' => System::createGUID(32),
'guid' => System::createUUID(),
'username' => $username,
'password' => $new_password_encoded,
'email' => $email,

38
src/Module/Followers.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* @file src/Module/Followers.php
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\System;
use Friendica\Model\User;
/**
* ActivityPub Followers
*/
class Followers extends BaseModule
{
public static function rawContent()
{
$a = self::getApp();
if (empty($a->argv[1])) {
System::httpExit(404);
}
$owner = User::getOwnerDataByNick($a->argv[1]);
if (empty($owner)) {
System::httpExit(404);
}
$page = defaults($_REQUEST, 'page', null);
$followers = ActivityPub::getFollowers($owner, $page);
header('Content-Type: application/activity+json');
echo json_encode($followers);
exit();
}
}

38
src/Module/Following.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* @file src/Module/Following.php
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\System;
use Friendica\Model\User;
/**
* ActivityPub Following
*/
class Following extends BaseModule
{
public static function rawContent()
{
$a = self::getApp();
if (empty($a->argv[1])) {
System::httpExit(404);
}
$owner = User::getOwnerDataByNick($a->argv[1]);
if (empty($owner)) {
System::httpExit(404);
}
$page = defaults($_REQUEST, 'page', null);
$Following = ActivityPub::getFollowing($owner, $page);
header('Content-Type: application/activity+json');
echo json_encode($Following);
exit();
}
}

55
src/Module/Inbox.php Normal file
View file

@ -0,0 +1,55 @@
<?php
/**
* @file src/Module/Inbox.php
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\System;
use Friendica\Database\DBA;
use Friendica\Util\HTTPSignature;
/**
* ActivityPub Inbox
*/
class Inbox extends BaseModule
{
public static function rawContent()
{
$a = self::getApp();
$postdata = file_get_contents('php://input');
if (empty($postdata)) {
System::httpExit(400);
}
// Enable for test purposes
/*
if (HTTPSignature::getSigner($postdata, $_SERVER)) {
$filename = 'signed-activitypub';
} else {
$filename = 'failed-activitypub';
}
$tempfile = tempnam(get_temppath(), $filename);
file_put_contents($tempfile, json_encode(['argv' => $a->argv, 'header' => $_SERVER, 'body' => $postdata], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
logger('Incoming message stored under ' . $tempfile);
*/
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);
}
}

View file

@ -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.

41
src/Module/Object.php Normal file
View file

@ -0,0 +1,41 @@
<?php
/**
* @file src/Module/Object.php
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\System;
use Friendica\Model\Item;
use Friendica\Database\DBA;
/**
* ActivityPub Object
*/
class Object extends BaseModule
{
public static function rawContent()
{
$a = self::getApp();
if (empty($a->argv[1])) {
System::httpExit(404);
}
if (!ActivityPub::isRequest()) {
goaway(str_replace('object/', 'display/', $a->query_string));
}
$item = Item::selectFirst(['id'], ['guid' => $a->argv[1], 'wall' => true, 'private' => false]);
if (!DBA::isResult($item)) {
System::httpExit(404);
}
$data = ActivityPub::createObjectFromItemID($item['id']);
header('Content-Type: application/activity+json');
echo json_encode($data);
exit();
}
}

38
src/Module/Outbox.php Normal file
View file

@ -0,0 +1,38 @@
<?php
/**
* @file src/Module/Outbox.php
*/
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\System;
use Friendica\Model\User;
/**
* ActivityPub Outbox
*/
class Outbox extends BaseModule
{
public static function rawContent()
{
$a = self::getApp();
if (empty($a->argv[1])) {
System::httpExit(404);
}
$owner = User::getOwnerDataByNick($a->argv[1]);
if (empty($owner)) {
System::httpExit(404);
}
$page = defaults($_REQUEST, 'page', null);
$outbox = ActivityPub::getOutbox($owner, $page);
header('Content-Type: application/activity+json');
echo json_encode($outbox);
exit();
}
}

View file

@ -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);

View file

@ -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,17 @@ class Probe
$uid = local_user();
}
$data = self::detect($uri, $network, $uid);
if ($network != Protocol::ACTIVITYPUB) {
$data = self::detect($uri, $network, $uid);
} else {
$data = null;
}
$ap_profile = ActivityPub::probeProfile($uri);
if (!empty($ap_profile) && (defaults($data, 'network', '') != Protocol::DFRN)) {
$data = $ap_profile;
}
if (!isset($data["url"])) {
$data["url"] = $uri;

View file

@ -324,7 +324,7 @@ class Post extends BaseObject
$owner_name_e = $this->getOwnerName();
// Disable features that aren't available in several networks
if (!in_array($item["network"], [Protocol::DFRN, Protocol::DIASPORA]) && isset($buttons["dislike"])) {
if (!in_array($item["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA]) && isset($buttons["dislike"])) {
unset($buttons["dislike"]);
$isevent = false;
$tagger = '';

1944
src/Protocol/ActivityPub.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -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 "";
@ -3204,7 +3200,7 @@ class Diaspora
$author = self::myHandle($owner);
$message = ["author" => $author,
"guid" => System::createGUID(32),
"guid" => System::createUUID(),
"parent_type" => "Post",
"parent_guid" => $item["guid"]];

View file

@ -2004,8 +2004,7 @@ class OStatus
}
if (intval($item["parent"]) > 0) {
$conversation_href = System::baseUrl()."/display/".$owner["nick"]."/".$item["parent"];
$conversation_uri = $conversation_href;
$conversation_href = $conversation_uri = str_replace('/object/', '/context/', $item['parent-uri']);
if (isset($parent_item)) {
$conversation = DBA::selectFirst('conversation', ['conversation-uri', 'conversation-href'], ['item-uri' => $parent_item]);

View file

@ -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;
}

View file

@ -5,94 +5,64 @@
*/
namespace Friendica\Util;
use Friendica\BaseObject;
use Friendica\Core\Config;
use Friendica\Database\DBA;
use Friendica\Model\User;
use Friendica\Model\APContact;
use Friendica\Protocol\ActivityPub;
/**
* @brief Implements HTTP Signatures per draft-cavage-http-signatures-07.
*
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Web/HTTPSig.php
*
* Other parts of the code for HTTP signing are taken from the Osada project.
* https://framagit.org/macgirvin/osada
*
* @see https://tools.ietf.org/html/draft-cavage-http-signatures-07
*/
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 = '')
/**
* @brief Verifies a magic request
*
* @param $key
*
* @return array with verification data
*/
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'];
@ -112,13 +82,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'];
@ -127,12 +91,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;
}
@ -149,130 +107,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;
}
/**
* 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
*
* @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;
}
@ -280,35 +147,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));
@ -405,4 +264,178 @@ class HTTPSignature
return '';
}
/*
* Functions for ActivityPub
*/
/**
* @brief Transmit given data to a target for a user
*
* @param $data
* @param $target
* @param $uid
*/
public static function transmit($data, $target, $uid)
{
$owner = User::getOwnerDataById($uid);
if (!$owner) {
return;
}
$content = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// 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);
}
/**
* @brief Gets a signer from a given HTTP request
*
* @param $content
* @param $http_headers
*
* @return signer string
*/
public static function getSigner($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['pubkey'], $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 $key['url'];
}
/**
* @brief fetches a key for a given id and actor
*
* @param $id
* @param $actor
*
* @return array with actor url and public key
*/
private static function fetchKey($id, $actor)
{
$url = (strpos($id, '#') ? substr($id, 0, strpos($id, '#')) : $id);
$profile = APContact::getByURL($url);
if (!empty($profile)) {
logger('Taking key from id ' . $id, LOGGER_DEBUG);
return ['url' => $url, 'pubkey' => $profile['pubkey']];
} elseif ($url != $actor) {
$profile = APContact::getByURL($actor);
if (!empty($profile)) {
logger('Taking key from actor ' . $actor, LOGGER_DEBUG);
return ['url' => $actor, 'pubkey' => $profile['pubkey']];
}
}
return false;
}
}

143
src/Util/JsonLD.php Normal file
View file

@ -0,0 +1,143 @@
<?php
/**
* @file src/Util/JsonLD.php
*/
namespace Friendica\Util;
use Friendica\Core\Cache;
use Exception;
/**
* @brief This class contain methods to work with JsonLD data
*/
class JsonLD
{
/**
* @brief Loader for LD-JSON validation
*
* @param $url
*
* @return the loaded data
*/
public static function documentLoader($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('documentLoader:' . $url);
if (!is_null($result)) {
return $result;
}
$data = jsonld_default_document_loader($url);
Cache::set('documentLoader:' . $url, $data, CACHE_DAY);
return $data;
}
/**
* @brief Normalises a given JSON array
*
* @param array $json
*
* @return normalized JSON string
*/
public static function normalize($json)
{
jsonld_set_document_loader('Friendica\Util\JsonLD::documentLoader');
$jsonobj = json_decode(json_encode($json, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
try {
$normalized = jsonld_normalize($jsonobj, array('algorithm' => 'URDNA2015', 'format' => 'application/nquads'));
}
catch (Exception $e) {
$normalized = false;
logger('normalise error:' . print_r($e, true), LOGGER_DEBUG);
}
return $normalized;
}
/**
* @brief Compacts a given JSON array
*
* @param array $json
*
* @return comacted JSON array
*/
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, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
$compacted = jsonld_compact($jsonobj, $context);
return json_decode(json_encode($compacted, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), true);
}
/**
* @brief Fetches an element from a JSON array
*
* @param $array
* @param $element
* @param $key
* @param $type
* @param $type_value
*
* @return fetched element
*/
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;
}
}

89
src/Util/LDSignature.php Normal file
View file

@ -0,0 +1,89 @@
<?php
namespace Friendica\Util;
use Friendica\Util\JsonLD;
use Friendica\Util\DateTimeFormat;
use Friendica\Protocol\ActivityPub;
use Friendica\Model\APContact;
/**
* @brief Implements JSON-LD signatures
*
* Ported from Osada: https://framagit.org/macgirvin/osada
*/
class LDSignature
{
public static function isSigned($data)
{
return !empty($data['signature']);
}
public static function getSigner($data)
{
if (!self::isSigned($data)) {
return false;
}
$actor = JsonLD::fetchElement($data, 'actor', 'id');
if (empty($actor)) {
return false;
}
$profile = APContact::getByURL($actor);
if (empty($profile['pubkey'])) {
return false;
}
$pubkey = $profile['pubkey'];
$ohash = self::hash(self::signableOptions($data['signature']));
$dhash = self::hash(self::signableData($data));
$x = Crypto::rsaVerify($ohash . $dhash, base64_decode($data['signature']['signatureValue']), $pubkey);
logger('LD-verify: ' . intval($x));
if (empty($x)) {
return false;
} else {
return $actor;
}
}
public static function sign($data, $owner)
{
$options = [
'type' => 'RsaSignature2017',
'nonce' => random_string(64),
'creator' => $owner['url'] . '#main-key',
'created' => DateTimeFormat::utcNow(DateTimeFormat::ATOM)
];
$ohash = self::hash(self::signableOptions($options));
$dhash = self::hash(self::signableData($data));
$options['signatureValue'] = base64_encode(Crypto::rsaSign($ohash . $dhash, $owner['uprvkey']));
return array_merge($data, ['signature' => $options]);
}
private static function signableData($data)
{
unset($data['signature']);
return $data;
}
private static function signableOptions($options)
{
$newopts = ['@context' => 'https://w3id.org/identity/v1'];
unset($options['type']);
unset($options['id']);
unset($options['signatureValue']);
return array_merge($newopts, $options);
}
private static function hash($obj)
{
return hash('sha256', JsonLD::normalize($obj));
}
}

34
src/Worker/APDelivery.php Normal file
View file

@ -0,0 +1,34 @@
<?php
/**
* @file src/Worker/APDelivery.php
*/
namespace Friendica\Worker;
use Friendica\BaseObject;
use Friendica\Protocol\ActivityPub;
use Friendica\Model\Item;
use Friendica\Util\HTTPSignature;
class APDelivery extends BaseObject
{
public static function execute($cmd, $item_id, $inbox, $uid)
{
logger('Invoked: ' . $cmd . ': ' . $item_id . ' to ' . $inbox, LOGGER_DEBUG);
if ($cmd == Delivery::MAIL) {
} elseif ($cmd == Delivery::SUGGESTION) {
} elseif ($cmd == Delivery::RELOCATION) {
} elseif ($cmd == Delivery::REMOVAL) {
ActivityPub::transmitProfileDeletion($uid, $inbox);
} elseif ($cmd == Delivery::PROFILEUPDATE) {
ActivityPub::transmitProfileUpdate($uid, $inbox);
} else {
$data = ActivityPub::createActivityFromItem($item_id);
if (!empty($data)) {
HTTPSignature::transmit($data, $inbox, $uid);
}
}
return;
}
}

View file

@ -22,13 +22,14 @@ require_once 'include/items.php';
class Delivery extends BaseObject
{
const MAIL = 'mail';
const SUGGESTION = 'suggest';
const RELOCATION = 'relocate';
const DELETION = 'drop';
const POST = 'wall-new';
const COMMENT = 'comment-new';
const REMOVAL = 'removeme';
const MAIL = 'mail';
const SUGGESTION = 'suggest';
const RELOCATION = 'relocate';
const DELETION = 'drop';
const POST = 'wall-new';
const COMMENT = 'comment-new';
const REMOVAL = 'removeme';
const PROFILEUPDATE = 'profileupdate';
public static function execute($cmd, $item_id, $contact_id)
{

View file

@ -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;
@ -98,6 +99,14 @@ class Notifier
foreach ($r as $contact) {
Contact::terminateFriendship($user, $contact, true);
}
$inboxes = ActivityPub::fetchTargetInboxesforUser(0);
foreach ($inboxes as $inbox) {
logger('Account removal for user ' . $uid . ' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG);
Worker::add(['priority' => $a->queue['priority'], 'created' => $a->queue['created'], 'dont_fork' => true],
'APDelivery', Delivery::REMOVAL, '', $inbox, $uid);
}
return;
} elseif ($cmd == Delivery::RELOCATION) {
$normal_mode = false;
@ -413,6 +422,24 @@ class Notifier
}
}
$inboxes = [];
if ($target_item['origin']) {
$inboxes = ActivityPub::fetchTargetInboxes($target_item, $uid);
}
if ($parent['origin']) {
$parent_inboxes = ActivityPub::fetchTargetInboxes($parent, $uid);
$inboxes = array_merge($inboxes, $parent_inboxes);
}
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, $uid);
}
// 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) {

View file

@ -1,12 +1,15 @@
<?php
/**
* @file src/Worker/ProfileUpdate.php
* @brief Send updated profile data to Diaspora
* @brief Send updated profile data to Diaspora and ActivityPub
*/
namespace Friendica\Worker;
use Friendica\BaseObject;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\ActivityPub;
use Friendica\Core\Worker;
class ProfileUpdate {
public static function execute($uid = 0) {
@ -14,6 +17,16 @@ class ProfileUpdate {
return;
}
$a = BaseObject::getApp();
$inboxes = ActivityPub::fetchTargetInboxesforUser($uid);
foreach ($inboxes as $inbox) {
logger('Profile update for user ' . $uid . ' to ' . $inbox .' via ActivityPub', LOGGER_DEBUG);
Worker::add(['priority' => $a->queue['priority'], 'created' => $a->queue['created'], 'dont_fork' => true],
'APDelivery', Delivery::PROFILEUPDATE, '', $inbox, $uid);
}
Diaspora::sendProfile($uid);
}
}

View file

@ -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"]);
}

File diff suppressed because it is too large Load diff