diff options
Diffstat (limited to 'Zotlabs/Lib/Libzot.php')
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 362 |
1 files changed, 184 insertions, 178 deletions
diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 3f8685397..3495ede06 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -2,6 +2,7 @@ namespace Zotlabs\Lib; +use App; use Zotlabs\Web\HTTPSig; use Zotlabs\Access\Permissions; use Zotlabs\Access\PermissionLimits; @@ -759,12 +760,13 @@ class Libzot { || ($r[0]['xchan_connurl'] != $arr['primary_location']['connections_url']) || ($r[0]['xchan_addr'] != $arr['primary_location']['address']) || ($r[0]['xchan_follow'] != $arr['primary_location']['follow_url']) + || (isset($arr['ed25519_key']) && $r[0]['xchan_epubkey'] != $arr['ed25519_key']) || ($r[0]['xchan_connpage'] != $arr['connect_url']) || ($r[0]['xchan_url'] != $arr['primary_location']['url']) || $hidden_changed || $adult_changed || $deleted_changed || $pubforum_changed) { $rup = q("update xchan set xchan_name = '%s', xchan_name_date = '%s', xchan_connurl = '%s', xchan_follow = '%s', xchan_connpage = '%s', xchan_hidden = %d, xchan_selfcensored = %d, xchan_deleted = %d, xchan_pubforum = %d, - xchan_addr = '%s', xchan_url = '%s' where xchan_hash = '%s'", + xchan_addr = '%s', xchan_url = '%s', xchan_epubkey = '%s' where xchan_hash = '%s'", dbesc(($arr['name']) ? escape_tags($arr['name']) : '-'), dbesc($arr['name_updated']), dbesc($arr['primary_location']['connections_url']), @@ -776,6 +778,7 @@ class Libzot { intval($arr['public_forum']), dbesc(escape_tags($arr['primary_location']['address'])), dbesc(escape_tags($arr['primary_location']['url'])), + dbesc($arr['ed25519_key'] ?? ''), dbesc($xchan_hash) ); @@ -799,6 +802,7 @@ class Libzot { 'xchan_guid' => $arr['id'], 'xchan_guid_sig' => $arr['id_sig'], 'xchan_pubkey' => $arr['public_key'], + 'xchan_epubkey' => $arr['xchan_epubkey'] ?? '', 'xchan_photo_mimetype' => $arr['photo']['type'], 'xchan_photo_l' => $arr['photo']['url'], 'xchan_addr' => escape_tags($arr['primary_location']['address']), @@ -1139,7 +1143,6 @@ class Libzot { if ($env['encoding'] === 'activitystreams') { $AS = new ActivityStreams($data); - if (!$AS->is_valid()) { logger('Activity rejected: ' . print_r($data, true)); return; @@ -1154,7 +1157,6 @@ class Libzot { else { $item = []; } - logger($AS->debug(), LOGGER_DATA); } @@ -1201,7 +1203,6 @@ class Libzot { // @fixme; $deliveries = self::public_recips($env, $AS); - } $deliveries = array_unique($deliveries); @@ -1222,10 +1223,6 @@ class Libzot { $author_url = $AS->actor['id']; - if ($AS->type === 'Announce') { - $author_url = Activity::get_attributed_to_actor_url($AS); - } - $r = Activity::get_actor_hublocs($author_url); if (!$r) { @@ -1286,7 +1283,7 @@ class Libzot { } } - if (isset($AS->meta['hubloc']) && $AS->meta['hubloc']) { + if (!empty($AS->meta['hubloc']) || $AS->sigok) { $item['item_verified'] = true; } @@ -1304,6 +1301,8 @@ class Libzot { $relay = (($env['type'] === 'response') ? true : false); $result = self::process_delivery($env['sender'], $AS, $item, $deliveries, $relay, false, $message_request); + + Activity::init_background_fetch($env['sender']); } elseif ($env['type'] === 'sync') { // $item = get_channelsync_elements($data); @@ -1324,6 +1323,7 @@ class Libzot { if ($result) { $return = array_merge($return, $result); } + return $return; } @@ -1366,11 +1366,13 @@ class Libzot { static function find_parent_owner_hashes($env, $act) { $r = []; - $thread_parent = self::find_parent($env, $act); - if ($thread_parent) { - $uids = q("SELECT uid FROM item WHERE thr_parent = '%s' OR parent_mid = '%s'", - dbesc($thread_parent), - dbesc($thread_parent) + $parent = self::find_parent($env, $act); + + if ($parent) { + $uids = q("SELECT uid FROM item WHERE thr_parent = '%s' OR parent_mid = '%s' OR mid = '%s'", + dbesc($parent), + dbesc($parent), + dbesc($parent) ); if ($uids) { @@ -1530,7 +1532,7 @@ class Libzot { $local_public = $public; $item_result = null; - $DR = new DReport(z_root(), $sender, $d, $arr['mid']); + $DR = new DReport(z_root(), $sender, $d, $arr['mid'], $arr['uuid']); $channel = channelx_by_hash($d); @@ -1581,6 +1583,39 @@ class Libzot { continue; } + $arr['item_wall'] = 0; + + // This is our own post, possibly coming from a channel clone + if ($arr['owner_xchan'] === $d) { + $arr['item_wall'] = 1; + } + + if (isset($arr['item_deleted']) && $arr['item_deleted']) { + + // remove_community_tag is a no-op if this isn't a community tag activity + // self::remove_community_tag($sender, $arr, $channel['channel_id']); + + // set these just in case we need to store a fresh copy of the deleted post. + // This could happen if the delete got here before the original post did. + + $arr['aid'] = $channel['channel_account_id']; + $arr['uid'] = $channel['channel_id']; + + $item_id = self::delete_imported_item($sender, $act, $arr, $channel['channel_id'], $relay); + $DR->update(($item_id) ? 'deleted' : 'delete_failed'); + $result[] = $DR->get(); + + if ($relay && $item_id) { + logger('process_delivery: invoking relay'); + Master::Summon(['Notifier', 'relay', intval($item_id), 'delete']); + $DR->update('relayed'); + $result[] = $DR->get(); + } + + continue; + } + + // allow public postings to the sys channel regardless of permissions, but not // for comments travelling upstream. Wait and catch them on the way down. // They may have been blocked by the owner. @@ -1607,116 +1642,23 @@ class Libzot { } $tag_delivery = tgroup_check($channel['channel_id'], $arr); - $perm = 'send_stream'; - if (($arr['mid'] !== $arr['parent_mid']) && ($relay)) - $perm = 'post_comments'; - - // This is our own post, possibly coming from a channel clone - if ($arr['owner_xchan'] == $d) { - $arr['item_wall'] = 1; - } - else { - $arr['item_wall'] = 0; - } - - $friendofriend = false; - - if ((!$tag_delivery) && (!$local_public)) { - $allowed = (perm_is_allowed($channel['channel_id'], $sender, $perm)); - - $permit_mentions = intval(PConfig::Get($channel['channel_id'], 'system', 'permit_all_mentions') && i_am_mentioned($channel, $arr)); - - if (!$allowed) { - if ($perm === 'post_comments') { - $parent = q("select * from item where mid = '%s' and uid = %d limit 1", - dbesc($arr['parent_mid']), - intval($channel['channel_id']) - ); - if ($parent) { - $allowed = can_comment_on_post($sender, $parent[0]); - if (!$allowed && $permit_mentions) { - $allowed = true; - } - - if (!$allowed) { - if (PConfig::Get($channel['channel_id'], 'system', 'moderate_unsolicited_comments') && $arr['obj_type'] !== 'Answer') { - $arr['item_blocked'] = ITEM_MODERATED; - $allowed = true; - } - } - } - - } elseif ($permit_mentions) { - $allowed = true; - } - } - - if ($request) { - // Conversation fetches (e.g. $request == true) take place for - // a) new comments on expired posts - // b) hyperdrive (friend-of-friend) conversations - // c) Repeats of posts by others - - - // over-ride normal connection permissions for hyperdrive (friend-of-friend) conversations - // (if hyperdrive is enabled) and repeated posts by a friend. - // If $allowed is already true, this is probably the conversation of a direct friend or a - // conversation fetch for a new comment on an expired post - // Comments of all these activities are allowed and will only be rejected (later) if the parent - // doesn't exist. - - if ($perm === 'send_stream') { - if ($force || get_pconfig($channel['channel_id'], 'system', 'hyperdrive', false)) { - $allowed = true; - } - } - else { - $allowed = true; - } - - $friendofriend = true; - } - - if (intval($arr['item_private']) === 2) { - if (!perm_is_allowed($channel['channel_id'], $sender, 'post_mail')) { - $allowed = false; - } - } - - if (!$allowed) { - logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}"); - $DR->update('permission denied'); - $result[] = $DR->get(); - continue; - } - } - - // logger('item: ' . print_r($arr,true), LOGGER_DATA); + $perm = 'send_stream'; if ($arr['mid'] !== $arr['parent_mid']) { - logger('checking source: "' . $arr['mid'] . '" != "' . $arr['parent_mid'] . '"'); - - // check source route. - // We are only going to accept comments from this sender if the comment has the same route as the top-level-post, - // this is so that permissions mismatches between senders apply to the entire conversation - // As a side effect we will also do a preliminary check that we have the top-level-post, otherwise - // processing it is pointless. + if ($relay) + $perm = 'post_comments'; - $r = q("select route, id, parent_mid, mid, owner_xchan, item_private, obj_type from item where mid = '%s' and uid = %d limit 1", + $parent = q("select * from item where mid = '%s' and uid = %d limit 1", dbesc($arr['parent_mid']), intval($channel['channel_id']) ); - if (!$r) { + if (!$parent) { $DR->update('comment parent not found'); $result[] = $DR->get(); - if ($relay || $request || $local_public) { - continue; - } - // We don't seem to have a copy of this conversation or at least the parent // - so request a copy of the entire conversation to date. // Don't do this if it's a relay post as we're the ones who are supposed to @@ -1728,24 +1670,40 @@ class Libzot { // the top level post is unlikely to be imported and // this is just an exercise in futility. - if (perm_is_allowed($channel['channel_id'], $sender, 'send_stream')) { - Master::Summon(['Zotconvo', $channel['channel_id'], $arr['parent_mid']]); + if ($relay || $request || (!$local_public && !perm_is_allowed($channel['channel_id'], $sender, 'send_stream'))) { + continue; + } + + if ($arr['verb'] === 'Announce') { + App::$cache['as_fetch_objects'][$arr['mid']]['channels'][] = $channel['channel_id']; + App::$cache['as_fetch_objects'][$arr['mid']]['force'] = true; + } + else { + App::$cache['zot_fetch_objects'][$arr['mid']]['channels'][] = $channel['channel_id']; + App::$cache['zot_fetch_objects'][$arr['mid']]['force'] = false; } continue; } - if ($r[0]['obj_type'] === 'Question') { + logger('checking source: "' . $arr['mid'] . '" != "' . $arr['parent_mid'] . '"'); + + // check source route. + // We are only going to accept comments from this sender if the comment has the same route as the top-level-post, + // this is so that permissions mismatches between senders apply to the entire conversation + // As a side effect we will also do a preliminary check that we have the top-level-post, otherwise + // processing it is pointless. + + if ($parent[0]['obj_type'] === 'Question') { // route checking doesn't work correctly here because we've changed the privacy - $r[0]['route'] = EMPTY_STR; + $parent[0]['route'] = EMPTY_STR; // If this is a poll response, convert the obj_type to our (internal-only) "Answer" type - if ($arr['obj_type'] === ACTIVITY_OBJ_COMMENT && $arr['title'] && (!$arr['body'])) { + if (in_array($arr['obj_type'], ['Note', ACTIVITY_OBJ_COMMENT]) && $arr['title'] && (!$arr['body'])) { $arr['obj_type'] = 'Answer'; } } - - if ($relay || $friendofriend || (intval($r[0]['item_private']) === 0 && intval($arr['item_private']) === 0)) { + if ($relay || (intval($parent[0]['item_private']) === 0 && intval($arr['item_private']) === 0)) { // reset the route in case it travelled a great distance upstream // use our parent's route so when we go back downstream we'll match // with whatever route our parent has. @@ -1753,8 +1711,8 @@ class Libzot { // but we are now getting comments via listener delivery // and if there is no privacy on this or the parent, we don't care about the route, // so just set the owner and route accordingly. - $arr['route'] = $r[0]['route']; - $arr['owner_xchan'] = $r[0]['owner_xchan']; + $arr['route'] = $parent[0]['route']; + $arr['owner_xchan'] = $parent[0]['owner_xchan']; } else { @@ -1764,7 +1722,7 @@ class Libzot { // only compare the last hop since it could have arrived at the last location any number of ways. // Always accept empty routes and firehose items (route contains 'undefined') . - $existing_route = explode(',', $r[0]['route']); + $existing_route = explode(',', $parent[0]['route']); $routes = count($existing_route); if ($routes) { $last_hop = array_pop($existing_route); @@ -1781,8 +1739,8 @@ class Libzot { $current_route = ((isset($arr['route']) && $arr['route']) ? $arr['route'] . ',' : '') . $sender; if ($last_hop && $last_hop != $sender) { - logger('comment route mismatch: parent route = ' . $r[0]['route'] . ' expected = ' . $current_route, LOGGER_DEBUG); - logger('comment route mismatch: parent msg = ' . $r[0]['id'], LOGGER_DEBUG); + logger('comment route mismatch: parent route = ' . $parent[0]['route'] . ' expected = ' . $current_route, LOGGER_DEBUG); + logger('comment route mismatch: parent msg = ' . $parent[0]['id'], LOGGER_DEBUG); $DR->update('comment route mismatch'); $result[] = $DR->get(); continue; @@ -1795,42 +1753,80 @@ class Libzot { } } - // This is used to fetch allow/deny rules if either the sender - // or owner is a connection. post_is_importable() evaluates all of them - $abook = q("select * from abook where abook_channel = %d and ( abook_xchan = '%s' OR abook_xchan = '%s' )", - intval($channel['channel_id']), - dbesc($arr['owner_xchan']), - dbesc($arr['author_xchan']) - ); + if (!$tag_delivery && !$local_public) { + $allowed = perm_is_allowed($channel['channel_id'], $sender, $perm); - if (isset($arr['item_deleted']) && $arr['item_deleted']) { + $permit_mentions = intval(PConfig::Get($channel['channel_id'], 'system', 'permit_all_mentions') && i_am_mentioned($channel, $arr)); - // remove_community_tag is a no-op if this isn't a community tag activity - self::remove_community_tag($sender, $arr, $channel['channel_id']); + if (!$allowed) { + if ($parent && $perm === 'send_stream') { + // if we own the parent we will accept its comments + $allowed = true; + } - // set these just in case we need to store a fresh copy of the deleted post. - // This could happen if the delete got here before the original post did. + elseif ($parent && $perm === 'post_comments') { + $allowed = can_comment_on_post($sender, $parent[0]); - $arr['aid'] = $channel['channel_account_id']; - $arr['uid'] = $channel['channel_id']; + if (!$allowed && $permit_mentions) { + $allowed = true; + } - $item_id = self::delete_imported_item($sender, $act, $arr, $channel['channel_id'], $relay); - $DR->update(($item_id) ? 'deleted' : 'delete_failed'); - $result[] = $DR->get(); + if (!$allowed) { + if (PConfig::Get($channel['channel_id'], 'system', 'moderate_unsolicited_comments') && $arr['obj_type'] !== 'Answer') { + $arr['item_blocked'] = ITEM_MODERATED; + $allowed = true; + } + } - if ($relay && $item_id) { - logger('process_delivery: invoking relay'); - Master::Summon(['Notifier', 'relay', intval($item_id)]); - $DR->update('relayed'); - $result[] = $DR->get(); + } + elseif ($permit_mentions) { + $allowed = true; + } } - continue; + if ($request) { + // Conversation fetches (e.g. $request == true) take place for + // a) new comments on expired posts + // b) manual import of posts via search (in this case force will be true) + // c) import of conversations from friends of friends (they can currently arriuve from streams if a channel is configured to do so) + + // Comments of all these activities are allowed and will only be rejected (later) if the parent + // doesn't exist. + + if ($perm === 'send_stream') { + if ($force) { + $allowed = true; + } + } + else { + $allowed = true; + } + } + + if (intval($arr['item_private']) === 2) { + if (!perm_is_allowed($channel['channel_id'], $sender, 'post_mail')) { + $allowed = false; + } + } + + if (!$allowed) { + logger("permission denied for delivery to channel {$channel['channel_id']} {$channel['channel_address']}"); + $DR->update('permission denied'); + $result[] = $DR->get(); + continue; + } } + // This is used to fetch allow/deny rules if either the sender + // or owner is a connection. post_is_importable() evaluates all of them + $abook = q("select * from abook where abook_channel = %d and ( abook_xchan = '%s' OR abook_xchan = '%s' )", + intval($channel['channel_id']), + dbesc($arr['owner_xchan']), + dbesc($arr['author_xchan']) + ); + // reactions such as like and dislike could have an mid with /activity/ in it. // Check for both forms in order to prevent duplicates. - $r = q("select * from item where mid in ('%s','%s') and uid = %d limit 1", dbesc($arr['mid']), dbesc(str_replace(z_root() . '/activity/', z_root() . '/item/', $arr['mid'])), @@ -1838,14 +1834,6 @@ class Libzot { ); if ($r) { - // We already have this post. - // Dismiss its announce - if ($act->type === 'Announce') { - $DR->update('update ignored'); - $result[] = $DR->get(); - continue; - } - $item_id = $r[0]['id']; if (intval($r[0]['item_deleted'])) { @@ -1902,12 +1890,12 @@ class Libzot { $maxlen = get_max_import_size(); - if ($maxlen && mb_strlen($arr['body']) > $maxlen) { + if ($maxlen && isset($arr['body']) && mb_strlen($arr['body']) > $maxlen) { $arr['body'] = mb_substr($arr['body'], 0, $maxlen, 'UTF-8'); logger('message length exceeds max_import_size: truncated'); } - if ($maxlen && mb_strlen($arr['summary']) > $maxlen) { + if ($maxlen && isset($arr['summary']) && mb_strlen($arr['summary']) > $maxlen) { $arr['summary'] = mb_substr($arr['summary'], 0, $maxlen, 'UTF-8'); logger('message summary length exceeds max_import_size: truncated'); } @@ -1915,7 +1903,6 @@ class Libzot { if (post_is_importable($arr['uid'], $arr, $abook)) { $item_result = item_store($arr); if ($item_result['success']) { - $item_id = $item_result['item_id']; if ($item_source && in_array($item_result['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { @@ -1957,6 +1944,7 @@ class Libzot { // preserve conversations with which you are involved from expiration $stored = ((isset($item_result['item'])) ? $item_result['item'] : false); + if ((is_array($stored)) && ($stored['id'] != $stored['parent']) && ($stored['author_xchan'] === $channel['channel_hash'])) { retain_item($stored['item']['parent']); @@ -2005,11 +1993,14 @@ class Libzot { $ret = []; - $signer = q("select hubloc_hash, hubloc_url from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' order by hubloc_id desc limit 1", dbesc($a['signature']['signer']) ); + $signer_hash = $signer[0]['hubloc_hash'] ?? $a['signature']['signer']; + $conv_owner = $signer_hash; + + $i = 0; foreach ($items as $activity) { @@ -2023,14 +2014,14 @@ class Libzot { } if (!$AS->is_valid()) { - logger('FOF Activity rejected: ' . print_r($activity, true)); + logger('Fetched activity rejected: ' . print_r($activity, true)); continue; } // logger($AS->debug()); if(empty($AS->actor['id'])) { - logger('No actor id: ' . print_r($AS, true)); + logger('Fetched activity no actor id: ' . print_r($AS, true)); continue; } @@ -2043,7 +2034,7 @@ class Libzot { $r = self::zot_record_preferred($r); } if (!$r) { - logger('FOF Activity: no actor'); + logger('Fetched activity: no actor'); continue; } } @@ -2058,7 +2049,7 @@ class Libzot { $ro = self::zot_record_preferred($ro); } if (!$ro) { - logger('FOF Activity: no obj actor'); + logger('Fetched activity: no obj actor'); continue; } } @@ -2073,14 +2064,18 @@ class Libzot { $arr['author_xchan'] = $r['hubloc_hash']; - if ($signer) { - $arr['owner_xchan'] = $signer[0]['hubloc_hash']; - } - else { - $arr['owner_xchan'] = $a['signature']['signer']; + if ($i === 0) { + // Set the author of the toplevel post as conv_owner + $conv_owner = $r['hubloc_hash']; } - if (isset($AS->meta['hubloc']) || $arr['author_xchan'] === $arr['owner_xchan']) { + $arr['owner_xchan'] = $conv_owner; + $arr['source_xchan'] = $signer_hash; + + // WARNING: the presence of both source_xchan and non-zero item_uplink here will cause a delivery loop + $arr['item_uplink'] = 0; + + if (!empty($AS->meta['hubloc']) || $arr['author_xchan'] === $arr['owner_xchan'] || $AS->sigok) { $arr['item_verified'] = true; } @@ -2092,13 +2087,15 @@ class Libzot { } } - logger('FOF Activity received: ' . print_r($arr, true), LOGGER_DATA, LOG_DEBUG); - logger('FOF Activity recipient: ' . $channel['channel_hash'], LOGGER_DATA, LOG_DEBUG); + logger('Fetched activity received: ' . print_r($arr, true), LOGGER_DATA, LOG_DEBUG); + logger('Fetched activity recipient: ' . $channel['channel_hash'], LOGGER_DATA, LOG_DEBUG); $result = self::process_delivery($arr['owner_xchan'], $AS, $arr, [$channel['channel_hash']], false, false, true, $force); if ($result) { $ret = array_merge($ret, $result); } + + $i++; } return $ret; @@ -2320,12 +2317,20 @@ class Libzot { // this information from the metadata should have no other discernible impact. if (($stored['id'] != $stored['parent']) && intval($stored['item_origin'])) { - q("update item set item_origin = 0 where id = %d and uid = %d", - intval($stored['id']), - intval($stored['uid']) + q("update item set item_origin = 0 where id = %d", + intval($stored['id']) ); } - } + } else { + if ($stored['id'] !== $stored['parent']) { + q( + "update item set commented = '%s', changed = '%s' where id = %d", + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($stored['parent']) + ); + } + } // Use phased deletion to set the deleted flag, call both tag_deliver and the notifier to notify downstream channels @@ -2814,6 +2819,7 @@ class Libzot { ]; $ret['public_key'] = $e['channel_pubkey']; + $ret['ed25519_key'] = $e['xchan_epubkey']; $ret['signing_algorithm'] = 'rsa-sha256'; $ret['username'] = $e['channel_address']; $ret['name'] = $e['channel_name']; @@ -2979,7 +2985,7 @@ class Libzot { $ret['site']['admin'] = get_config('system', 'admin_email'); $visible_plugins = []; - if (is_array(\App::$plugins) && count(\App::$plugins)) { + if (is_array(App::$plugins) && count(App::$plugins)) { $r = q("select * from addon where hidden = 0"); if ($r) foreach ($r as $rr) |