diff options
-rw-r--r-- | Zotlabs/Daemon/Fetchparents.php | 42 | ||||
-rw-r--r-- | Zotlabs/Daemon/Notifier.php | 3 | ||||
-rw-r--r-- | Zotlabs/Lib/ASCache.php | 33 | ||||
-rw-r--r-- | Zotlabs/Lib/Activity.php | 858 | ||||
-rw-r--r-- | Zotlabs/Lib/ActivityStreams.php | 21 | ||||
-rw-r--r-- | Zotlabs/Lib/Cache.php | 16 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 303 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 6 | ||||
-rw-r--r-- | Zotlabs/Module/Share.php | 24 | ||||
-rw-r--r-- | Zotlabs/Module/Sse_bs.php | 14 | ||||
-rw-r--r-- | Zotlabs/Widget/Messages.php | 3 | ||||
-rw-r--r-- | boot.php | 3 | ||||
-rw-r--r-- | doc/admin/administrator_guide.md | 5 | ||||
-rw-r--r-- | doc/hook/check_account_password.bb | 16 | ||||
-rw-r--r-- | include/bbcode.php | 4 | ||||
-rw-r--r-- | include/event.php | 5 | ||||
-rw-r--r-- | include/items.php | 4 | ||||
-rw-r--r-- | include/text.php | 5 |
18 files changed, 441 insertions, 924 deletions
diff --git a/Zotlabs/Daemon/Fetchparents.php b/Zotlabs/Daemon/Fetchparents.php new file mode 100644 index 000000000..b00acdfbf --- /dev/null +++ b/Zotlabs/Daemon/Fetchparents.php @@ -0,0 +1,42 @@ +<?php + +namespace Zotlabs\Daemon; + +use Zotlabs\Lib\Activity; + +class Fetchparents { + + static public function run($argc, $argv) { + + logger('Fetchparents invoked: ' . print_r($argv, true)); + + if ($argc < 4) { + return; + } + + $channels = explode(',', $argv[1]); + if (!$channels) { + return; + } + + $observer_hash = $argv[2]; + if (!$observer_hash) { + return; + } + + $mid = $argv[3]; + if (!$mid) { + return; + } + + $force = $argv[4] ?? false; + + foreach ($channels as $channel_id) { + $channel = channelx_by_n($channel_id); + Activity::fetch_and_store_parents($channel, $observer_hash, $mid, null, $force); + } + + return; + + } +} diff --git a/Zotlabs/Daemon/Notifier.php b/Zotlabs/Daemon/Notifier.php index d1c0e4ec8..0474a4a26 100644 --- a/Zotlabs/Daemon/Notifier.php +++ b/Zotlabs/Daemon/Notifier.php @@ -374,7 +374,8 @@ class Notifier { if (($relay_to_owner || $uplink) && ($cmd !== 'relay')) { logger('notifier: followup relay', LOGGER_DEBUG); - $sendto = (($uplink) ? $parent_item['source_xchan'] : (($parent_item['verb'] === ACTIVITY_SHARE) ? $parent_item['author_xchan'] : $parent_item['owner_xchan'])); + // If the Parent item is an Announce the real owner is the parent author + $sendto = (($uplink) ? $parent_item['source_xchan'] : $parent_item['owner_xchan']); self::$recipients = [$sendto]; self::$private = true; $upstream = true; diff --git a/Zotlabs/Lib/ASCache.php b/Zotlabs/Lib/ASCache.php new file mode 100644 index 000000000..63bd73ea7 --- /dev/null +++ b/Zotlabs/Lib/ASCache.php @@ -0,0 +1,33 @@ +<?php /** @file */ + +namespace Zotlabs\Lib; + + /** + * A wrapper for the cache api + */ + +class ASCache { + public static function isEnabled() { + return Config::Get('system', 'as_object_cache_enabled', false); + } + + public static function getAge() { + return Config::Get('system', 'as_object_cache_time', '10 MINUTE'); + } + + public static function Get($key) { + if (!self::isEnabled()) { + return; + } + + return Cache::get($key, self::getAge()); + } + + public static function Set($key, $value) { + if (!self::isEnabled()) { + return; + } + + Cache::set($key, $value); + } +} diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 2bf8543b2..a225e551b 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -153,6 +153,7 @@ class Activity { } else { logger('logger_stats_data cmd:Activity_fetch' . ' start:' . $start_timestamp . ' ' . 'end:' . microtime(true) . ' meta:' . $url . '#' . random_string(16)); + btlogger('activity fetch'); } return json_decode($x['body'], true); @@ -363,7 +364,7 @@ class Activity { if ($items) { $x = []; foreach ($items as $i) { - $m = get_iconfig($i['id'], 'activitypub', 'rawmsg'); + $m = IConfig::Get($i['id'], 'activitypub', 'rawmsg'); if ($m) { if (is_string($m)) $t = json_decode($m, true); @@ -498,7 +499,7 @@ class Activity { // set this for descendants even if the current item is not private // because it may have been relayed from a private item. - $token = get_iconfig($i, 'ocap', 'relay'); + $token = IConfig::Get($i, 'ocap', 'relay'); if ($token && $has_images) { $matches_processed = []; for ($n = 0; $n < count($images); $n++) { @@ -1083,11 +1084,11 @@ class Activity { $ret['type'] = 'Person'; if ($c) { - if (get_pconfig($c['channel_id'], 'system', 'group_actor')) { + if (PConfig::Get($c['channel_id'], 'system', 'group_actor')) { $ret['type'] = 'Group'; } - $ret['manuallyApprovesFollowers'] = ((get_pconfig($c['channel_id'], 'system', 'autoperms')) ? false : true); + $ret['manuallyApprovesFollowers'] = ((PConfig::Get($c['channel_id'], 'system', 'autoperms')) ? false : true); } $ret['id'] = $id; @@ -1396,7 +1397,7 @@ class Activity { } } - $role = get_pconfig($channel['channel_id'], 'system', 'permissions_role', 'personal'); + $role = PConfig::Get($channel['channel_id'], 'system', 'permissions_role', 'personal'); $x = PermissionRoles::role_perms($role); $their_perms = Permissions::FilledPerms($x['perms_connect']); @@ -1417,7 +1418,7 @@ class Activity { // We've already approved them or followed them first // Send an Accept back to them - set_abconfig($channel['channel_id'], $person_obj['id'], 'pubcrawl', 'their_follow_id', $their_follow_id); + AbConfig::Set($channel['channel_id'], $person_obj['id'], 'pubcrawl', 'their_follow_id', $their_follow_id); Master::Summon(['Notifier', 'permission_accept', $contact['abook_id']]); return; @@ -1429,7 +1430,7 @@ class Activity { if(in_array($k, ['send_stream', 'post_wall'])) { continue; // Those will be set once we accept their follow request } - set_abconfig($channel['channel_id'], $contact['abook_xchan'], 'their_perms', $k, $v); + AbConfig::Set($channel['channel_id'], $contact['abook_xchan'], 'their_perms', $k, $v); } $abook_instance = $contact['abook_instance']; @@ -1463,7 +1464,7 @@ class Activity { // From here on out we assume a Follow activity to somebody we have no existing relationship with - set_abconfig($channel['channel_id'], $person_obj['id'], 'pubcrawl', 'their_follow_id', $their_follow_id); + AbConfig::Set($channel['channel_id'], $person_obj['id'], 'pubcrawl', 'their_follow_id', $their_follow_id); // The xchan should have been created by actor_store() above @@ -1481,7 +1482,7 @@ class Activity { $my_perms = $p['perms']; $automatic = $p['automatic']; - $closeness = get_pconfig($channel['channel_id'], 'system', 'new_abook_closeness', 80); + $closeness = PConfig::Get($channel['channel_id'], 'system', 'new_abook_closeness', 80); $r = abook_store_lowlevel( [ @@ -1500,11 +1501,11 @@ class Activity { if ($my_perms) foreach ($my_perms as $k => $v) - set_abconfig($channel['channel_id'], $ret['xchan_hash'], 'my_perms', $k, $v); + AbConfig::Set($channel['channel_id'], $ret['xchan_hash'], 'my_perms', $k, $v); if ($their_perms) foreach ($their_perms as $k => $v) - set_abconfig($channel['channel_id'], $ret['xchan_hash'], 'their_perms', $k, $v); + AbConfig::Set($channel['channel_id'], $ret['xchan_hash'], 'their_perms', $k, $v); if ($r) { logger("New ActivityPub follower for {$channel['channel_name']}"); @@ -1540,7 +1541,7 @@ class Activity { unset($clone['abook_account']); unset($clone['abook_channel']); - $abconfig = load_abconfig($channel['channel_id'], $clone['abook_xchan']); + $abconfig = AbConfig::Load($channel['channel_id'], $clone['abook_xchan']); if ($abconfig) $clone['abconfig'] = $abconfig; @@ -1581,7 +1582,7 @@ class Activity { ); if ($r) { // remove all permissions they provided - del_abconfig($channel['channel_id'], $r[0]['xchan_hash'], 'system', 'their_perms'); + AbConfig::Delete($channel['channel_id'], $r[0]['xchan_hash'], 'system', 'their_perms'); } } @@ -1869,31 +1870,6 @@ class Activity { } } - static function create_action($channel, $observer_hash, $act) { - - if (in_array($act->obj['type'], ['Note', 'Article', 'Video'])) { - self::create_note($channel, $observer_hash, $act); - } - - - } - - static function announce_action($channel, $observer_hash, $act) { - - if (in_array($act->type, ['Announce'])) { - self::announce_note($channel, $observer_hash, $act); - } - - } - - static function like_action($channel, $observer_hash, $act) { - - if (in_array($act->obj['type'], ['Note', 'Article', 'Video'])) { - self::like_note($channel, $observer_hash, $act); - } - - - } // sort function width decreasing static function vid_sort($a, $b) { @@ -1906,251 +1882,6 @@ class Activity { return (($a_width > $b_width) ? -1 : 1); } - static function create_note($channel, $observer_hash, $act) { - - $s = []; - $is_sys_channel = is_sys_channel($channel['channel_id']); - $parent = ((array_key_exists('inReplyTo', $act->obj)) ? urldecode($act->obj['inReplyTo']) : ''); - - if ($parent) { - - $r = q("select * from item where uid = %d and ( mid = '%s' or mid = '%s' ) limit 1", - intval($channel['channel_id']), - dbesc($parent), - dbesc(basename($parent)) - ); - - if (!$r) { - logger('parent not found.'); - return; - } - - if ($r[0]['owner_xchan'] === $channel['channel_hash']) { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { - logger('no comment permission.'); - return; - } - } - - $s['parent_mid'] = $r[0]['mid']; - $s['owner_xchan'] = $r[0]['owner_xchan']; - $s['author_xchan'] = $observer_hash; - - } - else { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { - logger('no send_stream permission'); - return; - } - $s['owner_xchan'] = $s['author_xchan'] = $observer_hash; - } - - if ($act->recips && (!in_array(ACTIVITY_PUBLIC_INBOX, $act->recips))) - $s['item_private'] = 1; - - - if (array_key_exists('directMessage', $act->obj) && intval($act->obj['directMessage'])) { - $s['item_private'] = 2; - } - - if (intval($s['item_private']) === 2) { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'post_mail')) { - logger('no post_mail permission'); - return; - } - } - - $content = self::get_content($act->obj); - - if (!$content) { - logger('no content'); - return; - } - - $s['aid'] = $channel['channel_account_id']; - $s['uid'] = $channel['channel_id']; - - // Make sure we use the zot6 identity where applicable - - $s['author_xchan'] = self::find_best_identity($s['author_xchan']); - $s['owner_xchan'] = self::find_best_identity($s['owner_xchan']); - - if (!$s['author_xchan']) { - logger('No author: ' . print_r($act, true)); - } - - if (!$s['owner_xchan']) { - logger('No owner: ' . print_r($act, true)); - } - - if (!$s['author_xchan'] || !$s['owner_xchan']) - return; - - $s['mid'] = urldecode($act->obj['id']); - $s['uuid'] = $act->obj['diaspora:guid']; - $s['plink'] = urldecode($act->obj['id']); - - - if ($act->data['published']) { - $s['created'] = datetime_convert('UTC', 'UTC', $act->data['published']); - } - elseif ($act->obj['published']) { - $s['created'] = datetime_convert('UTC', 'UTC', $act->obj['published']); - } - if ($act->data['updated']) { - $s['edited'] = datetime_convert('UTC', 'UTC', $act->data['updated']); - } - elseif ($act->obj['updated']) { - $s['edited'] = datetime_convert('UTC', 'UTC', $act->obj['updated']); - } - if ($act->data['expires']) { - $s['expires'] = datetime_convert('UTC', 'UTC', $act->data['expires']); - } - elseif ($act->obj['expires']) { - $s['expires'] = datetime_convert('UTC', 'UTC', $act->obj['expires']); - } - - if (!$s['created']) - $s['created'] = datetime_convert(); - - if (!$s['edited']) - $s['edited'] = $s['created']; - - - if (!$s['parent_mid']) - $s['parent_mid'] = $s['mid']; - - - $s['title'] = self::bb_content($content, 'name'); - $s['summary'] = self::bb_content($content, 'summary'); - $s['body'] = self::bb_content($content, 'content'); - $s['verb'] = ACTIVITY_POST; - $s['obj_type'] = ACTIVITY_OBJ_NOTE; - - $generator = $act->get_property_obj('generator'); - if (!$generator) - $generator = $act->get_property_obj('generator', $act->obj); - - if ($generator && array_key_exists('type', $generator) - && in_array($generator['type'], ['Application', 'Service']) && array_key_exists('name', $generator)) { - $s['app'] = escape_tags($generator['name']); - } - - if ($channel['channel_system']) { - $incl = get_config('system','pubstream_incl'); - $excl = get_config('system','pubstream_excl'); - - if(($incl || $excl) && !MessageFilter::evaluate($s, $incl, $excl)) { - logger('post is filtered'); - return; - } - } - - $abook = q("select * from abook where (abook_xchan = '%s' OR abook_xchan = '%s') and abook_channel = %d ", - dbesc($s['author_xchan']), - dbesc($s['owner_xchan']), - intval($channel['channel_id']) - ); - - if ($abook) { - if (!post_is_importable($channel['channel_id'], $s, $abook)) { - logger('post is filtered'); - return; - } - } - - if ($act->obj['conversation']) { - set_iconfig($s, 'ostatus', 'conversation', $act->obj['conversation'], 1); - } - - $a = self::decode_taxonomy($act->obj); - if ($a) { - $s['term'] = $a; - } - - $a = self::decode_attachment($act->obj); - if ($a) { - $s['attach'] = $a; - } - - if ($act->obj['type'] === 'Note' && $s['attach']) { - $s['body'] .= self::bb_attach($s['attach'], $s['body']); - } - - // we will need a hook here to extract magnet links e.g. peertube - // right now just link to the largest mp4 we find that will fit in our - // standard content region - - if ($act->obj['type'] === 'Video') { - - $vtypes = [ - 'video/mp4', - 'video/ogg', - 'video/webm' - ]; - - $mps = []; - if (array_key_exists('url', $act->obj) && is_array($act->obj['url'])) { - foreach ($act->obj['url'] as $vurl) { - if (in_array($vurl['mimeType'], $vtypes)) { - if (!array_key_exists('width', $vurl)) { - $vurl['width'] = 0; - } - $mps[] = $vurl; - } - } - } - if ($mps) { - usort($mps, [__CLASS__, 'vid_sort']); - foreach ($mps as $m) { - if (intval($m['width']) < 500) { - $s['body'] .= "\n\n" . '[video]' . $m['href'] . '[/video]'; - break; - } - } - } - } - - set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); - if ($parent) { - set_iconfig($s, 'activitypub', 'rawmsg', $act->raw, 1); - } - - $x = null; - - $r = q("select created, edited from item where mid = '%s' and uid = %d limit 1", - dbesc($s['mid']), - intval($s['uid']) - ); - if ($r) { - if ($s['edited'] > $r[0]['edited']) { - $x = item_store_update($s); - } - else { - return; - } - } - else { - $x = item_store($s); - } - - if (is_array($x) && $x['item_id']) { - if ($parent) { - if ($s['owner_xchan'] === $channel['channel_hash']) { - // We are the owner of this conversation, so send all received comments back downstream - Master::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]); - } - } - sync_an_item($channel['channel_id'], $x['item_id']); - } - - } static function get_actor_bbmention($id) { @@ -2330,7 +2061,16 @@ class Activity { $content = self::get_content($act->obj); } - $s['mid'] = $act->objprop('id') ?: $act->obj; + $s['mid'] = $act->objprop('id'); + + if (!$s['mid'] && is_string($act->obj)) { + $s['mid'] = $act->obj; + } + + // pleroma fetched activities + if (!$s['mid'] && isset($act->obj['data']['id'])) { + $s['mid'] = $act->obj['data']['id']; + } if (!$s['mid']) { return false; @@ -2908,22 +2648,22 @@ class Activity { } if ($ap_rawmsg) { - set_iconfig($s, 'activitypub', 'rawmsg', $ap_rawmsg, 1); + IConfig::Set($s, 'activitypub', 'rawmsg', $ap_rawmsg, 1); } elseif (!array_key_exists('signed', $raw_arr)) { - set_iconfig($s, 'activitypub', 'rawmsg', $act->raw, 1); + IConfig::Set($s, 'activitypub', 'rawmsg', $act->raw, 1); } if ($diaspora_rawmsg) { - set_iconfig($s, 'diaspora', 'fields', $diaspora_rawmsg, 1); + IConfig::Set($s, 'diaspora', 'fields', $diaspora_rawmsg, 1); } if ($act->raw_recips) { - set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); + IConfig::Set($s, 'activitypub', 'recips', $act->raw_recips); } if ($act->objprop('type') === 'Event' && $act->objprop('timezone')) { - set_iconfig($s, 'event', 'timezone', $act->objprop('timezone'), true); + IConfig::Set($s, 'event', 'timezone', $act->objprop('timezone'), true); } $hookinfo = [ @@ -2965,6 +2705,28 @@ class Activity { intval($channel['channel_id']) ); + if (!$parent) { + if (perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') || $is_sys_channel) { + if ($item['verb'] === 'Announce') { + $force = true; + } + + if ($fetch_parents) { + // Cache::set($item['mid'], 'json:' . $act->raw); + App::$cache['fetch_objects'][$item['mid']]['channels'][] = $channel['channel_id']; + App::$cache['fetch_objects'][$item['mid']]['force'] = intval($force); + + //Master::Summon(['Fetchparents', $channel['channel_id'], $observer_hash, $item['mid'], $force]); + //self::fetch_and_store_parents($channel, $observer_hash, $item, $act, $force); + return; + } + } + + logger('no parent'); + return; + } + + // TODO: if we do not have a parent stop here and move the fetch to background? if ($parent && $parent[0]['obj_type'] === 'Question') { @@ -3005,7 +2767,7 @@ class Activity { }*/ if (!$allowed) { - if (get_pconfig($channel['channel_id'], 'system', 'moderate_unsolicited_comments') && $item['obj_type'] !== 'Answer') { + if (PConfig::Get($channel['channel_id'], 'system', 'moderate_unsolicited_comments') && $item['obj_type'] !== 'Answer') { $item['item_blocked'] = ITEM_MODERATED; $allowed = true; } @@ -3050,7 +2812,7 @@ class Activity { if (tgroup_check($channel['channel_id'], $item) && (!$is_child_node)) { // for forum deliveries, make sure we keep a copy of the signed original - set_iconfig($item, 'activitypub', 'rawmsg', $act->raw, 1); + IConfig::Set($item, 'activitypub', 'rawmsg', $act->raw, 1); $allowed = true; } @@ -3110,6 +2872,7 @@ class Activity { $item['author_xchan'] = self::find_best_identity($item['author_xchan']); $item['owner_xchan'] = self::find_best_identity($item['owner_xchan']); + $item['source_xchan'] = ((!empty($item['source_xchan'])) ? self::find_best_identity($item['source_xchan']) : ''); if (!$item['author_xchan']) { logger('No author: ' . print_r($act, true)); @@ -3123,8 +2886,8 @@ class Activity { return; if ($channel['channel_system']) { - $incl = get_config('system','pubstream_incl'); - $excl = get_config('system','pubstream_excl'); + $incl = Config::Get('system','pubstream_incl'); + $excl = Config::Get('system','pubstream_excl'); if(($incl || $excl) && !MessageFilter::evaluate($item, $incl, $excl)) { logger('post is filtered'); @@ -3132,9 +2895,10 @@ class Activity { } } - $abook = q("select * from abook where ( abook_xchan = '%s' OR abook_xchan = '%s') and abook_channel = %d ", + $abook = q("select * from abook where ( abook_xchan = '%s' OR abook_xchan = '%s' OR abook_xchan = '%s') and abook_channel = %d ", dbesc($item['author_xchan']), dbesc($item['owner_xchan']), + dbesc($item['source_xchan']), intval($channel['channel_id']) ); @@ -3146,48 +2910,21 @@ class Activity { } if (array_key_exists('conversation', $act->obj)) { - set_iconfig($item, 'ostatus', 'conversation', $act->obj['conversation'], 1); + IConfig::Set($item, 'ostatus', 'conversation', $act->obj['conversation'], 1); } // This isn't perfect but the best we can do for now. $item['comment_policy'] = ((isset($act->data['commentPolicy'])) ? $act->data['commentPolicy'] : 'authenticated'); - set_iconfig($item, 'activitypub', 'recips', $act->raw_recips); + IConfig::Set($item, 'activitypub', 'recips', $act->raw_recips); if (intval($act->sigok)) { $item['item_verified'] = 1; } if ($is_child_node) { - if (!$parent) { - if (!plugin_is_installed('pubcrawl')) { - return; - } - - $fetch = false; - - if (perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') || $is_sys_channel) { - if ($item['verb'] === 'Announce') { - $force = true; - } - $fetch = (($fetch_parents) ? self::fetch_and_store_parents($channel, $observer_hash, $item, $force) : false); - } - - if ($fetch) { - $parent = q("select * from item where mid = '%s' and uid = %d", - dbesc($item['parent_mid']), - intval($item['uid']) - ); - } - } - - if (!$parent) { - logger('no parent'); - return; - } - - $item['owner_xchan'] = (($item['verb'] === 'Announce') ? $parent[0]['author_xchan'] : $parent[0]['owner_xchan']); + $item['owner_xchan'] = $parent[0]['owner_xchan']; if ($parent[0]['parent_mid'] !== $item['parent_mid']) { $item['thr_parent'] = $item['parent_mid']; @@ -3306,20 +3043,55 @@ class Activity { } - static public function fetch_and_store_parents($channel, $observer_hash, $item, $force = false) { + /** + * @brief fetch a thread upwards by either providing a message id or an item/activity pair + * + * @param array $channel + * @param array $observer_hash + * @param array $item string|array + * @param object $act activitystreams object (optional) default null + * @param bool $force disregard permissions and force storage (optional) default false + * @return bool + */ + + static public function fetch_and_store_parents($channel, $observer_hash, $item, $act = null, $force = false) { logger('fetching parents'); + if (!$item) { + return false; + } + $p = []; + $announce_init = false; - $announce_init = $item['verb'] === 'Announce'; + if (is_object($act) && is_array($item)) { + $p[] = [$act, $item]; + $announce_init = ($item['verb'] === 'Announce'); + } + + if (is_string($item)) { + $mid = $item; + $item = [ + 'parent_mid' => $mid, + 'mid' => '' + ]; + } $current_item = $item; - while ($current_item['parent_mid'] !== $current_item['mid']) { - $n = self::fetch($current_item['parent_mid'], $channel); + $i = 0; - if (!$n) { - break; + while ($current_item['parent_mid'] !== $current_item['mid']) { + $cached = ASCache::Get($current_item['parent_mid']); + if ($cached) { + $n = unserialise($cached); + } + else { + $n = self::fetch($current_item['parent_mid'], $channel); + if (!$n) { + break; + } + ASCache::Set($current_item['parent_mid'], serialise($n)); } $a = new ActivityStreams($n); @@ -3357,6 +3129,19 @@ class Activity { $item['item_fetched'] = true; + if ($announce_init) { + // Store the sender of the initial announce + $item['source_xchan'] = $observer_hash; + // WARNING: the presence of both source_xchan and non-zero item_uplink here will cause a delivery loop + $item['item_uplink'] = 0; + $item['verb'] = 'Announce'; + $item['parent_mid'] = $item['thr_parent'] = $item['mid']; + $item['item_thread_top'] = 1; + } + else { + $announce_init = ($i === 0 && $item['verb'] === 'Announce'); + } + if (intval($channel['channel_system']) && intval($item['item_private'])) { $p = []; break; @@ -3367,27 +3152,18 @@ class Activity { break; } - if ($announce_init) { - // If the fetch was initiated by an announce activity - // do not set item fetched. This way the owner will be set to the - // observer -> the announce actor - unset($item['item_fetched']); - $item['verb'] = 'Announce'; - $item['parent_mid'] = $item['mid']; - $item['item_thread_top'] = 1; - } - array_unshift($p, [$a, $item]); - if ($announce_init || $item['parent_mid'] === $item['mid']) { + if ($item['parent_mid'] === $item['mid']) { break; } + } $current_item = $item; + $i++; } - if ($p) { foreach ($p as $pv) { if ($pv[0]->is_valid()) { @@ -3396,407 +3172,9 @@ class Activity { } return true; } - return false; } - /* - static public function fetch_and_store_parents($channel, $item) { - - logger('fetching parents'); - - $p = []; - - $current_item = $item; - - while ($current_item['parent_mid'] !== $current_item['mid']) { - $n = self::fetch($current_item['parent_mid'], $channel); - if (!$n) { - break; - } - $a = new ActivityStreams($n); - - //logger($a->debug()); - - if (!$a->is_valid()) { - break; - } - - if (is_array($a->actor) && array_key_exists('id', $a->actor)) { - self::actor_store($a->actor); - } - - $replies = null; - if (isset($a->obj['replies']['first']['items'])) { - $replies = $a->obj['replies']['first']['items']; - // we already have this one - array_diff($replies, [$current_item['mid']]); - } - - $item = null; - - switch ($a->type) { - case 'Create': - case 'Update': - //case 'Like': - //case 'Dislike': - case 'Announce': - $item = self::decode_note($a); - break; - default: - break; - - } - - $hookinfo = [ - 'a' => $a, - 'item' => $item - ]; - - call_hooks('fetch_and_store', $hookinfo); - - $item = $hookinfo['item']; - - if ($item) { - - array_unshift($p, [$a, $item, $replies]); - - if ($item['parent_mid'] === $item['mid'] || count($p) > 20) { - break; - } - - } - $current_item = $item; - } - - if ($p) { - foreach ($p as $pv) { - self::store($channel, $pv[0]->actor['id'], $pv[0], $pv[1], false); - if ($pv[2]) - self::fetch_and_store_replies($channel, $pv[2]); - } - return true; - } - - return false; - } - - static public function fetch_and_store_replies($channel, $arr) { - - logger('fetching replies'); - logger(print_r($arr, true)); - - $p = []; - - foreach ($arr as $url) { - - $n = self::fetch($url, $channel); - if (!$n) { - break; - } - - $a = new ActivityStreams($n); - - if (!$a->is_valid()) { - break; - } - - $item = null; - - switch ($a->type) { - case 'Create': - case 'Update': - case 'Like': - case 'Dislike': - case 'Announce': - $item = self::decode_note($a); - break; - default: - break; - } - - $hookinfo = [ - 'a' => $a, - 'item' => $item - ]; - - call_hooks('fetch_and_store', $hookinfo); - - $item = $hookinfo['item']; - - if ($item) { - array_unshift($p, [$a, $item]); - } - - } - - if ($p) { - foreach ($p as $pv) { - self::store($channel, $pv[0]->actor['id'], $pv[0], $pv[1], false); - } - } - - } -*/ - -/* this is deprecated and not used anymore - static function announce_note($channel, $observer_hash, $act) { - - $s = []; - $is_sys_channel = is_sys_channel($channel['channel_id']); - - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'send_stream') && !$is_sys_channel) { - logger('no permission'); - return; - } - - $content = self::get_content($act->obj); - - if (!$content) { - logger('no content'); - return; - } - - $s['owner_xchan'] = $s['author_xchan'] = $observer_hash; - - $s['aid'] = $channel['channel_account_id']; - $s['uid'] = $channel['channel_id']; - $s['mid'] = urldecode($act->obj['id']); - $s['plink'] = urldecode($act->obj['id']); - - if (!$s['created']) - $s['created'] = datetime_convert(); - - if (!$s['edited']) - $s['edited'] = $s['created']; - - - $s['parent_mid'] = $s['mid']; - - $s['verb'] = ACTIVITY_POST; - $s['obj_type'] = ACTIVITY_OBJ_NOTE; - $s['app'] = t('ActivityPub'); - - if ($channel['channel_system']) { - if (!MessageFilter::evaluate($s, get_config('system', 'pubstream_incl'), get_config('system', 'pubstream_excl'))) { - logger('post is filtered'); - return; - } - } - - $abook = q("select * from abook where abook_xchan = '%s' and abook_channel = %d limit 1", - dbesc($observer_hash), - intval($channel['channel_id']) - ); - - if ($abook) { - if (!post_is_importable($s, $abook[0])) { - logger('post is filtered'); - return; - } - } - - if ($act->obj['conversation']) { - set_iconfig($s, 'ostatus', 'conversation', $act->obj['conversation'], 1); - } - - $a = self::decode_taxonomy($act->obj); - if ($a) { - $s['term'] = $a; - } - - $a = self::decode_attachment($act->obj); - if ($a) { - $s['attach'] = $a; - } - - $body = "[share author='" . urlencode($act->sharee['name']) . - "' profile='" . $act->sharee['url'] . - "' avatar='" . $act->sharee['photo_s'] . - "' link='" . ((is_array($act->obj['url'])) ? $act->obj['url']['href'] : $act->obj['url']) . - "' auth='" . ((is_matrix_url($act->obj['url'])) ? 'true' : 'false') . - "' posted='" . $act->obj['published'] . - "' message_id='" . $act->obj['id'] . - "']"; - - if ($content['name']) - $body .= self::bb_content($content, 'name') . "\r\n"; - - $body .= self::bb_content($content, 'content'); - - if ($act->obj['type'] === 'Note' && $s['attach']) { - $body .= self::bb_attach($s['attach'], $body); - } - - $body .= "[/share]"; - - $s['title'] = self::bb_content($content, 'name'); - $s['body'] = $body; - - if ($act->recips && (!in_array(ACTIVITY_PUBLIC_INBOX, $act->recips))) - $s['item_private'] = 1; - - set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); - - $r = q("select created, edited from item where mid = '%s' and uid = %d limit 1", - dbesc($s['mid']), - intval($s['uid']) - ); - if ($r) { - if ($s['edited'] > $r[0]['edited']) { - $x = item_store_update($s); - } - else { - return; - } - } - else { - $x = item_store($s); - } - - if (is_array($x) && $x['item_id']) { - if ($s['owner_xchan'] === $channel['channel_hash']) { - // We are the owner of this conversation, so send all received comments back downstream - Master::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]); - } - - sync_an_item($channel['channel_id'], $x['item_id']); - } - - } -*/ - - static function like_note($channel, $observer_hash, $act) { - - $s = []; - - $parent = $act->obj['id']; - - if ($act->type === 'Like') - $s['verb'] = ACTIVITY_LIKE; - if ($act->type === 'Dislike') - $s['verb'] = ACTIVITY_DISLIKE; - - if (!$parent) - return; - - $r = q("select * from item where uid = %d and ( mid = '%s' or mid = '%s' ) limit 1", - intval($channel['channel_id']), - dbesc($parent), - dbesc(urldecode(basename($parent))) - ); - - if (!$r) { - logger('parent not found.'); - return; - } - - xchan_query($r); - $parent_item = $r[0]; - - if ($parent_item['owner_xchan'] === $channel['channel_hash']) { - if (!perm_is_allowed($channel['channel_id'], $observer_hash, 'post_comments')) { - logger('no comment permission.'); - return; - } - } - - if ($parent_item['mid'] === $parent_item['parent_mid']) { - $s['parent_mid'] = $parent_item['mid']; - } - else { - $s['thr_parent'] = $parent_item['mid']; - $s['parent_mid'] = $parent_item['parent_mid']; - } - - $s['owner_xchan'] = $parent_item['owner_xchan']; - $s['author_xchan'] = $observer_hash; - - $s['aid'] = $channel['channel_account_id']; - $s['uid'] = $channel['channel_id']; - $s['mid'] = $act->id; - - if (!$s['parent_mid']) - $s['parent_mid'] = $s['mid']; - - - $post_type = (($parent_item['resource_type'] === 'photo') ? t('photo') : t('post')); - - $links = [['rel' => 'alternate', 'type' => 'text/html', 'href' => $parent_item['plink']]]; - $objtype = (($parent_item['resource_type'] === 'photo') ? ACTIVITY_OBJ_PHOTO : ACTIVITY_OBJ_NOTE); - - $z = q("select * from xchan where xchan_hash = '%s' limit 1", - dbesc($parent_item['author_xchan']) - ); - if ($z) - $item_author = $z[0]; - - $object = json_encode([ - 'type' => $post_type, - 'id' => $parent_item['mid'], - 'parent' => (($parent_item['thr_parent']) ? $parent_item['thr_parent'] : $parent_item['parent_mid']), - 'link' => $links, - 'title' => $parent_item['title'], - 'content' => $parent_item['body'], - 'created' => $parent_item['created'], - 'edited' => $parent_item['edited'], - 'author' => [ - 'name' => $item_author['xchan_name'], - 'address' => $item_author['xchan_addr'], - 'guid' => $item_author['xchan_guid'], - 'guid_sig' => $item_author['xchan_guid_sig'], - 'link' => [ - ['rel' => 'alternate', 'type' => 'text/html', 'href' => $item_author['xchan_url']], - ['rel' => 'photo', 'type' => $item_author['xchan_photo_mimetype'], 'href' => $item_author['xchan_photo_m']]], - ], - ], JSON_UNESCAPED_SLASHES - ); - - if ($act->type === 'Like') - $bodyverb = t('%1$s likes %2$s\'s %3$s'); - if ($act->type === 'Dislike') - $bodyverb = t('%1$s doesn\'t like %2$s\'s %3$s'); - - $ulink = '[url=' . $item_author['xchan_url'] . ']' . $item_author['xchan_name'] . '[/url]'; - $alink = '[url=' . $parent_item['author']['xchan_url'] . ']' . $parent_item['author']['xchan_name'] . '[/url]'; - $plink = '[url=' . z_root() . '/display/' . urlencode($act->id) . ']' . $post_type . '[/url]'; - $s['body'] = sprintf($bodyverb, $ulink, $alink, $plink); - - $s['app'] = t('ActivityPub'); - - // set the route to that of the parent so downstream hubs won't reject it. - - $s['route'] = $parent_item['route']; - $s['item_private'] = $parent_item['item_private']; - $s['obj_type'] = $objtype; - $s['obj'] = $object; - - if ($act->obj['conversation']) { - set_iconfig($s, 'ostatus', 'conversation', $act->obj['conversation'], 1); - } - - if ($act->recips && (!in_array(ACTIVITY_PUBLIC_INBOX, $act->recips))) - $s['item_private'] = 1; - - set_iconfig($s, 'activitypub', 'recips', $act->raw_recips); - - $result = item_store($s); - - if ($result['success']) { - // if the message isn't already being relayed, notify others - if (intval($parent_item['item_origin'])) - Master::Summon(['Notifier', 'comment-import', $result['item_id']]); - sync_an_item($channel['channel_id'], $result['item_id']); - } - - return; - } public static function bb_attach($item) { diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index 0770f2040..3749126d3 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -90,6 +90,15 @@ class ActivityStreams { // Attempt to assemble an Activity from what we were given. if ($this->is_valid()) { $this->id = $this->get_property_obj('id'); + + if (!$this->id) { + logger('Data with mmissing id: ' . print_r($this->data, true)); + return; + } + + // cache for future use + ASCache::Set($this->id, 'json:' . $this->raw); + $this->type = $this->get_primary_type(); $this->actor = $this->get_actor('actor', '', ''); $this->obj = $this->get_compound_property('object'); @@ -394,12 +403,22 @@ class ActivityStreams { $x = $this->get_property_obj($property, $base, $namespace); if ($this->is_url($x)) { - $y = $this->fetch_property($x); + $cached = ASCache::Get($x); + if ($cached) { + $y = unserialise($cached); + } + else { + $y = $this->fetch_property($x); + if ($y) { + ASCache::Set($x, serialise($y)); + } + } if (is_array($y)) { $x = $y; } } + // verify and unpack JSalmon signature if present if (is_array($x) && array_key_exists('signed', $x)) { diff --git a/Zotlabs/Lib/Cache.php b/Zotlabs/Lib/Cache.php index 60bf64611..f3f520496 100644 --- a/Zotlabs/Lib/Cache.php +++ b/Zotlabs/Lib/Cache.php @@ -17,8 +17,8 @@ class Cache { */ public static function get($key, $age = '') { - - $hash = hash('whirlpool',$key); +// $hash = hash('whirlpool',$key); + $hash = uuid_from_url($key); $r = q("SELECT v FROM cache WHERE k = '%s' AND updated > %s - INTERVAL %s LIMIT 1", dbesc($hash), @@ -32,23 +32,25 @@ class Cache { } public static function set($key,$value) { +// $hash = hash('whirlpool',$key); + $hash = uuid_from_url($key); - $hash = hash('whirlpool',$key); - - $r = q("SELECT * FROM cache WHERE k = '%s' limit 1", + $r = q("SELECT * FROM cache WHERE k = '%s' LIMIT 1", dbesc($hash) ); if($r) { q("UPDATE cache SET v = '%s', updated = '%s' WHERE k = '%s'", dbesc($value), dbesc(datetime_convert()), - dbesc($hash)); + dbesc($hash) + ); } else { q("INSERT INTO cache (k, v, updated) VALUES ('%s', '%s', '%s')", dbesc($hash), dbesc($value), - dbesc(datetime_convert())); + dbesc(datetime_convert()) + ); } } } diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index be1ca15c0..4a327e24f 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -1157,7 +1157,6 @@ class Libzot { else { $item = []; } - logger($AS->debug(), LOGGER_DATA); } @@ -1364,11 +1363,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) { @@ -1579,6 +1580,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. @@ -1605,119 +1639,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'] . '"'); + if ($relay) + $perm = 'post_comments'; - // 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. - - $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 @@ -1729,28 +1667,38 @@ class Libzot { // the top level post is unlikely to be imported and // this is just an exercise in futility. + if ($relay || $request || $local_public || !perm_is_allowed($channel['channel_id'], $sender, 'send_stream')) { + continue; + } + if ($arr['verb'] === 'Announce') { - Activity::fetch_and_store_parents($channel, $sender, $arr, true); + Master::Summon(['Fetchparents', $channel['channel_id'], $sender, $arr['mid'], true]); } else { - if (perm_is_allowed($channel['channel_id'], $sender, 'send_stream')) { - Master::Summon(['Zotconvo', $channel['channel_id'], $arr['parent_mid']]); - } - continue; + Master::Summon(['Zotconvo', $channel['channel_id'], $arr['parent_mid']]); } + + 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'])) { $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. @@ -1758,10 +1706,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 { @@ -1771,7 +1717,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); @@ -1788,8 +1734,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; @@ -1802,42 +1748,86 @@ 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; + } + + 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; + } - $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)]); - $DR->update('relayed'); - $result[] = $DR->get(); } - continue; + if ($request) { + // Conversation fetches (e.g. $request == true) take place for + // a) new comments on expired posts + // b) if we received an announce + + + // 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) { + $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'])), @@ -1845,7 +1835,6 @@ class Libzot { ); if ($r) { - $item_id = $r[0]['id']; if (intval($r[0]['item_deleted'])) { @@ -1915,7 +1904,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 +1945,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']); @@ -2320,12 +2309,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 diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index e7d6e33f8..77f26c386 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -907,6 +907,12 @@ class ThreadItem { $this->owner_name = $this->data['owner']['xchan_name']; $this->wall_to_wall = true; } + elseif($this->is_toplevel() && $this->get_data_value('verb') === 'Announce' && isset($this->data['source'])) { + $this->owner_url = chanlink_hash($this->data['source']['xchan_hash']); + $this->owner_photo = $this->data['source']['xchan_photo_s']; + $this->owner_name = $this->data['source']['xchan_name']; + $this->wall_to_wall = true; + } } private function is_wall_to_wall() { diff --git a/Zotlabs/Module/Share.php b/Zotlabs/Module/Share.php index 248126f7f..ea9313fa8 100644 --- a/Zotlabs/Module/Share.php +++ b/Zotlabs/Module/Share.php @@ -65,8 +65,6 @@ class Share extends \Zotlabs\Web\Controller { $item = $r[0]; - $owner_uid = $r[0]['uid']; - $owner_aid = $r[0]['aid']; /* $can_comment = false; if((array_key_exists('owner',$item)) && intval($item['owner']['abook_self'])) @@ -96,25 +94,39 @@ class Share extends \Zotlabs\Web\Controller { else killme(); - - $arr['aid'] = $owner_aid; - $arr['uid'] = $owner_uid; + $arr['aid'] = $item['aid']; + $arr['uid'] = $item['uid']; $arr['item_origin'] = 1; $arr['item_wall'] = $item['item_wall']; + $arr['item_private'] = $item['item_private']; $arr['uuid'] = item_message_id(); $arr['mid'] = z_root() . '/activity/' . $arr['uuid']; - $arr['parent_mid'] = $item['mid']; + $arr['parent_mid'] = $item['parent_mid']; + $arr['thr_parent'] = $item['mid']; + + $created = datetime_convert(); + + $arr['created'] = $created; + $arr['edited'] = $created; + $arr['commented'] = $created; + $arr['received'] = $created; + $arr['changed'] = $created; + $arr['item_type'] = ITEM_TYPE_POST; $mention = '@[zrl=' . $item['author']['xchan_url'] . ']' . $item['author']['xchan_name'] . '[/zrl]'; $arr['body'] = sprintf( t('🔁 Repeated %1$s\'s %2$s'), $mention, Activity::activity_obj_mapper($item['obj_type'])); $arr['author_xchan'] = $channel['channel_hash']; $arr['owner_xchan'] = $item['author_xchan']; + $arr['source_xchan'] = ''; + $arr['obj'] = $item['obj']; $arr['obj_type'] = $item['obj_type']; $arr['verb'] = ACTIVITY_SHARE; + call_hooks('post_local', $arr); + $post = item_store($arr); $post_id = $post['item_id']; diff --git a/Zotlabs/Module/Sse_bs.php b/Zotlabs/Module/Sse_bs.php index 78adf1859..2e9904cd3 100644 --- a/Zotlabs/Module/Sse_bs.php +++ b/Zotlabs/Module/Sse_bs.php @@ -121,12 +121,7 @@ class Sse_bs extends Controller { $mids = []; $str = ''; - $mids_all_json = Cache::get('sse_mids_all_' . session_id()); - - if (!$mids_all_json) - $mids_all_json = '[]'; - - $mids_all = json_decode($mids_all_json, true); + $mids_all = unserialise(Cache::get('sse_mids_all_' . session_id())); foreach($arr as $a) { $mid_str = '\'' . dbesc(unpack_link_id($a)) . '\''; @@ -137,7 +132,7 @@ class Sse_bs extends Controller { } } - Cache::set('sse_mids_all_' . session_id(), json_encode($mids_all)); + Cache::set('sse_mids_all_' . session_id(), serialise($mids_all)); if(! self::$uid) { return; @@ -448,9 +443,8 @@ class Sse_bs extends Controller { $sql_extra2 = " AND CASE WHEN verb = '" . ACTIVITY_SHARE . "' THEN owner_xchan ELSE author_xchan END IN (" . self::$xchans . ") "; $sql_extra3 = ''; - $sse_mids_all_json = Cache::get('sse_mids_all_' . session_id()); - if ($sse_mids_all_json) { - $sse_mids_all = json_decode($sse_mids_all_json, true); + $sse_mids_all = unserialise(Cache::get('sse_mids_all_' . session_id())); + if ($sse_mids_all) { $sql_extra3 = " AND mid NOT IN (" . protect_sprintf(implode(',', $sse_mids_all)) . ") "; } diff --git a/Zotlabs/Widget/Messages.php b/Zotlabs/Widget/Messages.php index cdd889121..b56fc86f9 100644 --- a/Zotlabs/Widget/Messages.php +++ b/Zotlabs/Widget/Messages.php @@ -147,6 +147,9 @@ class Messages { if($item['owner_xchan'] !== $item['author_xchan']) { $info .= t('via') . ' ' . $item['owner']['xchan_name']; } + elseif($item['verb'] === 'Announce' && isset($item['source'])) { + $info .= t('via') . ' ' . $item['source']['xchan_name']; + } $summary = $item['title']; if (!$summary) { @@ -62,7 +62,7 @@ require_once('include/conversation.php'); require_once('include/acl_selectors.php'); define('PLATFORM_NAME', 'hubzilla'); -define('STD_VERSION', '8.9.3'); +define('STD_VERSION', '8.9.4'); define('ZOT_REVISION', '6.0'); define('DB_UPDATE_VERSION', 1261); @@ -804,6 +804,7 @@ class App { public static $is_sys = false; public static $nav_sel; public static $comanche; + public static $cache = []; // general purpose cache public static $channel_links; diff --git a/doc/admin/administrator_guide.md b/doc/admin/administrator_guide.md index 0cc6b4c0f..bf4dc7355 100644 --- a/doc/admin/administrator_guide.md +++ b/doc/admin/administrator_guide.md @@ -100,7 +100,10 @@ There are several ways to deploy a new hub. * some form of email server or email gateway such that PHP mail() works. -* Mysql 5.x or MariaDB or postgres database server. +* A supported database server. The supported databases are: + - Mysql version 8.0.22 or later + - MariaDB version 10.4 or later + - PostgreSQL version 12 or later * ability to schedule jobs with cron. diff --git a/doc/hook/check_account_password.bb b/doc/hook/check_account_password.bb index 53562ec6e..ce5202f48 100644 --- a/doc/hook/check_account_password.bb +++ b/doc/hook/check_account_password.bb @@ -1 +1,17 @@ [h2]check_account_password[/h2] +Use this hook to provide additional checks or validations of the password given when +registering and account. +[h3]Arguments:[/h3] +[code=php]array( + 'password' => $password, // The password to check + 'result' => array( + 'error' => false, + 'message' => '' + ) +)[/code] +[h3]Results:[/h3] +For a failed check set the [code]error[/code] member of the [code]result[/code] +array to [code]true[/code] and the [code]message[/code] to a short message +explaining why it failed. + +Otherwise, leave it alone. diff --git a/include/bbcode.php b/include/bbcode.php index e0a0fe9a1..fb10ceb4a 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -1097,6 +1097,10 @@ function parseIdentityAwareHTML($Text) { function bbcode($Text, $options = []) { + if (!$Text) { + return EMPTY_STR; + } + if(! is_array($options)) { $options = []; } diff --git a/include/event.php b/include/event.php index 745469064..da504c2d7 100644 --- a/include/event.php +++ b/include/event.php @@ -58,7 +58,9 @@ function format_event_html($ev) { $ev['dtend'] , $bd_format ))) . '</span></div>' . "\r\n"; - $o .= '<div class="event-description">' . zidify_links(smilies(bbcode($ev['description']))) . '</div>' . "\r\n"; + if (!empty($ev['description'])) { + $o .= '<div class="event-description">' . zidify_links(smilies(bbcode($ev['description']))) . '</div>' . "\r\n"; + } if(isset($ev['location']) && $ev['location']) $o .= '<div class="event-location"><span class="event-label"> ' . t('Location:') . '</span> <span class="location">' @@ -117,6 +119,7 @@ function format_event_obj($jobject) { $dtdiff = $dtstart->diff($dtend_obj); + $oneday = false; if($allday && ($dtdiff->days < 2)) $oneday = true; diff --git a/include/items.php b/include/items.php index 6cef3d2cd..56534c4e4 100644 --- a/include/items.php +++ b/include/items.php @@ -353,7 +353,7 @@ function can_comment_on_post($observer_xchan, $item) { case 'specific': case 'contacts': case '': - if(local_channel() && get_abconfig(local_channel(), (($item['verb'] === ACTIVITY_SHARE) ? $item['author_xchan'] : $item['owner_xchan']), 'their_perms', 'post_comments')) { + if(local_channel() && get_abconfig(local_channel(), (($item['verb'] === ACTIVITY_SHARE) ? $item['source_xchan'] : $item['owner_xchan']), 'their_perms', 'post_comments')) { return true; } if(intval($item['item_wall']) && perm_is_allowed($item['uid'],$observer_xchan,'post_comments')) { @@ -1837,7 +1837,7 @@ function item_store($arr, $allow_exec = false, $deliver = true) { dbesc($r[0]['parent_mid']), intval($arr['uid']) ); - if($z && count($z)) + if($z) $r = $z; } diff --git a/include/text.php b/include/text.php index c6b2bed78..41fb11ba2 100644 --- a/include/text.php +++ b/include/text.php @@ -2616,7 +2616,7 @@ function trim_and_unpunify($s) { * @param number $effective_uid */ function xchan_query(&$items, $abook = true, $effective_uid = 0) { - $arr = array(); + $arr = []; if($items && count($items)) { if($effective_uid) { @@ -2631,6 +2631,8 @@ function xchan_query(&$items, $abook = true, $effective_uid = 0) { $arr[] = "'" . dbesc($item['owner_xchan']) . "'"; if($item['author_xchan'] && (! in_array("'" . dbesc($item['author_xchan']) . "'",$arr))) $arr[] = "'" . dbesc($item['author_xchan']) . "'"; + if($item['source_xchan'] && (! in_array("'" . dbesc($item['source_xchan']) . "'",$arr))) + $arr[] = "'" . dbesc($item['source_xchan']) . "'"; } } if(count($arr)) { @@ -2654,6 +2656,7 @@ function xchan_query(&$items, $abook = true, $effective_uid = 0) { for($x = 0; $x < count($items); $x ++) { $items[$x]['owner'] = find_xchan_in_array($items[$x]['owner_xchan'],$chans); $items[$x]['author'] = find_xchan_in_array($items[$x]['author_xchan'],$chans); + $items[$x]['source'] = find_xchan_in_array($items[$x]['source_xchan'],$chans); } } } |