From 9fac840f2c217296e5e2a097641534010b629f6f Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 12 Mar 2022 12:44:12 -0800 Subject: [PATCH 01/16] provide activitypub collection feeds for search results --- Code/Module/Search.php | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Code/Module/Search.php b/Code/Module/Search.php index 82f4c6ae4..ce9bef8c0 100644 --- a/Code/Module/Search.php +++ b/Code/Module/Search.php @@ -12,6 +12,8 @@ use Code\Daemon\Run; use Code\Lib\Channel; use Code\Lib\Navbar; use Code\Render\Theme; +use Code\Lib\LDSignatures; +use Code\Web\HTTPSig; require_once("include/bbcode.php"); @@ -52,7 +54,7 @@ class Search extends Controller } Navbar::set_selected('Search'); - $format = (($_REQUEST['format']) ? $_REQUEST['format'] : ''); + $format = (($_REQUEST['module_format']) ? $_REQUEST['module_format'] : ''); if ($format !== '') { $this->updating = $this->loading = 1; } @@ -66,15 +68,19 @@ class Search extends Controller if (x(App::$data, 'search')) { $search = trim(App::$data['search']); + $saved_id = 'search=' . urlencode($_GET['search']); } else { $search = ((x($_GET, 'search')) ? trim(escape_tags(rawurldecode($_GET['search']))) : ''); + $saved_id = 'search=' . urlencode($_GET['search']); } $tag = false; if (x($_GET, 'tag')) { $tag = true; $search = ((x($_GET, 'tag')) ? trim(escape_tags(rawurldecode($_GET['tag']))) : ''); + $saved_id = 'tag=' . urlencode($_GET['tag']); } + $static = ((array_key_exists('static', $_REQUEST)) ? intval($_REQUEST['static']) : 0); $o .= search($search, 'search-box', '/search', ((local_channel()) ? true : false)); @@ -335,16 +341,31 @@ class Search extends Controller $items = []; } - if ($format == 'json') { - $result = []; - require_once('include/conversation.php'); - foreach ($items as $item) { - $item['html'] = zidify_links(bbcode($item['body'])); - $x = encode_item($item); - $x['html'] = prepare_text($item['body'], $item['mimetype']); - $result[] = $x; + if ($format === 'json') { + + $i = Activity::encode_item_collection($items, 'search?' . $saved_id , 'OrderedCollection', true, count($items)); + + if ($i) { + + $chan = Channel::get_system(); + + $x = array_merge(['@context' => [ + ACTIVITYSTREAMS_JSONLD_REV, + 'https://w3id.org/security/v1', + Activity::ap_schema() + ]], $i); + + $headers = []; + $headers['Content-Type'] = 'application/x-nomad+json'; + $x['signature'] = LDSignatures::sign($x, $chan); + $ret = json_encode($x, JSON_UNESCAPED_SLASHES); + $headers['Digest'] = HTTPSig::generate_digest_header($ret); + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; + $h = HTTPSig::create_sig($headers, $chan['channel_prvkey'], Channel::url($chan)); + HTTPSig::set_headers($h); + echo $ret; + killme(); } - json_return_and_die(array('success' => true, 'messages' => $result)); } if ($tag) { From 9296aad67d4e287b79ca604b643fdef95e37a27f Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 12 Mar 2022 14:04:16 -0800 Subject: [PATCH 02/16] cleanup json-search feature --- Code/Module/Item.php | 3 ++- Code/Module/Search.php | 39 ++++++++++++++++++--------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Code/Module/Item.php b/Code/Module/Item.php index 9ccd8ac9e..56ffcbfc1 100644 --- a/Code/Module/Item.php +++ b/Code/Module/Item.php @@ -219,7 +219,8 @@ class Item extends Controller dbesc($r[0]['parent_mid']), dbesc($portable_id) ); - } elseif (Config::get('system', 'require_authenticated_fetch', false)) { + } + elseif (Config::get('system', 'require_authenticated_fetch', false)) { http_status_exit(403, 'Permission denied'); } diff --git a/Code/Module/Search.php b/Code/Module/Search.php index ce9bef8c0..65be3dc5d 100644 --- a/Code/Module/Search.php +++ b/Code/Module/Search.php @@ -343,29 +343,26 @@ class Search extends Controller if ($format === 'json') { - $i = Activity::encode_item_collection($items, 'search?' . $saved_id , 'OrderedCollection', true, count($items)); - - if ($i) { - - $chan = Channel::get_system(); + $chan = Channel::get_system(); - $x = array_merge(['@context' => [ - ACTIVITYSTREAMS_JSONLD_REV, - 'https://w3id.org/security/v1', - Activity::ap_schema() - ]], $i); + $i = Activity::encode_item_collection($items, 'search?' . $saved_id , 'OrderedCollection', true, count($items)); + + $x = array_merge(['@context' => [ + ACTIVITYSTREAMS_JSONLD_REV, + 'https://w3id.org/security/v1', + Activity::ap_schema() + ]], $i); - $headers = []; - $headers['Content-Type'] = 'application/x-nomad+json'; - $x['signature'] = LDSignatures::sign($x, $chan); - $ret = json_encode($x, JSON_UNESCAPED_SLASHES); - $headers['Digest'] = HTTPSig::generate_digest_header($ret); - $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; - $h = HTTPSig::create_sig($headers, $chan['channel_prvkey'], Channel::url($chan)); - HTTPSig::set_headers($h); - echo $ret; - killme(); - } + $headers = []; + $headers['Content-Type'] = 'application/x-nomad+json'; + $x['signature'] = LDSignatures::sign($x, $chan); + $ret = json_encode($x, JSON_UNESCAPED_SLASHES); + $headers['Digest'] = HTTPSig::generate_digest_header($ret); + $headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']) . ' ' . $_SERVER['REQUEST_URI']; + $h = HTTPSig::create_sig($headers, $chan['channel_prvkey'], Channel::url($chan)); + HTTPSig::set_headers($h); + echo $ret; + killme(); } if ($tag) { From 9548592761631c7cc5e04b28b87c9eff625cc70e Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 12 Mar 2022 17:50:30 -0800 Subject: [PATCH 03/16] regression in util/addons --- util/addons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/addons b/util/addons index 63f933218..d19618320 100755 --- a/util/addons +++ b/util/addons @@ -31,7 +31,7 @@ cli_startup(); list($tmp, $id) = array_map('trim', explode('/', $file)); $info = Addon::get_info($id); $enabled = in_array($id,$installed); - $x = check_plugin_versions($info); + $x = Addon::check_versions($info); if($enabled && ! $x) { $enabled = false; Addon::uninstall($id); From 1adad9d99912244a618b359ae87b33d742b83a4a Mon Sep 17 00:00:00 2001 From: nobody Date: Sat, 12 Mar 2022 22:29:12 -0800 Subject: [PATCH 04/16] missing DBA namespace --- Code/Extend/Hook.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Code/Extend/Hook.php b/Code/Extend/Hook.php index b0c79476c..c1d4ac8ff 100644 --- a/Code/Extend/Hook.php +++ b/Code/Extend/Hook.php @@ -3,6 +3,7 @@ namespace Code\Extend; use App; +use DBA; /** * @brief Hook class. From 9fcb326d78401a3785bff865672f3b4c545bd6ef Mon Sep 17 00:00:00 2001 From: nobody Date: Sun, 13 Mar 2022 12:58:26 -0700 Subject: [PATCH 05/16] change authentication plugin workflow so that username is not required --- include/auth.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/include/auth.php b/include/auth.php index 010c77641..ca0429050 100644 --- a/include/auth.php +++ b/include/auth.php @@ -43,15 +43,10 @@ function account_verify_password($login, $pass) { $ret = [ 'account' => null, 'channel' => null, 'xchan' => null ]; - $login = punify($login); $email_verify = get_config('system', 'verify_email'); $register_policy = get_config('system', 'register_policy'); - if (! $login) { - return null; - } - $account = null; $channel = null; $xchan = null; @@ -76,7 +71,13 @@ function account_verify_password($login, $pass) if (($addon_auth['authenticated']) && is_array($addon_auth['user_record']) && (! empty($addon_auth['user_record']))) { $ret['account'] = $addon_auth['user_record']; return $ret; - } else { + } + else { + if (! $login) { + logger('No login identity provided or authenticate addon failed.'); + return false; + } + $login = punify($login); if (! strpos($login, '@')) { $channel = Channel::from_username($login); if (! $channel) { From d4b0fd8f8111529e11cfe70d8748aded975a3c7d Mon Sep 17 00:00:00 2001 From: nobody Date: Mon, 14 Mar 2022 03:09:03 -0700 Subject: [PATCH 06/16] c2s --- Code/Lib/Activity.php | 17 ++-- Code/Lib/ActivityStreams.php | 4 +- Code/Module/Outbox.php | 150 ++++++++++++++++++++++++----------- 3 files changed, 117 insertions(+), 54 deletions(-) diff --git a/Code/Lib/Activity.php b/Code/Lib/Activity.php index d7f599499..b902d71c3 100644 --- a/Code/Lib/Activity.php +++ b/Code/Lib/Activity.php @@ -3882,13 +3882,16 @@ class Activity // We are the owner of this conversation, so send all received comments back downstream Run::Summon(['Notifier', 'comment-import', $x['item_id']]); } - $r = q( - "select * from item where id = %d limit 1", - intval($x['item_id']) - ); - if ($r) { - send_status_notifications($x['item_id'], $r[0]); - } + } + elseif ($act->client && $channel['channel_hash'] === $observer_hash) { + Run::Summon(['Notifier', 'wall-new', $x['item_id']]); + } + $r = q( + "select * from item where id = %d limit 1", + intval($x['item_id']) + ); + if ($r) { + send_status_notifications($x['item_id'], $r[0]); } sync_an_item($channel['channel_id'], $x['item_id']); } diff --git a/Code/Lib/ActivityStreams.php b/Code/Lib/ActivityStreams.php index b0f64ae6b..e85c274f6 100644 --- a/Code/Lib/ActivityStreams.php +++ b/Code/Lib/ActivityStreams.php @@ -16,6 +16,7 @@ class ActivityStreams public $data = null; public $meta = null; public $hub = null; + public $client = false; public $valid = false; public $deleted = false; public $id = ''; @@ -48,7 +49,8 @@ class ActivityStreams $this->raw = $string; $this->hub = $hub; - + $this->client = $client; + if (is_array($string)) { $this->data = $string; $this->raw = json_encode($string, JSON_UNESCAPED_SLASHES); diff --git a/Code/Module/Outbox.php b/Code/Module/Outbox.php index bc26f40ef..a120bfb01 100644 --- a/Code/Module/Outbox.php +++ b/Code/Module/Outbox.php @@ -11,20 +11,34 @@ use Code\Web\HTTPSig; use Code\Lib\Activity; use Code\Lib\ActivityPub; use Code\Lib\Config; +use Code\Lib\PConfig; use Code\Lib\Channel; +require_once('include/api_auth.php'); +require_once('include/api.php'); + /** * Implements an ActivityPub outbox. */ class Outbox extends Controller { + public function init() { + if (! api_user()) { + api_login(); + } + } + public function post() { if (argc() < 2) { killme(); } + if (! api_user()) { + killme(); + } + $channel = Channel::from_username(argv(1)); if (!$channel) { killme(); @@ -34,25 +48,20 @@ class Outbox extends Controller killme(); } - // At this point there is unlikely to be an authenticated observer using the C2S ActivityPub API. - // Mostly we're protecting the page from malicious mischief until the project's OAuth2 interface - // is linked to this page. - $observer = App::get_observer(); if (!$observer) { killme(); } - + if ($observer['xchan_hash'] !== $channel['channel_hash']) { if (!perm_is_allowed($channel['channel_id'], $observer['xchan_hash'], 'post_wall')) { logger('outbox post permission denied to ' . $observer['xchan_name']); killme(); } } -// disable C2S until we've fully tested it. - return; - + $observer_hash = get_observer_hash(); + $data = file_get_contents('php://input'); if (!$data) { return; @@ -60,7 +69,8 @@ class Outbox extends Controller logger('outbox_activity: ' . jindent($data), LOGGER_DATA); - $AS = new ActivityStreams($data); + // the third parameter signals to the parser that we are using C2S and that implied Create activities are supported + $AS = new ActivityStreams($data, null, true); if (!$AS->is_valid()) { return; @@ -70,41 +80,57 @@ class Outbox extends Controller return; } + // ensure the posted activity has required attributes + + $uuid = new_uuid(); + + if (! $AS->id) { + $AS->id = z_root() . '/activity/' . $uuid; + } + + if (isset($AS->obj) && (! isset($AS->obj['id']))) { + $AS->obj['id'] = z_root() . '/item/' . $uuid; + } + + if (! isset($AS->actor)) { + $AS->actor = Channel::url($channel); + } + logger('outbox_channel: ' . $channel['channel_address'], LOGGER_DEBUG); -// switch ($AS->type) { -// case 'Follow': -// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type']) && isset($AS->obj['id'])) { -// // do follow activity -// Activity::follow($channel,$AS); -// } -// break; -// case 'Invite': -// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') { -// // do follow activity -// Activity::follow($channel,$AS); -// } -// break; -// case 'Join': -// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') { -// // do follow activity -// Activity::follow($channel,$AS); -// } -// break; -// case 'Accept': -// // Activitypub for wordpress sends lowercase 'follow' on accept. -// // https://github.com/pfefferle/wordpress-activitypub/issues/97 -// // Mobilizon sends Accept/"Member" (not in vocabulary) in response to Join/Group -// if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && in_array($AS->obj['type'], ['Follow','follow', 'Member'])) { -// // do follow activity -// Activity::follow($channel,$AS); -// } -// break; -// case 'Reject': -// default: -// break; -// -// } + switch ($AS->type) { + case 'Follow': + if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type']) && isset($AS->obj['id'])) { + // do follow activity + Activity::follow($channel,$AS); + } + break; + case 'Invite': + if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') { + // do follow activity + Activity::follow($channel,$AS); + } + break; + case 'Join': + if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && $AS->obj['type'] === 'Group') { + // do follow activity + Activity::follow($channel,$AS); + } + break; + case 'Accept': + // Activitypub for wordpress sends lowercase 'follow' on accept. + // https://github.com/pfefferle/wordpress-activitypub/issues/97 + // Mobilizon sends Accept/"Member" (not in vocabulary) in response to Join/Group + if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && in_array($AS->obj['type'], ['Follow','follow', 'Member'])) { + // do follow activity + Activity::follow($channel,$AS); + } + break; + case 'Reject': + default: + break; + + } // These activities require permissions @@ -113,10 +139,7 @@ class Outbox extends Controller switch ($AS->type) { case 'Update': if (is_array($AS->obj) && array_key_exists('type', $AS->obj) && ActivityStreams::is_an_actor($AS->obj['type'])) { - // pretend this is an old cache entry to force an update of all the actor details - $AS->obj['cached'] = true; - $AS->obj['updated'] = datetime_convert('UTC', 'UTC', '1980-01-01', ATOM_TIME); - Activity::actor_store($AS->obj['id'], $AS->obj); + Activity::actor_store($AS->obj['id'], $AS->obj, true /* force cache refresh */); break; } case 'Accept': @@ -186,8 +209,43 @@ class Outbox extends Controller } if ($item) { + // fixup some of the item fields when using C2S + + if (! (isset($item['parent_mid']) && $item['parent_mid'])) { + $item['parent_mid'] = $item['mid']; + } + $item['item_private'] = ((in_array(ACTIVITY_PUBLIC_INBOX, $AS->recips) + || in_array('Public', $AS->recips) + || in_array('as:Public', $AS->recips)) + ? false + : true + ); + if ($AS->recips) { + foreach ($AS->recips as $recip) { + if (strpos($recip,'/lists/')) { + $r = q("select * from pgrp where hash = '%s' and uid = %d", + dbesc(basename($recip)), + intval($channel['channel_id']) + ); + if ($r) { + $item['allow_gid'] .= '<' . $r[0]['hash'] . '>'; + } + continue; + } + $r = q("select * from hubloc where hubloc_id_url = '%s'", + dbesc($recip) + ); + if ($r) { + $item['allow_cid'] .= '<' . $r[0]['hubloc_hash'] . '>'; + } + } + } + $item['item_wall'] = 1; + logger('parsed_item: ' . print_r($item, true), LOGGER_DATA); Activity::store($channel, $observer_hash, $AS, $item); + + } http_status_exit(200, 'OK'); From 09d4cca00da6088e76892e2331809dafedd796be Mon Sep 17 00:00:00 2001 From: nobody Date: Mon, 14 Mar 2022 15:42:42 -0700 Subject: [PATCH 07/16] hashtag issue --- include/text.php | 2 +- view/theme/redbasic/php/style.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/text.php b/include/text.php index 0fd06f5a8..8ec893048 100644 --- a/include/text.php +++ b/include/text.php @@ -948,7 +948,7 @@ function get_tags($s) // Pull out single word tags. These can be @nickname, @first_last // and #hash tags. - if (preg_match_all('/(? Date: Tue, 15 Mar 2022 01:42:34 -0700 Subject: [PATCH 08/16] php 8.1 changes --- Code/Lib/Account.php | 2 +- Code/Module/Lockview.php | 2 +- Code/Module/Outbox.php | 12 +++++++++++- Code/Module/Register.php | 2 +- include/api_auth.php | 2 +- include/auth.php | 4 ++-- include/security.php | 2 +- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Code/Lib/Account.php b/Code/Lib/Account.php index de415db26..97a83aa6c 100644 --- a/Code/Lib/Account.php +++ b/Code/Lib/Account.php @@ -625,7 +625,7 @@ class Account { Channel::auto_create($register[0]['uid']); } else { $_SESSION['login_return_url'] = 'new_channel'; - authenticate_success($account[0], null, true, true, false, true); + authenticate_success($account[0], false, true, true, false, true); } return true; diff --git a/Code/Module/Lockview.php b/Code/Module/Lockview.php index b53d810e6..47a9abb76 100644 --- a/Code/Module/Lockview.php +++ b/Code/Module/Lockview.php @@ -98,7 +98,7 @@ class Lockview extends Controller $l = array_merge($l, $recips['cc']); } for ($x = 0; $x < count($l); $x++) { - if ($l[$x] === ACTIVITY_PUBLIC_INBOX) { + if ($l[$x] === ACTIVITY_PUBLIC_INBOX || $l[$x] === 'Public' || $l[$x] === 'as:Public') { $l[$x] = '' . t('Everybody') . ''; } else { $l[$x] = '' . $l[$x] . ''; diff --git a/Code/Module/Outbox.php b/Code/Module/Outbox.php index a120bfb01..8010d6837 100644 --- a/Code/Module/Outbox.php +++ b/Code/Module/Outbox.php @@ -214,13 +214,15 @@ class Outbox extends Controller if (! (isset($item['parent_mid']) && $item['parent_mid'])) { $item['parent_mid'] = $item['mid']; } + // map ActivityPub recipients to Nomad ACLs to the extent possible. $item['item_private'] = ((in_array(ACTIVITY_PUBLIC_INBOX, $AS->recips) || in_array('Public', $AS->recips) || in_array('as:Public', $AS->recips)) ? false : true ); - if ($AS->recips) { + // The item ACL is only considered public if empty and item_private is 0. + if ($AS->recips && ! $item['item_private']) { foreach ($AS->recips as $recip) { if (strpos($recip,'/lists/')) { $r = q("select * from pgrp where hash = '%s' and uid = %d", @@ -232,6 +234,14 @@ class Outbox extends Controller } continue; } + if ($recip === z_root() . '/followers/' . $channel['channel_address']) { + // map to a virtual list/group even if the app isn't installed. This should do the right + // thing and create a followers-only post with the correct ACL as long as the public stream + // isn't addressed. And if it is, the post will still go to all your connections - so the ACL isn't + // necessary. + $item['allow_gid'] .= ''; + continue; + } $r = q("select * from hubloc where hubloc_id_url = '%s'", dbesc($recip) ); diff --git a/Code/Module/Register.php b/Code/Module/Register.php index fae8eea39..0c5287a0f 100644 --- a/Code/Module/Register.php +++ b/Code/Module/Register.php @@ -174,7 +174,7 @@ class Register extends Controller // fall through and authenticate if no approvals or verifications were required. - authenticate_success($result['account'], null, true, false, true); + authenticate_success($result['account'], false, true, false, true); $new_channel = false; $next_page = 'new_channel'; diff --git a/include/api_auth.php b/include/api_auth.php index 78b871d61..e56e3d938 100644 --- a/include/api_auth.php +++ b/include/api_auth.php @@ -59,7 +59,7 @@ function api_login() ); if ($x) { require_once('include/security.php'); - authenticate_success($x[0], null, true, false, true, true); + authenticate_success($x[0], false, true, false, true, true); $_SESSION['allow_api'] = true; Hook::call('logged_in', App::$user); return; diff --git a/include/auth.php b/include/auth.php index 010c77641..e711bda8c 100644 --- a/include/auth.php +++ b/include/auth.php @@ -230,7 +230,7 @@ if ( App::$session->new_cookie(60 * 60 * 24); // one day $_SESSION['last_login_date'] = datetime_convert(); unset($_SESSION['visitor_id']); // no longer a visitor - authenticate_success($x[0], null, true, true); + authenticate_success($x[0], false, true, true); } } if (array_key_exists('atoken', $_SESSION)) { @@ -279,7 +279,7 @@ if ( $login_refresh = true; } $ch = (($_SESSION['uid']) ? Channel::from_id($_SESSION['uid']) : null); - authenticate_success($r[0], null, $ch, false, false, $login_refresh); + authenticate_success($r[0], false, $ch, false, false, $login_refresh); } else { $_SESSION['account_id'] = 0; App::$session->nuke(); diff --git a/include/security.php b/include/security.php index ef060b85e..3a917f617 100644 --- a/include/security.php +++ b/include/security.php @@ -17,7 +17,7 @@ use Code\Extend\Hook; * @param bool $return * @param bool $update_lastlog */ -function authenticate_success($user_record, $channel = null, $login_initial = false, $interactive = false, $return = false, $update_lastlog = false) +function authenticate_success($user_record, $channel = false, $login_initial = false, $interactive = false, $return = false, $update_lastlog = false) { $_SESSION['addr'] = $_SERVER['REMOTE_ADDR']; From 5bcd59db4ad52c4528207fe11f2517b3a514ba69 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 01:45:05 -0700 Subject: [PATCH 09/16] revision --- version.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.php b/version.php index 93f244be5..3cb4f368c 100644 --- a/version.php +++ b/version.php @@ -1,2 +1,2 @@ Date: Tue, 15 Mar 2022 02:02:44 -0700 Subject: [PATCH 10/16] cleanup --- include/api_auth.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/api_auth.php b/include/api_auth.php index e56e3d938..5031ff476 100644 --- a/include/api_auth.php +++ b/include/api_auth.php @@ -14,7 +14,7 @@ require_once('include/security.php'); /** - * API Login via basic-auth or OAuth + * API Login via basic-auth, OpenWebAuth, or OAuth2 */ function api_login() @@ -58,7 +58,6 @@ function api_login() intval($record['channel_account_id']) ); if ($x) { - require_once('include/security.php'); authenticate_success($x[0], false, true, false, true, true); $_SESSION['allow_api'] = true; Hook::call('logged_in', App::$user); @@ -164,8 +163,8 @@ function api_login() function retry_basic_auth($method = 'Basic') { - header('WWW-Authenticate: ' . $method . ' realm="' . System::get_platform_name() . '"'); + header('WWW-Authenticate: ' . $method . ' realm="' . System::get_project_name() . '"'); header('HTTP/1.0 401 Unauthorized'); - echo( t('This api method requires authentication')); + echo( t('This API method requires authentication.')); killme(); } From 1bf3095d97c9fdc49b85568a47e7867ebb0b492c Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 12:38:53 -0700 Subject: [PATCH 11/16] C2S: set DM flag if message fits the characeristics of a DM. --- Code/Module/Outbox.php | 78 +++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/Code/Module/Outbox.php b/Code/Module/Outbox.php index 8010d6837..cdafe5c33 100644 --- a/Code/Module/Outbox.php +++ b/Code/Module/Outbox.php @@ -215,46 +215,62 @@ class Outbox extends Controller $item['parent_mid'] = $item['mid']; } // map ActivityPub recipients to Nomad ACLs to the extent possible. - $item['item_private'] = ((in_array(ACTIVITY_PUBLIC_INBOX, $AS->recips) - || in_array('Public', $AS->recips) - || in_array('as:Public', $AS->recips)) - ? false - : true - ); - // The item ACL is only considered public if empty and item_private is 0. - if ($AS->recips && ! $item['item_private']) { - foreach ($AS->recips as $recip) { - if (strpos($recip,'/lists/')) { - $r = q("select * from pgrp where hash = '%s' and uid = %d", - dbesc(basename($recip)), - intval($channel['channel_id']) + if (isset($AS->recips)) { + $item['item_private'] = ((in_array(ACTIVITY_PUBLIC_INBOX, $AS->recips) + || in_array('Public', $AS->recips) + || in_array('as:Public', $AS->recips)) + ? 0 + : 1 + ); + + if ($item['item_private']) { + foreach ($AS->recips as $recip) { + if (strpos($recip,'/lists/')) { + $r = q("select * from pgrp where hash = '%s' and uid = %d", + dbesc(basename($recip)), + intval($channel['channel_id']) + ); + if ($r) { + if (! isset($item['allow_gid'])) { + $item['allow_gid'] = EMPTY_STR; + } + $item['allow_gid'] .= '<' . $r[0]['hash'] . '>'; + } + continue; + } + if ($recip === z_root() . '/followers/' . $channel['channel_address']) { + // map to a virtual list/group even if the app isn't installed. This should do the right + // thing and create a followers-only post with the correct ACL as long as the public stream + // isn't addressed. And if it is, the post will still go to all your connections - so the ACL isn't + // necessary. + if (! isset($item['allow_gid'])) { + $item['allow_gid'] = EMPTY_STR; + } + $item['allow_gid'] .= ''; + continue; + } + $r = q("select * from hubloc where hubloc_id_url = '%s'", + dbesc($recip) ); if ($r) { - $item['allow_gid'] .= '<' . $r[0]['hash'] . '>'; + if (! isset($item['allow_cid'])) { + $item['allow_cid'] = EMPTY_STR; + } + $item['allow_cid'] .= '<' . $r[0]['hubloc_hash'] . '>'; } - continue; - } - if ($recip === z_root() . '/followers/' . $channel['channel_address']) { - // map to a virtual list/group even if the app isn't installed. This should do the right - // thing and create a followers-only post with the correct ACL as long as the public stream - // isn't addressed. And if it is, the post will still go to all your connections - so the ACL isn't - // necessary. - $item['allow_gid'] .= ''; - continue; - } - $r = q("select * from hubloc where hubloc_id_url = '%s'", - dbesc($recip) - ); - if ($r) { - $item['allow_cid'] .= '<' . $r[0]['hubloc_hash'] . '>'; } } - } + // set the DM flag if needed + if ($item['item_private'] && isset($item['allow_cid']) && ! isset($item['allow_gid']) + && in_array(substr_count($item['allow_cid'],'<'), [ 1, 2 ])) { + $item['item_private'] = 2; + } + } + $item['item_wall'] = 1; logger('parsed_item: ' . print_r($item, true), LOGGER_DATA); Activity::store($channel, $observer_hash, $AS, $item); - } From d88245e08823036635674101c6ea9321d4c38a56 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 15:15:39 -0700 Subject: [PATCH 12/16] federation issue --- Code/Lib/Activity.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Code/Lib/Activity.php b/Code/Lib/Activity.php index b902d71c3..df86b00c0 100644 --- a/Code/Lib/Activity.php +++ b/Code/Lib/Activity.php @@ -778,13 +778,14 @@ class Activity if (!in_array($ret['type'], ['Create', 'Update', 'Accept', 'Reject', 'TentativeAccept', 'TentativeReject'])) { $ret['inReplyTo'] = $i['thr_parent']; - $cnv = get_iconfig($i['parent'], 'activitypub', 'context'); - if (!$cnv) { - $cnv = get_iconfig($i['parent'], 'ostatus', 'conversation'); - } - if (!$cnv) { - $cnv = $ret['parent_mid']; - } + } + + $cnv = get_iconfig($i['parent'], 'activitypub', 'context'); + if (!$cnv) { + $cnv = get_iconfig($i['parent'], 'ostatus', 'conversation'); + } + if (!$cnv) { + $cnv = $ret['parent_mid']; } } From aeb06b8576363d28e725e2374ce6a8853164a136 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 17:06:52 -0700 Subject: [PATCH 13/16] issues with replyto occasionally getting set incorrectly. --- Code/Lib/Activity.php | 21 ++++++++++++++------- Code/Module/Item.php | 29 +++++++++++++++++++---------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/Code/Lib/Activity.php b/Code/Lib/Activity.php index df86b00c0..655399041 100644 --- a/Code/Lib/Activity.php +++ b/Code/Lib/Activity.php @@ -3818,17 +3818,24 @@ class Activity $item['allow_gid'] = $item['deny_cid'] = $item['deny_gid'] = ''; } } - - // Private conversation, but this comment went rogue and was published publicly - // Set item_restrict to indicate this condition so we can flag it in the UI - - if (intval($parent[0]['item_private']) !== 0 && $act->recips && (in_array(ACTIVITY_PUBLIC_INBOX, $act->recips) || in_array('Public', $act->recips) || in_array('as:Public', $act->recips))) { - $item['item_restrict'] = $item['item_restrict'] | 2; - } } self::rewrite_mentions($item); + if (! isset($item['replyto'])) { + if (strpos($item['owner_xchan'],'http') === 0) { + $item['replyto'] = $item['owner_xchan']; + } + else { + $r = q("select hubloc_id_url from hubloc where hubloc_hash = '%s' and hubloc_primary = 1", + dbesc($item['owner_xchan']) + ); + if ($r) { + $item['replyto'] = $r[0]['hubloc_id_url']; + } + } + } + $r = q( "select id, created, edited from item where mid = '%s' and uid = %d limit 1", dbesc($item['mid']), diff --git a/Code/Module/Item.php b/Code/Module/Item.php index 9ccd8ac9e..453c80ef7 100644 --- a/Code/Module/Item.php +++ b/Code/Module/Item.php @@ -738,7 +738,7 @@ class Item extends Controller dbesc($channel['channel_hash']) ); if ($r && count($r)) { - $owner_xchan = $r[0]; + $owner_xchan = array_shift($r); } else { logger("mod_item: no owner."); if ($api_source) { @@ -775,15 +775,6 @@ class Item extends Controller } } - if (!isset($replyto)) { - if (strpos($owner_xchan['xchan_hash'], 'http') === 0) { - $replyto = $owner_xchan['xchan_hash']; - } else { - $replyto = $owner_xchan['xchan_url']; - } - } - - $acl = new AccessControl($channel); $view_policy = PermissionLimits::Get($channel['channel_id'], 'view_stream'); @@ -1396,6 +1387,24 @@ class Item extends Controller $datarray['obj']['id'] = $mid; } + + if (! (isset($replyto) && $replyto)) { + if ($owner_hash && strpos($owner_hash,'http') === 0) { + $replyto = $owner_hash; + } + else { + $tmp = $owner_hash ? $owner_hash : $owner_xchan['xchan_hash']; + if ($tmp) { + $r = q("select hubloc_id_url from hubloc where hubloc_hash = '%s' and hubloc_primary = 1", + dbesc($tmp) + ); + if ($r) { + $replyto = $r[0]['hubloc_id_url']; + } + } + } + } + if ($private && !$parent) { if ( intval($private) === 1 && (!$str_group_allow) && in_array(substr_count($str_contact_allow,'<'), [ 1, 2 ])) { $private = 2; From 07e0a99e904869728a3f9f8eceb367308be34f40 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 17:37:50 -0700 Subject: [PATCH 14/16] update federation docs --- FEDERATION.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FEDERATION.md b/FEDERATION.md index 9cf2ac39a..d5dbb241c 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -3,6 +3,9 @@ Federation The ActivityPub implementation in this project strives to be compliant to the core spec where possible, while offering a range of services and features which normally aren't provided by ActivityPub projects. +C2S + +This project supports ActivityPub C2S. You may authenticate with HTTP basic-auth, OAuth2, or OpenWebAuth. There is no media upload endpoint since the (deprecated) specification of that service has no workarounds for working in memory-restricted environments and most mobile phone photos exceed PHP's default upload size limits. Direct Messages From 1abe136724355285cab8ee3361a5b4761b6accf9 Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 22:49:19 -0700 Subject: [PATCH 15/16] federation update --- FEDERATION.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/FEDERATION.md b/FEDERATION.md index d5dbb241c..6ce5eb654 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -7,6 +7,15 @@ C2S This project supports ActivityPub C2S. You may authenticate with HTTP basic-auth, OAuth2, or OpenWebAuth. There is no media upload endpoint since the (deprecated) specification of that service has no workarounds for working in memory-restricted environments and most mobile phone photos exceed PHP's default upload size limits. +Client search interface + +If public access is allowed to the content search interface (a site security setting), clients may search the content of public messages or tags and are returned an ActivityStreams Collection of search results. When authenticated via OpenWebAuth, the search results may contain their own content or private content which they are permitted to access. + +The URL endpoints are: + + https://example.com/search?search=banana + https://example.com/search?tag=banana + Direct Messages Direct Messages (DM) are differentiated from other private messaging using the zot:directMessage flag (boolean). This is compatible with the same facility provided by other projects in other namespaces and is not prefixed within activities so that these may potentially be aggregated. From 467a2299eeda854e8afe495e7d432efd31a6417a Mon Sep 17 00:00:00 2001 From: nobody Date: Tue, 15 Mar 2022 22:57:30 -0700 Subject: [PATCH 16/16] don't call "special" search routines if ActivityPub output is requested --- Code/Module/Search.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Code/Module/Search.php b/Code/Module/Search.php index 65be3dc5d..c598d3ef0 100644 --- a/Code/Module/Search.php +++ b/Code/Module/Search.php @@ -205,21 +205,21 @@ class Search extends Controller $tag = true; $search = substr($search, 1); } - if (strpos($search, '@') === 0) { + if (strpos($search, '@') === 0 && $format === '') { $search = substr($search, 1); goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search); } - if (strpos($search, '!') === 0) { + if (strpos($search, '!') === 0 && $format === '') { $search = substr($search, 1); goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search); } - if (strpos($search, '?') === 0) { + if (strpos($search, '?') === 0 && $format === '') { $search = substr($search, 1); goaway(z_root() . '/help' . '?f=1&navsearch=1&search=' . $search); } // look for a naked webbie - if (strpos($search, '@') !== false && strpos($search, 'http') !== 0) { + if (strpos($search, '@') !== false && strpos($search, 'http') !== 0 && $format === '') { goaway(z_root() . '/directory' . '?f=1&navsearch=1&search=' . $search); }