diff options
Diffstat (limited to 'Zotlabs/Lib')
-rw-r--r-- | Zotlabs/Lib/Activity.php | 292 | ||||
-rw-r--r-- | Zotlabs/Lib/ActivityStreams.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Apps.php | 2 | ||||
-rw-r--r-- | Zotlabs/Lib/Connect.php | 8 | ||||
-rw-r--r-- | Zotlabs/Lib/DReport.php | 10 | ||||
-rw-r--r-- | Zotlabs/Lib/Enotify.php | 77 | ||||
-rw-r--r-- | Zotlabs/Lib/IConfig.php | 6 | ||||
-rw-r--r-- | Zotlabs/Lib/JcsEddsa2022.php | 25 | ||||
-rw-r--r-- | Zotlabs/Lib/JcsEddsa2022SignException.php | 15 | ||||
-rw-r--r-- | Zotlabs/Lib/Libzot.php | 70 | ||||
-rw-r--r-- | Zotlabs/Lib/Mailer.php | 86 | ||||
-rw-r--r-- | Zotlabs/Lib/MessageFilter.php | 154 | ||||
-rw-r--r-- | Zotlabs/Lib/Text.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadItem.php | 226 | ||||
-rw-r--r-- | Zotlabs/Lib/ThreadStream.php | 9 | ||||
-rw-r--r-- | Zotlabs/Lib/Traits/HelpHelperTrait.php | 2 |
16 files changed, 550 insertions, 443 deletions
diff --git a/Zotlabs/Lib/Activity.php b/Zotlabs/Lib/Activity.php index 54a1b8d2a..296129ea2 100644 --- a/Zotlabs/Lib/Activity.php +++ b/Zotlabs/Lib/Activity.php @@ -13,6 +13,7 @@ use Zotlabs\Entity\Item; require_once('include/event.php'); require_once('include/html2plain.php'); require_once('include/items.php'); +require_once('include/markdown.php'); class Activity { @@ -68,10 +69,10 @@ class Activity { if ($j) { xchan_query($j, true); $items = fetch_post_tags($j); - } - if ($items) { - return self::encode_item(array_shift($items)); + if ($items) { + return self::encode_item(array_shift($items)); + } } return null; @@ -165,7 +166,7 @@ class Activity { } else { logger('fetch failed: ' . $url); - logger($x['body']); + logger(print_r($x, true), LOGGER_DEBUG); } @@ -580,14 +581,12 @@ class Activity { } } - if (intval($i['item_wall'])) { - $ret['commentPolicy'] = map_scope(PermissionLimits::Get($i['uid'], 'post_comments')); - } - if (intval($i['item_private']) === 2) { $ret['directMessage'] = true; } + $ret['commentPolicy'] = (($i['item_wall']) ? map_scope(PermissionLimits::Get($i['uid'], 'post_comments')) : ''); + if (array_key_exists('comments_closed', $i) && $i['comments_closed'] !== EMPTY_STR && $i['comments_closed'] > NULL_DATE) { if ($ret['commentPolicy']) { $ret['commentPolicy'] .= ' '; @@ -616,6 +615,7 @@ class Activity { if (!empty($cnv)) { if (is_string($cnv) && str_starts_with($cnv, z_root())) { $cnv = str_replace(['/item/', '/activity/'], ['/conversation/', '/conversation/'], $cnv); + $ret['contextHistory'] = $cnv; } $ret['context'] = $cnv; } @@ -694,7 +694,7 @@ class Activity { if (is_array($t) && !array_key_exists('type', $t)) $t['type'] = 'Hashtag'; - if (is_array($t) && (array_key_exists('href', $t) || array_key_exists('id', $t)) && array_key_exists('name', $t)) { + if (is_array($t) && (array_key_exists('href', $t) || array_key_exists('id', $t) || isset($t['icon']['url'])) && array_key_exists('name', $t)) { switch ($t['type']) { case 'Hashtag': $ret[] = ['ttype' => TERM_HASHTAG, 'url' => $t['href'], 'term' => escape_tags((substr($t['name'], 0, 1) === '#') ? substr($t['name'], 1) : $t['name'])]; @@ -709,7 +709,7 @@ class Activity { break; case 'Emoji': - $ret[] = ['ttype' => TERM_EMOJI, 'url' => $t['id'], 'term' => escape_tags($t['name']), 'imgurl' => $t['icon']['url']]; + $ret[] = ['ttype' => TERM_EMOJI, 'url' => $t['id'] ?? $t['icon']['url'], 'term' => escape_tags($t['name']), 'imgurl' => $t['icon']['url']]; break; default: @@ -802,7 +802,7 @@ class Activity { $ret = []; - if (isset($item['attachment'])) { + if (isset($item['attachment']) && is_array($item['attachment'])) { $ptr = $item['attachment']; if (!array_key_exists(0, $ptr)) { $ptr = [$ptr]; @@ -852,6 +852,8 @@ class Activity { $entry['type'] = $att['mediaType']; } elseif (array_key_exists('type', $att) && $att['type'] === 'Image') { $entry['type'] = 'image/jpeg'; + } elseif (array_key_exists('type', $att) && $att['type'] === 'Link') { + $entry['type'] = 'text/uri-list'; } if (array_key_exists('name', $att) && $att['name']) { $entry['name'] = html2plain(purify_html($att['name']), 256); @@ -941,36 +943,8 @@ class Activity { } - if ($ret['type'] === 'emojiReaction') { - // There may not be an object for these items for legacy reasons - it should be the conversation parent. - $p = q("select * from item where mid = '%s' and uid = %d", - dbesc($i['parent_mid']), - intval($i['uid']) - ); - if ($p) { - xchan_query($p, true); - $p = fetch_post_tags($p); - $i['obj'] = self::encode_item($p[0]); - - // convert to zot6 emoji reaction encoding which uses the target object to indicate the - // specific emoji instead of overloading the verb or type. - - $im = explode('#', $i['verb']); - if ($im && count($im) > 1) - $emoji = $im[1]; - if (preg_match("/\[img(.*?)\](.*?)\[\/img\]/ism", $i['body'], $match)) { - $ln = $match[2]; - } - - $i['tgt_type'] = 'Image'; - - $i['target'] = [ - 'type' => 'Image', - 'name' => $emoji, - 'url' => (($ln) ? $ln : z_root() . '/images/emoji/' . $emoji . '.png') - ]; - - } + if ($ret['type'] === 'EmojiReact') { + $ret['content'] = $i['body']; } if (strpos($i['mid'], z_root() . '/item/') !== false) { @@ -985,15 +959,15 @@ class Activity { $ret['diaspora:guid'] = $i['uuid']; - if (isset($i['title']) && $i['title']) - $ret['name'] = html2plain(bbcode($i['title'], ['cache' => true])); + if (!empty($i['title'])) + $ret['name'] = html2plain(bbcode($i['title'])); - if (isset($i['summary']) && $i['summary']) - $ret['summary'] = bbcode($i['summary'], ['cache' => true]); + if (!empty($i['summary'])) + $ret['summary'] = bbcode($i['summary']); if ($ret['type'] === 'Announce') { $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism', EMPTY_STR, $i['body']); - $ret['content'] = bbcode($tmp, ['cache' => true]); + $ret['content'] = bbcode($tmp); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' @@ -1009,7 +983,7 @@ class Activity { } } - if (isset($i['app']) && $i['app']) { + if (!empty($i['app'])) { $ret['generator'] = ['type' => 'Application', 'name' => $i['app']]; } if (!empty($i['location']) || !empty($i['coord'])) { @@ -1050,6 +1024,7 @@ class Activity { if (!empty($cnv)) { if (is_string($cnv) && str_starts_with($cnv, z_root())) { $cnv = str_replace(['/item/', '/activity/'], ['/conversation/', '/conversation/'], $cnv); + $ret['contextHistory'] = $cnv; } $ret['context'] = $cnv; } @@ -1060,7 +1035,7 @@ class Activity { else return []; - if (isset($i['obj']) && $i['obj']) { + if (!empty($i['obj'])) { if (!is_array($i['obj'])) { $i['obj'] = json_decode($i['obj'], true); } @@ -1088,7 +1063,7 @@ class Activity { $ret['type'] = 'Invite'; } - if (isset($i['target']) && $i['target']) { + if (!empty($i['target'])) { if (!is_array($i['target'])) { $i['target'] = json_decode($i['target'], true); } @@ -1099,12 +1074,10 @@ class Activity { return []; } -/* this should not be needed $t = self::encode_taxonomy($i); if ($t) { $ret['tag'] = $t; } -*/ $a = self::encode_attachment($i, true); if ($a) { @@ -1115,7 +1088,6 @@ class Activity { $ret['to'] = [ACTIVITY_PUBLIC_INBOX]; } - $hookinfo = [ 'item' => $i, 'encoded' => $ret @@ -1716,9 +1688,9 @@ class Activity { return; } - $name = $person_obj['name'] ?? ''; + $name = ((isset($person_obj['name'])) ? escape_tags($person_obj['name']) : ''); if (!$name) { - $name = $person_obj['preferredUsername'] ?? ''; + $name = ((isset($person_obj['preferredUsername'])) ? escape_tags($person_obj['preferredUsername']) : ''); } if (!$name) { $name = t('Unknown'); @@ -1727,13 +1699,11 @@ class Activity { $webfinger_addr = ((isset($person_obj['webfinger'])) ? str_replace('acct:', '', $person_obj['webfinger']) : ''); $hostname = ''; $baseurl = ''; - $site_url = ''; $m = parse_url($url); if ($m) { - $hostname = $m['host']; - $baseurl = $m['scheme'] . '://' . $m['host'] . ((isset($m['port'])) ? ':' . $m['port'] : ''); - $site_url = $m['scheme'] . '://' . $m['host']; + $hostname = unparse_url($m, ['host']); + $baseurl = unparse_url($m, ['scheme', 'host', 'port']); } if (!$webfinger_addr && !empty($person_obj['preferredUsername']) && $hostname) { @@ -1835,7 +1805,7 @@ class Activity { q("UPDATE site SET site_update = '%s', site_dead = 0 WHERE site_url = '%s'", dbesc(datetime_convert()), - dbesc($site_url) + dbesc($baseurl) ); // update existing xchan record @@ -2081,6 +2051,9 @@ class Activity { $i = fetch_post_tags($i); $i[0]['obj'] = $o; + $edited = datetime_convert(); + $i[0]['edited'] = $edited; + // create the new object $newObj = self::build_packet(self::encode_activity($i[0]), $channel, true); @@ -2098,7 +2071,7 @@ class Activity { dbesc(json_encode($o)), intval($relatedItem['id']), dbesc($newObj), - dbesc(datetime_convert()) + dbesc($edited) ); dbq("COMMIT"); @@ -2145,35 +2118,25 @@ class Activity { $s['owner_xchan'] = $act->actor['id']; $s['author_xchan'] = $act->actor['id']; - $content = []; + $s['mid'] = self::getMessageID($act); - if (is_array($act->obj)) { - $content = self::get_content($act->obj); + if (!$s['mid']) { + return false; } - $s['mid'] = $act->objprop('id'); - - if (!$s['mid'] && is_string($act->obj)) { - $s['mid'] = $act->obj; - } + $s['uuid'] = self::getUUID($act); - // pleroma fetched activities - if (!$s['mid'] && isset($act->obj['data']['id'])) { - $s['mid'] = $act->obj['data']['id']; + if (!$s['uuid']) { + // If we have not found anything useful, create an uuid v5 from the mid + $s['uuid'] = uuid_from_url($s['mid']); } - if ($act->objprop('type') === 'Profile') { - $s['mid'] = $act->id; - } + $content = []; - if (!$s['mid']) { - return false; + if (is_array($act->obj)) { + $content = self::get_content($act->obj); } - // Friendica sends the diaspora guid in a nonstandard field via AP - // If no uuid is provided we will create an uuid v5 from the mid - $s['uuid'] = (($act->objprop('diaspora:guid')) ?: uuid_from_url($s['mid'])); - $s['parent_mid'] = $act->parent_id; if (array_key_exists('published', $act->data)) { @@ -2212,23 +2175,8 @@ class Activity { $response_activity = true; - $s['mid'] = $act->id; - $s['uuid'] = ((!empty($act->data['diaspora:guid'])) ? $act->data['diaspora:guid'] : uuid_from_url($s['mid'])); - $s['parent_mid'] = $act->objprop('id') ?: $act->obj; -/* - if ($act->objprop('inReplyTo')) { - $s['parent_mid'] = $act->objprop('inReplyTo'); - } - - $s['thr_parent'] = $act->objprop('id') ?: $act->obj; - - if (empty($s['parent_mid']) || empty($s['thr_parent'])) { - logger('response activity without parent_mid or thr_parent'); - return; - } -*/ // over-ride the object timestamp with the activity if (isset($act->data['published'])) { @@ -2239,9 +2187,9 @@ class Activity { $s['edited'] = datetime_convert('UTC', 'UTC', $act->data['updated']); } - $obj_actor = $act->objprop('actor') ?: $act->get_actor('attributedTo', $act->obj); + $obj_actor = is_array($act->objprop('actor')) ? $act->objprop('actor') : $act->get_actor('attributedTo', $act->obj); - if (!isset($obj_actor['id'])) { + if (empty($obj_actor['id'])) { return false; } @@ -2277,12 +2225,8 @@ class Activity { $content['content'] = sprintf(t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); } - // TODO: Deprecated - if ($act->type === 'emojiReaction') { - $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); - } - if (in_array($act->type, ['EmojiReact'])) { + // Pleroma reactions $t = trim(self::get_textfield($act->data, 'content')); @@ -2306,6 +2250,8 @@ class Activity { if ($s['mid'] === $s['parent_mid']) { $s['item_thread_top'] = 1; + $s['item_nocomment'] = 0; + $s['comments_closed'] = NULL_DATE; // it is a parent node - decode the comment policy info if present if ($act->objprop('commentPolicy')) { @@ -2313,7 +2259,7 @@ class Activity { if ($until !== false) { $s['comments_closed'] = datetime_convert('UTC', 'UTC', substr($act->obj['commentPolicy'], $until + 6)); if ($s['comments_closed'] < datetime_convert()) { - $s['nocomment'] = true; + $s['item_nocomment'] = 1; } } @@ -2330,10 +2276,15 @@ class Activity { if (!array_key_exists('edited', $s)) $s['edited'] = $s['created']; - $s['title'] = (($response_activity) ? EMPTY_STR : self::bb_content($content, 'name')); - $s['summary'] = self::bb_content($content, 'summary'); + $s['title'] = (($response_activity) ? EMPTY_STR : html2plain($content['name'])); + $s['summary'] = (($content['summary'] !== $content['content']) ? html2plain($content['summary']) : ''); $s['body'] = ((self::bb_content($content, 'bbcode') && (!$response_activity)) ? self::bb_content($content, 'bbcode') : self::bb_content($content, 'content')); + // peertube quirks + if ($act->objprop('mediaType') === 'text/markdown') { + $s['body'] = markdown_to_bb($act->objprop('content')); + } + if ($act->objprop('quoteUrl')) { $quote_bbcode = self::get_quote_bbcode($act->obj['quoteUrl']); @@ -2465,7 +2416,8 @@ class Activity { } } - $tag = (($poster) ? '[video poster="' . $poster . '"]' : '[video]' ); + $tag = (($poster) ? '[video poster=\'' . $poster . '\']' : '[video]' ); + $ptr = null; if ($act->objprop('url')) { @@ -2680,6 +2632,7 @@ class Activity { } } + if (!$ap_rawmsg && array_key_exists('signed', $raw_arr)) { // zap $ap_rawmsg = json_encode($act->data, JSON_UNESCAPED_SLASHES); @@ -2713,8 +2666,7 @@ class Activity { return $hookinfo['s']; } - - static function store($channel, $observer_hash, $act, $item, $fetch_parents = true, $force = false) { + static function store($channel, $observer_hash, $act, $item, $fetch_parents = true, $force = false, $is_collection_operation = false) { $is_sys_channel = is_sys_channel($channel['channel_id']); $is_child_node = false; $parent = null; @@ -2745,6 +2697,8 @@ class Activity { } $allowed = false; + $relay = false; + $permit_mentions = intval(PConfig::Get($channel['channel_id'], 'system','permit_all_mentions') && i_am_mentioned($channel, $item)); if ($is_child_node) { @@ -2771,13 +2725,22 @@ class Activity { return; } + $relay = $channel['channel_hash'] === $parent[0]['owner_xchan']; + + if (str_contains($parent[0]['tgt_type'], 'Collection') && !$relay && !$is_collection_operation) { + logger('not a collection activity'); + return; + } + if ($parent[0]['obj_type'] === 'Question') { if (in_array($item['obj_type'], ['Note', ACTIVITY_OBJ_COMMENT]) && $item['title'] && (!$item['body'])) { $item['obj_type'] = 'Answer'; + $item['item_hidden'] = 1; } } if ($parent[0]['item_wall']) { + // set the owner to the owner of the parent $item['owner_xchan'] = $parent[0]['owner_xchan']; @@ -2958,7 +2921,10 @@ class Activity { // This isn't perfect but the best we can do for now. $item['comment_policy'] = ((isset($act->data['commentPolicy'])) ? $act->data['commentPolicy'] : 'authenticated'); - if (!empty($act->obj['context'])) { + if (!empty($act->obj['contextHistory'])) { + IConfig::Set($item, 'activitypub', 'context', $act->obj['contextHistory'], 1); + } + elseif (!empty($act->obj['context'])) { IConfig::Set($item, 'activitypub', 'context', $act->obj['context'], 1); } @@ -2993,7 +2959,7 @@ class Activity { if (intval($parent[0]['item_private']) === 0) { if (intval($item['item_private'])) { - $item['item_restrict'] = $item['item_restrict'] | 1; + $item['item_restrict'] = ((isset($item['item_restrict'])) ? $item['item_restrict'] | 1 : 1); $item['allow_cid'] = '<' . $channel['channel_hash'] . '>'; $item['allow_gid'] = $item['deny_cid'] = $item['deny_gid'] = ''; } @@ -3010,7 +2976,7 @@ class Activity { } } } - +/* if (isset($item['term']) && !PConfig::Get($channel['channel_id'], 'system', 'no_smilies')) { foreach ($item['term'] as $t) { if ($t['ttype'] === TERM_EMOJI) { @@ -3024,6 +2990,7 @@ class Activity { } } } +*/ // TODO: not implemented // self::rewrite_mentions($item); @@ -3032,17 +2999,42 @@ class Activity { dbesc($item['mid']), intval($item['uid']) ); + if ($r) { if ($item['edited'] > $r[0]['edited']) { $item['id'] = $r[0]['id']; - $x = item_store_update($item); + $x = item_store_update($item, deliver: false); } else { return; } } else { - $x = item_store($item); + $x = item_store($item, deliver: false, addAndSync: false); + } + + if ($x['success']) { + if ($relay && $channel['channel_hash'] === $x['item']['owner_xchan'] && $x['item']['verb'] !== 'Add' && !$is_collection_operation) { + $approval = Activity::addToCollection($channel, $act->data, $x['item']['parent_mid'], $x['item'], deliver: false); + } + + if (check_item_source($channel['channel_id'], $x['item']) && in_array($x['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { + event_addtocal($x['item_id'], $channel['channel_id']); + } + + tag_deliver($channel['channel_id'], $x['item_id']); + + if ($relay && $is_child_node) { + // We are the owner of this conversation, so send all received comments back downstream + Master::Summon(['Notifier', 'comment-import', $x['item_id']]); + if (!empty($approval['item_id'])) { + Master::Summon(['Notifier', 'comment-import', $approval['item_id']]); + } + } + + send_status_notifications($x['item_id'], $x['item']); + + sync_an_item($channel['channel_id'], $x['item_id']); } if ($fetch_parents && $parent && !intval($parent[0]['item_private'])) { @@ -3069,28 +3061,6 @@ class Activity { } } } - - if ($x['success']) { - - if (check_item_source($channel['channel_id'], $x['item']) && in_array($x['item']['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { - event_addtocal($x['item_id'], $channel['channel_id']); - } - - if ($is_child_node) { - if ($item['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']); - } - } /** @@ -3380,10 +3350,10 @@ class Activity { if (array_key_exists('startTime', $act) && strpos($act['startTime'], -1, 1) === 'Z') { $adjust = true; $event['adjust'] = 1; - $event['dtstart'] = datetime_convert('UTC', 'UTC', $event['startTime'] . (($adjust) ? '' : 'Z')); + $event['dtstart'] = datetime_convert('UTC', 'UTC', $act['startTime'] . (($adjust) ? '' : 'Z')); } if (array_key_exists('endTime', $act)) { - $event['dtend'] = datetime_convert('UTC', 'UTC', $event['endTime'] . (($adjust) ? '' : 'Z')); + $event['dtend'] = datetime_convert('UTC', 'UTC', $act['endTime'] . (($adjust) ? '' : 'Z')); } else { $event['nofinish'] = true; @@ -3669,6 +3639,8 @@ class Activity { return [ 'zot' => z_root() . '/apschema#', + + 'contextHistory' => 'https://w3id.org/fep/171b/contextHistory', 'schema' => 'http://schema.org#', 'ostatus' => 'http://ostatus.org#', 'diaspora' => 'https://diasporafoundation.org/ns/', @@ -3692,7 +3664,6 @@ class Activity { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'Hashtag' => 'as:Hashtag' - ]; } @@ -3785,8 +3756,6 @@ class Activity { ->setObjType($object['type']) ->setParentMid(str_replace('/conversation/','/item/', $target)) ->setThrParent(str_replace('/conversation/','/item/', $target)) - // ->setApproved($object['object']['id'] ?? '') - // ->setReplyto(z_root() . '/channel/' . $channel['channel_address']) ->setTgtType('Collection') ->setTarget([ 'id' => str_replace('/item/','/conversation/', $target), @@ -3801,13 +3770,19 @@ class Activity { ->setDenyCid($sourceItem['deny_cid']) ->setDenyGid($sourceItem['deny_gid']) ->setPrivate($sourceItem['item_private']) - ->setNocomment($sourceItem['item_nocomment']) + ->setRestrict($sourceItem['item_restrict']) + ->setHidden($sourceItem['item_hidden']) + ->setDelayed($sourceItem['item_delayed']) + ->setUnpublished($sourceItem['item_unpublished']) + ->setBlocked($sourceItem['item_blocked']) + ->setType($sourceItem['item_type']) ->setCommentPolicy($sourceItem['comment_policy']) ->setPublicPolicy($sourceItem['public_policy']) ->setPostopts($sourceItem['postopts']); } $result = post_activity_item($item->toArray(), deliver: $deliver, channel: $channel, observer: $channel, addAndSync: false); logger('addToCollection: ' . print_r($result, true)); + return $result; } @@ -3839,4 +3814,39 @@ class Activity { return $result; } + + /** + * @brief Retrieves message ID from activity object. + * @param object $act Activity object + * @return string Message ID or empty string if not found + */ + public static function getMessageID($act): string + { + if (ActivityStreams::is_response_activity($act->type) || $act->objprop('type') === 'Profile') { + return $act->id; + } + + return $act->objprop('id', null) + ?? (is_string($act->obj) ? $act->obj : null) + ?? ''; + } + + /** + * @brief Retrieves the UUID from an activity object. + * @param object $act Activity object + * @return string UUID or empty string if not found + */ + public static function getUUID($act): string + { + if (ActivityStreams::is_response_activity($act->type)) { + return $act->data['uuid'] + ?? $act->data['diaspora:guid'] + ?? ''; + } + + return $act->objprop('uuid', null) + ?? $act->objprop('diaspora:guid', null) + ?? ''; + } + } diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index 55a1de5dd..f2b9050e3 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -529,8 +529,8 @@ class ActivityStreams { public function checkEddsaSignature() { $signer = $this->get_property_obj('verificationMethod', $this->sig); - $parseUrl = parse_url($signer); + $publicKey = null; if (isset($parseUrl['fragment'])) { if (str_starts_with($parseUrl['fragment'], 'z6Mk')) { diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php index 0dc405ea9..337344645 100644 --- a/Zotlabs/Lib/Apps.php +++ b/Zotlabs/Lib/Apps.php @@ -341,7 +341,7 @@ class Apps { 'Suggest Channels' => t('Suggest Channels'), 'Login' => t('Login'), 'Channel Manager' => t('Channel Manager'), - 'Network' => t('Stream'), + 'Network' => t('Network'), 'Settings' => t('Settings'), 'Files' => t('Files'), 'Webpages' => t('Webpages'), diff --git a/Zotlabs/Lib/Connect.php b/Zotlabs/Lib/Connect.php index b8e7a5c4e..9f6d077b4 100644 --- a/Zotlabs/Lib/Connect.php +++ b/Zotlabs/Lib/Connect.php @@ -24,10 +24,16 @@ class Connect { $uid = $channel['channel_id']; - if (strpos($url,'@') === false && strpos($url,'/') === false) { + // If we get just a channel name and it is not an URL turn it into a local webbie + if (!str_contains($url, '@') && strpos($url,'/') === false) { $url = $url . '@' . App::get_hostname(); } + // Remove a possible leading @ + if (str_starts_with($url, '@')) { + $url = ltrim($url, '@'); + } + $result = [ 'success' => false, 'message' => '' ]; $my_perms = false; diff --git a/Zotlabs/Lib/DReport.php b/Zotlabs/Lib/DReport.php index ac8e0d377..99bb05293 100644 --- a/Zotlabs/Lib/DReport.php +++ b/Zotlabs/Lib/DReport.php @@ -35,7 +35,7 @@ class DReport { } function addto_update($status) { - $this->status = $this->status . ' ' . $status; + $this->status = $this->status . ', ' . $status; } @@ -89,8 +89,14 @@ class DReport { if(array_key_exists('reject',$dr) && intval($dr['reject'])) return false; - if(! ($dr['sender'])) + if (!$dr['sender']) { return false; + } + + // do not store dismissed create activities + if ($dr['status'] === 'not a collection activity') { + return false; + } // Is the sender one of our channels? diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 9bffc53a0..6d5e249ef 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -95,8 +95,8 @@ class Enotify { if (array_key_exists('verb', $params['item'])) { // localize_item() alters the original item so make a copy first $i = $params['item']; - logger('calling localize'); - localize_item($i); + // logger('calling localize'); + // localize_item($i); $title = $i['title']; $body = $i['body']; $private = (($i['item_private']) || intval($i['item_obscured'])); @@ -131,9 +131,9 @@ class Enotify { logger('notification: mail'); $subject = sprintf( t('[$Projectname:Notify] New direct message received at %s'), $sitename); - $preamble = sprintf( t('%1$s sent you a new direct message at %2$s'), $sender['xchan_name'], $sitename); + $preamble = sprintf( t('%1$s sent you a new private message at %2$s'), $sender['xchan_name'], $sitename); $epreamble = sprintf( t('%1$s sent you %2$s.'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', '[zrl=$itemlink]' . t('a direct message') . '[/zrl]'); - $sitelink = t('Please visit %s to view and/or reply to your direct messages.'); + $sitelink = t('Please visit %s to view and/or reply to your private messages.'); $tsitelink = sprintf( $sitelink, $siteurl . '/hq/' . gen_link_id($params['item']['mid'])); $hsitelink = sprintf( $sitelink, '<a href="' . $siteurl . '/hq/' . gen_link_id($params['item']['mid']) . '">' . $sitename . '</a>'); $itemlink = $siteurl . '/hq/' . gen_link_id($params['item']['mid']); @@ -146,7 +146,7 @@ class Enotify { $itemlink = $params['link']; - $action = (($moderated) ? t('requested to comment on') : t('commented on')); + $action = (($moderated) ? t('requested to post in') : t('posted in')); if(array_key_exists('item',$params)) { @@ -164,8 +164,8 @@ class Enotify { if(activity_match($params['verb'], ['Dislike', ACTIVITY_DISLIKE])) $action = (($moderated) ? t('requested to dislike') : t('disliked')); - if(activity_match($params['verb'], ACTIVITY_SHARE)) - $action = t('repeated'); + if(activity_match($params['verb'], [ACTIVITY_SHARE])) + $action = (($moderated) ? t('requested to repeat') : t('repeated')); } @@ -213,28 +213,36 @@ class Enotify { //$possess_desc = str_replace('<!item_type!>',$possess_desc); // "a post" - $dest_str = sprintf(t('%1$s %2$s [zrl=%3$s]a %4$s[/zrl]'), + $dest_str = sprintf( + t('%1$s %2$s [zrl=%3$s]a %4$s[/zrl]'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', $action, $itemlink, - $item_post_type); + $item_post_type + ); // "George Bull's post" - if($p) - $dest_str = sprintf(t('%1$s %2$s [zrl=%3$s]%4$s\'s %5$s[/zrl]'), + if($p) { + $dest_str = sprintf( + t('%1$s %2$s [zrl=%3$s]%4$s\'s %5$s[/zrl]'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', $action, $itemlink, - $p[0]['author']['xchan_name'], - $item_post_type); + $parent_item['author']['xchan_name'], + $item_post_type + ); + } // "your post" - if($p[0]['owner']['xchan_name'] == $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) - $dest_str = sprintf(t('%1$s %2$s [zrl=%3$s]your %4$s[/zrl]'), + if ($parent_item['owner']['xchan_hash'] === $recip['channel_hash'] && intval($parent_item['item_wall'])) { + $dest_str = sprintf( + t('%1$s %2$s [zrl=%3$s]your %4$s[/zrl]'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', $action, $itemlink, - $item_post_type); + $item_post_type + ); + } // Some mail softwares relies on subject field for threading. // So, we cannot have different subjects for notifications of the same thread. @@ -263,7 +271,7 @@ class Enotify { $itemlink = $params['link']; - if (array_key_exists('item',$params) && (activity_match($params['item']['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE]))) { + if (array_key_exists('item',$params) && (activity_match($params['item']['verb'], ['Like', 'Dislike', ACTIVITY_LIKE, ACTIVITY_DISLIKE, 'Announce']))) { if(! $always_show_in_notices || !($vnotify & VNOTIFY_LIKE) || !feature_enabled($recip['channel_id'], 'dislike')) { logger('notification: not a visible activity. Ignoring.'); pop_lang(); @@ -308,7 +316,6 @@ class Enotify { $item_post_type = item_post_type($p[0]); // $private = $p[0]['item_private']; $parent_id = $p[0]['id']; - $parent_item = $p[0]; //$verb = ((activity_match($params['item']['verb'], ACTIVITY_DISLIKE)) ? t('disliked') : t('liked')); @@ -320,14 +327,18 @@ class Enotify { if(activity_match($params['item']['verb'], ['Dislike', ACTIVITY_DISLIKE])) $verb = (($moderated) ? t('requested to dislike') : t('disliked')); + if(activity_match($params['item']['verb'], [ACTIVITY_SHARE])) + $verb = (($moderated) ? t('requested to repeat') : t('repeated')); + // "your post" - if($p[0]['owner']['xchan_name'] === $p[0]['author']['xchan_name'] && intval($p[0]['item_wall'])) + if ($parent_item['author']['xchan_hash'] === $recip['channel_hash']) { $dest_str = sprintf(t('%1$s %2$s [zrl=%3$s]your %4$s[/zrl]'), '[zrl=' . $sender['xchan_url'] . ']' . $sender['xchan_name'] . '[/zrl]', $verb, $itemlink, $item_post_type ); + } else { pop_lang(); return; @@ -500,9 +511,14 @@ class Enotify { */ + $hash = ((in_array($params['verb'], ['Create', 'Update'])) ? $params['item']['uuid'] : $params['item']['thr_parent_uuid']); + + if (!$hash) { + $hash = new_uuid(); + } $datarray = []; - $datarray['hash'] = $params['item']['uuid'] ?? new_uuid(); + $datarray['hash'] = $hash; $datarray['sender_hash'] = $sender['xchan_hash']; $datarray['xname'] = $sender['xchan_name']; $datarray['url'] = $sender['xchan_url']; @@ -561,8 +577,9 @@ class Enotify { dbesc($datarray['otype']) ); - $r = q("select id from notify where hash = '%s' and ntype = %d and uid = %d limit 1", + $r = q("select id from notify where hash = '%s' and link = '%s' and ntype = %d and uid = %d limit 1", dbesc($datarray['hash']), + dbesc($itemlink), intval($datarray['ntype']), intval($recip['channel_id']) ); @@ -840,8 +857,8 @@ class Enotify { } else { $itemem_text = (($item['item_thread_top']) - ? (($item['obj_type'] === 'Question') ? t('created a new poll') : t('created a new post')) - : (($item['obj_type'] === 'Answer') ? sprintf( t('voted on %s\'s poll'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]') : sprintf( t('commented on %s\'s post'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]')) + ? (($item['obj_type'] === 'Question') ? t('started a poll') : t('started a conversation')) + : (($item['obj_type'] === 'Answer') ? sprintf( t('voted on %s\'s poll'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]') : sprintf( t('posted in %s\'s conversation'), '[bdi]' . $item['owner']['xchan_name'] . '[/bdi]')) ); if(in_array($item['obj_type'], ['Document', 'Video', 'Audio', 'Image'])) { @@ -853,12 +870,7 @@ class Enotify { if($item['edited'] > $item['created']) { $edit = true; - if($item['item_thread_top']) { - $itemem_text = sprintf( t('edited a post dated %s'), relative_date($item['created'])); - } - else { - $itemem_text = sprintf( t('edited a comment dated %s'), relative_date($item['created'])); - } + $itemem_text = sprintf( t('edited a message dated %s'), relative_date($item['created'])); } @@ -878,7 +890,7 @@ class Enotify { 'when' => (($edit) ? datetime_convert('UTC', date_default_timezone_get(), $item['edited']) : datetime_convert('UTC', date_default_timezone_get(), $item['created'])), 'class' => (intval($item['item_unseen']) ? 'notify-unseen' : 'notify-seen'), // 'b64mid' => (($item['mid']) ? gen_link_id($item['mid']) : ''), - 'b64mid' => (($item['uuid']) ? $item['uuid'] : ''), + 'b64mid' => ((in_array($item['verb'] , ['Like', 'Dislike', 'Announce']) && !empty($item['thr_parent_uuid'])) ? $item['thr_parent_uuid'] : $item['uuid'] ?? ''), //'b64mid' => ((in_array($item['verb'], [ACTIVITY_LIKE, ACTIVITY_DISLIKE])) ? gen_link_id($item['thr_parent']) : gen_link_id($item['mid'])), 'thread_top' => (($item['item_thread_top']) ? true : false), 'message' => bbcode(escape_tags($itemem_text)), @@ -898,14 +910,13 @@ class Enotify { } static public function format_notify($tt) { - $message = trim(strip_tags(bbcode($tt['msg']))); if(strpos($message, $tt['xname']) === 0) $message = substr($message, strlen($tt['xname']) + 1); $x = [ - 'notify_link' => (($tt['ntype'] === NOTIFY_MAIL) ? $tt['link'] : z_root() . '/notify/view/' . $tt['id']), + 'notify_link' => (($tt['ntype'] === NOTIFY_INTRO) ? z_root() . '/notify/view/' . $tt['id'] : $tt['link']), 'name' => $tt['xname'], 'url' => $tt['url'], 'photo' => $tt['photo'], @@ -917,11 +928,9 @@ class Enotify { ]; return $x; - } static public function format_intros($rr) { - return [ 'notify_link' => z_root() . '/connections#' . $rr['abook_id'], 'name' => $rr['xchan_name'], diff --git a/Zotlabs/Lib/IConfig.php b/Zotlabs/Lib/IConfig.php index 74c1107f0..3540c2b24 100644 --- a/Zotlabs/Lib/IConfig.php +++ b/Zotlabs/Lib/IConfig.php @@ -13,6 +13,7 @@ class IConfig { static public function Get(&$item, $family, $key, $default = false) { $is_item = false; + $iid = null; if(is_array($item)) { $is_item = true; @@ -27,12 +28,13 @@ class IConfig { elseif(intval($item)) $iid = $item; - if(! $iid) + if (!$iid) return $default; + if(is_array($item) && array_key_exists('iconfig',$item) && is_array($item['iconfig'])) { foreach($item['iconfig'] as $c) { - if($c['iid'] == $iid && $c['cat'] == $family && $c['k'] == $key) + if (isset($c['iid']) && $c['iid'] == $iid && isset($c['cat']) && $c['cat'] == $family && isset($c['k']) && $c['k'] == $key) return $c['v']; } } diff --git a/Zotlabs/Lib/JcsEddsa2022.php b/Zotlabs/Lib/JcsEddsa2022.php index 14f16c94b..c56f093af 100644 --- a/Zotlabs/Lib/JcsEddsa2022.php +++ b/Zotlabs/Lib/JcsEddsa2022.php @@ -7,11 +7,28 @@ use StephenHill\Base58; class JcsEddsa2022 { - public function __construct() { - return $this; - } - + /** + * Sign arbitrary data with the keys of the provided channel. + * + * @param $data The data to be signed. + * @param array $channel A channel as an array of key/value pairs. + * + * @return An array with the following fields: + * - `type`: The type of signature, always `DataIntegrityProof`. + * - `cryptosuite`: The cryptographic algorithm used, always `eddsa-jcs-2022`. + * - `created`: The UTC date and timestamp when the signature was created. + * - `verificationMethod`: The channel URL and the public key separated by a `#`. + * - `proofPurpose`: The purpose of the signature, always `assertionMethod`. + * - `proofValue`: The signature itself. + * + * @throws JcsEddsa2022SignatureException if the channel is missing, or + * don't have valid keys. + */ public function sign($data, $channel): array { + if (!is_array($channel) || !isset($channel['channel_epubkey'], $channel['channel_eprvkey'])) { + throw new JcsEddsa2022SignException('Invalid or missing channel provided.'); + } + $base58 = new Base58(); $pubkey = (new Multibase())->publicKey($channel['channel_epubkey']); $options = [ diff --git a/Zotlabs/Lib/JcsEddsa2022SignException.php b/Zotlabs/Lib/JcsEddsa2022SignException.php new file mode 100644 index 000000000..81d02d631 --- /dev/null +++ b/Zotlabs/Lib/JcsEddsa2022SignException.php @@ -0,0 +1,15 @@ +<?php +/* + * SPDX-FileCopyrightText: 2025 The Hubzilla Community + * SPDX-FileContributor: Harald Eilertsen <haraldei@anduin.net> + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +use Exception; + +class JcsEddsa2022SignException extends Exception +{ +} diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 3d499bd08..d2d696356 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -655,6 +655,11 @@ class Libzot { return $ret; } + if (empty($arr['primary_location']['address'])) { + logger('Empty primary location address: ' . print_r($arr, true), LOGGER_DEBUG); + return $ret; + } + /** * @hooks import_xchan * Called when processing the result of zot_finger() to store the result @@ -1148,7 +1153,7 @@ class Libzot { $AS = new ActivityStreams($data); // process add/remove from collection separately, as it requires a target. - // use the raw object, as it will not include actor expansion + // use the data object, as it will not include actor expansion if (in_array($AS->type, ['Add', 'Remove']) && is_array($AS->obj) && array_key_exists('object', $AS->obj) @@ -1164,10 +1169,6 @@ class Libzot { $raw_activity = $AS->data; $AS = new ActivityStreams($raw_activity['object'], portable_id: $env['sender']); - - // Store the original activity id and type for later usage - $AS->meta['original_id'] = $original_id; - $AS->meta['original_type'] = $original_type; } if (is_array($AS->obj)) { @@ -1227,7 +1228,6 @@ class Libzot { logger('public post'); - // Public post. look for any site members who are or may be accepting posts from this sender // and who are allowed to see them based on the sender's permissions // @fixme; @@ -1294,25 +1294,6 @@ class Libzot { $item['item_private'] = 1; } - if ($item['mid'] === $item['parent_mid']) { - if (is_array($AS->obj) && array_key_exists('commentPolicy', $AS->obj)) { - $p = strstr($AS->obj['commentPolicy'], 'until='); - if ($p !== false) { - $comments_closed_at = datetime_convert('UTC', 'UTC', substr($p, 6)); - if ($comments_closed_at === $item['created']) { - $item['item_nocomment'] = 1; - } - else { - $item['comments_closed'] = $comments_closed_at; - $aritemr['comment_policy'] = trim(str_replace($p, '', $AS->obj['commentPolicy'])); - } - } - else { - $item['comment_policy'] = $AS->obj['commentPolicy']; - } - } - } - if (!empty($AS->meta['hubloc']) || $AS->sigok) { $item['item_verified'] = true; } @@ -1561,6 +1542,7 @@ class Libzot { $local_public = $public; $item_result = null; + $parent = null; $DR = new DReport(z_root(), $sender, $d, $arr['mid'], $arr['uuid']); @@ -1576,7 +1558,7 @@ class Libzot { $conversation_operation = $is_collection_operation && isset($arr['target']['attributedTo']); - if (str_contains($arr['tgt_type'], 'Collection') && !$relay && !$conversation_operation) { + if (isset($arr['tgt_type']) && str_contains($arr['tgt_type'], 'Collection') && !$relay && !$conversation_operation) { $DR->update('not a collection activity'); $result[] = $DR->get(); continue; @@ -1663,22 +1645,24 @@ class Libzot { if (intval($channel['channel_system']) && (!$arr['item_private']) && (!$relay)) { $local_public = true; - $r = q("select xchan_selfcensored from xchan where xchan_hash = '%s' limit 1", - dbesc($sender) - ); - // don't import sys channel posts from selfcensored authors - if ($r && (intval($r[0]['xchan_selfcensored']))) { + $incl = Config::Get('system','pubstream_incl'); + $excl = Config::Get('system','pubstream_excl'); + + if(($incl || $excl) && !MessageFilter::evaluate($arr, $incl, $excl)) { $local_public = false; continue; } - $incl = Config::Get('system','pubstream_incl'); - $excl = Config::Get('system','pubstream_excl'); + $r = q("select xchan_selfcensored, xchan_censored from xchan where xchan_hash = '%s'", + dbesc($sender) + ); - if(($incl || $excl) && !MessageFilter::evaluate($arr, $incl, $excl)) { + // don't import sys channel posts from selfcensored or censored authors + if ($r && ($r[0]['xchan_selfcensored'] || $r[0]['xchan_censored'])) { $local_public = false; continue; } + } $tag_delivery = tgroup_check($channel['channel_id'], $arr); @@ -1740,6 +1724,7 @@ class Libzot { // If this is a poll response, convert the obj_type to our (internal-only) "Answer" type if (in_array($arr['obj_type'], ['Note', ACTIVITY_OBJ_COMMENT]) && $arr['title'] && (!$arr['body'])) { $arr['obj_type'] = 'Answer'; + $arr['item_hidden'] = 1; } } @@ -1865,19 +1850,12 @@ class Libzot { dbesc($arr['author_xchan']) ); - // If we import an add/remove activity ($is_collection_operation) we strip off the - // add/remove part and only process the object. - // When looking up the item to pass it to the notifier for relay, we need to look up - // the original (stripped off) message id which we stored in $act->meta. - - $sql_mid = (($is_collection_operation && $relay && $channel['channel_hash'] === $arr['owner_xchan']) ? $act->meta['original_id'] : $arr['mid']); - // 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($sql_mid), - dbesc(reverse_activity_mid($sql_mid)), + dbesc($arr['mid']), + dbesc(reverse_activity_mid($arr['mid'])), intval($channel['channel_id']) ); @@ -2008,10 +1986,10 @@ class Libzot { if ((is_array($stored)) && ($stored['id'] != $stored['parent']) && ($stored['author_xchan'] === $channel['channel_hash'])) { - retain_item($stored['item']['parent']); + retain_item($stored['parent']); } - if ($relay && $item_id && $stored['item_blocked'] !== ITEM_MODERATED) { + if ($relay && $item_id && item_forwardable($stored)) { logger('Invoking relay'); Master::Summon(['Notifier', 'relay', intval($item_id)]); if (!empty($approval) && $approval['item_id']) { @@ -2019,7 +1997,7 @@ class Libzot { } $DR->addto_update('relayed'); - $result[] = $DR->get(); + $result = [$DR->get()]; } } diff --git a/Zotlabs/Lib/Mailer.php b/Zotlabs/Lib/Mailer.php new file mode 100644 index 000000000..ca2d84d0d --- /dev/null +++ b/Zotlabs/Lib/Mailer.php @@ -0,0 +1,86 @@ +<?php +/** + * Mailer class for sending emails from Hubzilla. + * + * SPDX-FileCopyrightText: 2024 Hubzilla Community + * SPDX-FileContributor: Harald Eilertsen + * + * SPDX-License-Identifier: MIT + */ + +namespace Zotlabs\Lib; + +use App; + +/** + * A class for sending emails. + * + * Based on the previous `z_mail` function, but adaped and made more + * robust and usable as a class. + */ +class Mailer { + + public function __construct(private array $params = []) { + } + + public function deliver(): bool { + + if(empty($this->params['fromEmail'])) { + $this->params['fromEmail'] = Config::Get('system','from_email'); + if(empty($this->params['fromEmail'])) { + $this->params['fromEmail'] = 'Administrator@' . App::get_hostname(); + } + } + + if(empty($this->params['fromName'])) { + $this->params['fromName'] = Config::Get('system','from_email_name'); + if(empty($this->params['fromName'])) { + $this->params['fromName'] = System::get_site_name(); + } + } + + if(empty($this->params['replyTo'])) { + $this->params['replyTo'] = Config::Get('system','reply_address'); + if(empty($this->params['replyTo'])) { + $this->params['replyTo'] = 'noreply@' . App::get_hostname(); + } + } + + if (!isset($this->params['additionalMailHeader'])) { + $this->params['additionalMailHeader'] = ''; + } + + $this->params['sent'] = false; + $this->params['result'] = false; + + /** + * @hooks email_send + * * \e params @see z_mail() + */ + call_hooks('email_send', $this->params); + + if($this->params['sent']) { + logger('notification: z_mail returns ' . (($this->params['result']) ? 'success' : 'failure'), LOGGER_DEBUG); + return $this->params['result']; + } + + $fromName = email_header_encode(html_entity_decode($this->params['fromName'],ENT_QUOTES,'UTF-8'),'UTF-8'); + $messageSubject = email_header_encode(html_entity_decode($this->params['messageSubject'],ENT_QUOTES,'UTF-8'),'UTF-8'); + + $messageHeader = + $this->params['additionalMailHeader'] . + "From: $fromName <{$this->params['fromEmail']}>" . PHP_EOL . + "Reply-To: $fromName <{$this->params['replyTo']}>" . PHP_EOL . + "Content-Type: text/plain; charset=UTF-8"; + + // send the message + $res = mail( + $this->params['toEmail'], // send to address + $messageSubject, // subject + $this->params['textVersion'], + $messageHeader // message headers + ); + logger('notification: z_mail returns ' . (($res) ? 'success' : 'failure'), LOGGER_DEBUG); + return $res; + } +} diff --git a/Zotlabs/Lib/MessageFilter.php b/Zotlabs/Lib/MessageFilter.php index e7382c0d5..3f2db88c3 100644 --- a/Zotlabs/Lib/MessageFilter.php +++ b/Zotlabs/Lib/MessageFilter.php @@ -8,17 +8,18 @@ class MessageFilter { public static function evaluate($item, $incl, $excl) { - $text = prepare_text($item['body'],((isset($item['mimetype'])) ? $item['mimetype'] : 'text/bbcode')); - $text = html2plain(($item['title']) ? $item['title'] . ' ' . $text : $text); + $text = prepare_text($item['body'], ((isset($item['mimetype'])) ? $item['mimetype'] : 'text/bbcode')); + $text = html2plain((!empty($item['title'])) ? $item['title'] . ' ' . $text : $text); $lang = null; - if ((strpos($incl, 'lang=') !== false) || (strpos($excl, 'lang=') !== false) || (strpos($incl, 'lang!=') !== false) || (strpos($excl, 'lang!=') !== false)) { $lang = detect_language($text); } $tags = ((isset($item['term']) && is_array($item['term']) && count($item['term'])) ? $item['term'] : false); + $until = null; + // exclude always has priority $exclude = (($excl) ? explode("\n", $excl) : null); @@ -41,7 +42,13 @@ class MessageFilter { return false; } } - elseif (substr($word, 0, 1) === '#' && $tags) { + elseif (str_starts_with($word, 'until=')) { + $until = strtotime(trim(substr($word, 6))); + if ($until > strtotime($item['created'] . ' UTC')) { + return false; + } + } + elseif (substr($word, 0, 1) === '#' && $tags) { foreach ($tags as $t) { if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { return false; @@ -89,7 +96,13 @@ class MessageFilter { return true; } } - elseif (substr($word, 0, 1) === '#' && $tags) { + elseif (str_starts_with($word, 'until=')) { + $until = strtotime(trim(substr($word, 6))); + if ($until > strtotime($item['created'] . ' UTC')) { + return true; + } + } + elseif (substr($word, 0, 1) === '#' && $tags) { foreach ($tags as $t) { if ((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word, 1)) || (substr($word, 1) === '*'))) { return true; @@ -124,9 +137,7 @@ class MessageFilter { /** - * @brief Test for Conditional Execution conditions. Shamelessly ripped off from Code/Render/Comanche - * - * This is extensible. The first version of variable testing supports tests of the forms: + * Evaluate a conditional expression with support for AND (&&) and OR (||) operators. * * - ?foo ~= baz which will check if item.foo contains the string 'baz'; * - ?foo == baz which will check if item.foo is the string 'baz'; @@ -143,103 +154,110 @@ class MessageFilter { * * The values 0, '', an empty array, and an unset value will all evaluate to false. * - * @param string $s - * @param array $item - * @return bool + * @param string $s The condition string to evaluate. + * @param array $item The associative array providing variable values. + * @return bool True if the condition is met, false otherwise. */ - public static function test_condition($s,$item) { + public static function test_condition($s, $item) { + $s = trim($s); - if (preg_match('/(.*?)\s\~\=\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if (stripos($x, trim($matches[2])) !== false) { - return true; + // Handle OR (||) + // Split on '||' not inside quotes + $or_parts = preg_split('/\s*\|\|\s*/', $s); + if (count($or_parts) > 1) { + foreach ($or_parts as $part) { + if (self::test_condition(ltrim($part, '?+'), $item)) { + return true; + } } return false; } - if (preg_match('/(.*?)\s\=\=\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x == trim($matches[2])) { - return true; + // Handle AND (&&) + // Split on '&&' not inside quotes + $and_parts = preg_split('/\s*\&\&\s*/', $s); + if (count($and_parts) > 1) { + foreach ($and_parts as $part) { + if (!self::test_condition(ltrim($part, '?+'), $item)) { + return false; + } } - return false; + return true; + } + + // Basic checks + + // Contains substring (case-insensitive) + if (preg_match('/(.*?)\s\~\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return (stripos($x, trim($matches[2])) !== false); + } + + // Equality + if (preg_match('/(.*?)\s\=\=\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x == trim($matches[2])); } + // Inequality if (preg_match('/(.*?)\s\!\=\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x != trim($matches[2])) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x != trim($matches[2])); } + // Greater than or equal if (preg_match('/(.*?)\s\>\=\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x >= trim($matches[2])) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x >= trim($matches[2])); } + // Less than or equal if (preg_match('/(.*?)\s\<\=\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x <= trim($matches[2])) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x <= trim($matches[2])); } + // Greater than if (preg_match('/(.*?)\s\>\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x > trim($matches[2])) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x > trim($matches[2])); } - if (preg_match('/(.*?)\s\>\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x < trim($matches[2])) { - return true; - } - return false; + // Less than + if (preg_match('/(.*?)\s\<\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return ($x < trim($matches[2])); } - if (preg_match('/[\$](.*?)\s\{\}\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if (is_array($x) && in_array(trim($matches[2]), $x)) { - return true; - } - return false; + // Array contains value + if (preg_match('/(.*?)\s\{\}\s(.*?)$/', $s, $matches)) { + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return (is_array($x) && in_array(trim($matches[2]), $x)); } + // Array contains key if (preg_match('/(.*?)\s\{\*\}\s(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if (is_array($x) && array_key_exists(trim($matches[2]), $x)) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return (is_array($x) && array_key_exists(trim($matches[2]), $x)); } // Ordering of this check (for falsiness) with relation to the following one (check for truthiness) is important. + // Falsy check if (preg_match('/\!(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if (!$x) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return !$x; } + // Truthy check (default) if (preg_match('/(.*?)$/', $s, $matches)) { - $x = ((array_key_exists(trim($matches[1]),$item)) ? $item[trim($matches[1])] : EMPTY_STR); - if ($x) { - return true; - } - return false; + $x = ((array_key_exists(trim($matches[1]), $item)) ? $item[trim($matches[1])] : EMPTY_STR); + return (bool)$x; } + // If no conditions matched, return false return false; } + } diff --git a/Zotlabs/Lib/Text.php b/Zotlabs/Lib/Text.php index f593f9dd6..4a962670a 100644 --- a/Zotlabs/Lib/Text.php +++ b/Zotlabs/Lib/Text.php @@ -21,4 +21,13 @@ class Text { return htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false); } + public static function rawurlencode_parts(string $string): string { + if (!$string) { + return EMPTY_STR; + } + + return implode('/', array_map('rawurlencode', explode('/', $string))); + } + + } diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 90a3d3fc8..46fe6d815 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -4,8 +4,6 @@ namespace Zotlabs\Lib; use App; use Zotlabs\Access\AccessList; -use Zotlabs\Lib\Apps; -use Zotlabs\Lib\Config; require_once('include/text.php'); @@ -26,6 +24,7 @@ class ThreadItem { private $parent = null; private $conversation = null; private $redirect_url = null; + private $owner_addr = ''; private $owner_url = ''; private $owner_photo = ''; private $owner_name = ''; @@ -35,14 +34,12 @@ class ThreadItem { private $channel = null; private $display_mode = 'normal'; private $reload = ''; - private $mid_uuid_map = []; - public function __construct($data) { $this->data = $data; $this->toplevel = ($this->get_id() == $this->get_data_value('parent')); - $this->threaded = Config::Get('system','thread_allow'); + $this->threaded = ((local_channel()) ? PConfig::Get(local_channel(), 'system', 'thread_allow', true) : Config::Get('system', 'thread_allow', true)); // Prepare the children if(isset($data['children'])) { @@ -65,8 +62,6 @@ class ThreadItem { unset($this->data['children']); } - - // allow a site to configure the order and content of the reaction emoji list if($this->toplevel) { $x = Config::Get('system','reactions'); @@ -84,7 +79,7 @@ class ThreadItem { * _ false on failure */ - public function get_template_data($conv_responses, $mid_uuid_map, $thread_level=1, $conv_flags = []) { + public function get_template_data($thread_level=1, $conv_flags = []) { $result = []; $item = $this->get_data(); @@ -103,6 +98,8 @@ class ThreadItem { $conv = $this->get_conversation(); $observer = $conv->get_observer(); + $conv->mid_uuid_map[$item['mid']] = $item['uuid']; + $acl = new AccessList([]); $acl->set($item); @@ -114,7 +111,7 @@ class ThreadItem { $locktype = intval($item['item_private']); if ($locktype === 2) { - $lock = t('Direct message'); + $lock = t('Private message'); } // 0 = limited based on public policy @@ -209,9 +206,9 @@ class ThreadItem { } if (in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) { - $response_verbs[] = 'attendyes'; - $response_verbs[] = 'attendno'; - $response_verbs[] = 'attendmaybe'; + $response_verbs[] = 'accept'; + $response_verbs[] = 'reject'; + $response_verbs[] = 'tentativeaccept'; if($this->is_commentable() && $observer) { $isevent = true; $attend = array( t('I will attend'), t('I will not attend'), t('I might attend')); @@ -222,56 +219,8 @@ class ThreadItem { $response_verbs[] = 'answer'; } - if (!feature_enabled($conv->get_profile_owner(),'dislike')) { - unset($conv_responses['dislike']); - } - - $responses = get_responses($conv_responses,$response_verbs,$this,$item); - - $my_responses = []; - foreach($response_verbs as $v) { - $my_responses[$v] = ((isset($conv_responses[$v][$item['mid'] . '-m'])) ? 1 : 0); - } - -/* - - $like_count = ((x($conv_responses['like'],$item['mid'])) ? $conv_responses['like'][$item['mid']] : ''); - $like_list = ((x($conv_responses['like'],$item['mid'])) ? $conv_responses['like'][$item['mid'] . '-l'] : ''); - if (($like_list) && (count($like_list) > MAX_LIKERS)) { - $like_list_part = array_slice($like_list, 0, MAX_LIKERS); - array_push($like_list_part, '<a class="dropdown-item" href="#" data-toggle="modal" data-target="#likeModal-' . $this->get_id() . '"><b>' . t('View all') . '</b></a>'); - } else { - $like_list_part = ''; - } - $like_button_label = tt('Like','Likes',$like_count,'noun'); - - $repeat_count = ((x($conv_responses['announce'],$item['mid'])) ? $conv_responses['announce'][$item['mid']] : ''); - $repeat_list = ((x($conv_responses['announce'],$item['mid'])) ? $conv_responses['announce'][$item['mid'] . '-l'] : ''); - if (($repeat_list) && (count($repeat_list) > MAX_LIKERS)) { - $repeat_list_part = array_slice($repeat_list, 0, MAX_LIKERS); - array_push($repeat_list_part, '<a class="dropdown-item" href="#" data-toggle="modal" data-target="#repeatModal-' . $this->get_id() . '"><b>' . t('View all') . '</b></a>'); - } else { - $repeat_list_part = ''; - } - $repeat_button_label = tt('Repeat','Repeats',$repeat_count,'noun'); - - $showdislike = ''; - if (feature_enabled($conv->get_profile_owner(),'dislike')) { - $dislike_count = ((x($conv_responses['dislike'],$item['mid'])) ? $conv_responses['dislike'][$item['mid']] : ''); - $dislike_list = ((x($conv_responses['dislike'],$item['mid'])) ? $conv_responses['dislike'][$item['mid'] . '-l'] : ''); - $dislike_button_label = tt('Dislike','Dislikes',$dislike_count,'noun'); - if (($dislike_list) && (count($dislike_list) > MAX_LIKERS)) { - $dislike_list_part = array_slice($dislike_list, 0, MAX_LIKERS); - array_push($dislike_list_part, '<a class="dropdown-item" href="#" data-toggle="modal" data-target="#dislikeModal-' . $this->get_id() . '"><b>' . t('View all') . '</b></a>'); - } else { - $dislike_list_part = ''; - } - - $showdislike = ((x($conv_responses['dislike'],$item['mid'])) ? format_like($conv_responses['dislike'][$item['mid']],$conv_responses['dislike'][$item['mid'] . '-l'],'dislike',$item['mid']) : ''); - } - - $showlike = ((x($conv_responses['like'],$item['mid'])) ? format_like($conv_responses['like'][$item['mid']],$conv_responses['like'][$item['mid'] . '-l'],'like',$item['mid']) : ''); -*/ + $response_verbs[] = 'comment'; + $responses = get_responses($response_verbs, $item); /* * We should avoid doing this all the time, but it depends on the conversation mode @@ -281,7 +230,13 @@ class ThreadItem { $this->check_wall_to_wall(); + $children = $this->get_children(); + $children_count = count($children); + if($this->is_toplevel()) { + $conv->comments_total = $responses['comment']['count'] ?? 0; + $conv->comments_loaded = $children_count; + if((local_channel() && $conv->get_profile_owner() === local_channel()) || (local_channel() && App::$module === 'pubstream')) { $star = [ 'toggle' => t("Toggle Star Status"), @@ -293,7 +248,6 @@ class ThreadItem { $is_comment = true; } - $verified = (intval($item['item_verified']) ? t('Message signature validated') : ''); $forged = ((($item['sig']) && (! intval($item['item_verified']))) ? t('Message signature incorrect') : ''); $unverified = '' ; // (($this->is_wall_to_wall() && (! intval($item['item_verified']))) ? t('Message cannot be verified') : ''); @@ -326,15 +280,11 @@ class ThreadItem { if((in_array($item['obj_type'], ['Event', ACTIVITY_OBJ_EVENT])) && $conv->get_profile_owner() == local_channel()) $has_event = true; - $like = []; - $dislike = []; $reply_to = []; $reactions_allowed = false; if($this->is_commentable() && $observer) { - $like = array( t("I like this \x28toggle\x29"), t("like")); - $dislike = array( t("I don't like this \x28toggle\x29"), t("dislike")); - $reply_to = array( t("Reply to this comment"), t("reply"), t("Reply to")); + $reply_to = array( t("Reply to this message"), t("reply"), t("Reply to")); $reactions_allowed = true; } @@ -378,9 +328,8 @@ class ThreadItem { $viewthread = z_root() . '/channel/' . $owner_address . '?f=&mid=' . urlencode(gen_link_id($item['mid'])); $comment_count_txt = ['label' => sprintf(tt('%d comment', '%d comments', $total_children), $total_children), 'count' => $total_children]; - $list_unseen_txt = $unseen_comments ? ['label' => sprintf(t('%d unseen'), $unseen_comments), 'count' => $unseen_comments] : []; - $children = $this->get_children(); + $list_unseen_txt = $unseen_comments ? ['label' => sprintf(t('%d unseen'), $unseen_comments), 'count' => $unseen_comments] : []; $has_tags = (($body['tags'] || $body['categories'] || $body['mentions'] || $body['attachments'] || $body['folders']) ? true : false); @@ -390,14 +339,7 @@ class ThreadItem { $midb64 = $item['uuid']; $mids = [ $item['uuid'] ]; - $response_mids = []; - foreach($response_verbs as $v) { - if(isset($conv_responses[$v]['mids'][$item['mid']])) { - $response_mids = array_merge($response_mids, $conv_responses[$v]['mids'][$item['mid']]); - } - } - $mids = array_merge($mids, $response_mids); $json_mids = json_encode($mids); // Pinned item processing @@ -411,11 +353,26 @@ class ThreadItem { $contact = App::$contacts[$item['author_xchan']]; } + $blog_mode = $this->get_display_mode() === 'list'; + $load_more = false; + $load_more_title = ''; + $comments_total_percent = 0; + if (($conv->comments_total > $conv->comments_loaded) || ($blog_mode && $conv->comments_total > 3)) { + // provide a load more comments button + $load_more = true; + $load_more_title = sprintf(t('Load the next few of total %d comments'), $conv->comments_total); + $comments_total_percent = round(100 * 3 / $conv->comments_total); + } + + $expand = ''; + if ($this->threaded && !empty($item['comment_count'] && !$this->is_toplevel())) { + $expand = t('Expand Replies'); + } + $tmp_item = array( 'template' => $this->get_template(), 'mode' => $mode, 'item_type' => intval($item['item_type']), - //'type' => implode("",array_slice(explode("/",$item['verb']),-1)), 'body' => $body['html'], 'tags' => $body['tags'], 'categories' => $body['categories'], @@ -424,9 +381,9 @@ class ThreadItem { 'folders' => $body['folders'], 'text' => strip_tags($body['html']), 'id' => $this->get_id(), + 'parent' => $item['parent'], 'mid' => $midb64, 'mids' => $json_mids, - 'parent' => $item['parent'], 'author_id' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), 'author_is_group_actor' => (($item['author']['xchan_pubforum']) ? t('Forum') : ''), 'isevent' => $isevent, @@ -450,16 +407,15 @@ class ThreadItem { 'sparkle' => $sparkle, 'title' => $item['title'], 'title_tosource' => get_pconfig($conv->get_profile_owner(),'system','title_tosource'), - //'ago' => relative_date($item['created']), 'app' => $item['app'], 'str_app' => sprintf( t('from %s'), $item['app']), 'isotime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'c'), - 'localtime' => datetime_convert('UTC', date_default_timezone_get(), $item['created'], 'r'), - 'editedtime' => (($item['edited'] != $item['created']) ? sprintf( t('last edited: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['edited'], 'r')) : ''), - 'expiretime' => (($item['expires'] > NULL_DATE) ? sprintf( t('Expires: %s'), datetime_convert('UTC', date_default_timezone_get(), $item['expires'], 'r')):''), + 'localtime' => datetime_convert('UTC', date_default_timezone_get(), $item['created']), + 'editedtime' => (($item['edited'] != $item['created']) ? sprintf(t('Last edited %s'), relative_time($item['edited'])) : ''), + 'expiretime' => (($item['expires'] > NULL_DATE) ? sprintf(t('Expires %s'), relative_time($item['expires'])) : ''), 'lock' => $lock, 'locktype' => $locktype, - 'delayed' => $item['item_delayed'], + 'delayed' => (($item['item_delayed']) ? sprintf(t('Published %s'), relative_time($item['created'])) : ''), 'privacy_warning' => $privacy_warning, 'verified' => $verified, 'unverified' => $unverified, @@ -472,6 +428,7 @@ class ThreadItem { 'vote_title' => t('Voting Options'), 'is_comment' => $is_comment, 'is_new' => $is_new, + 'owner_addr' => $this->get_owner_addr(), 'owner_url' => $this->get_owner_url(), 'owner_photo' => $this->get_owner_photo(), 'owner_name' => $this->get_owner_name(), @@ -479,17 +436,16 @@ class ThreadItem { 'event' => $body['event'], 'has_tags' => $has_tags, 'reactions' => $this->reactions, -// Item toolbar buttons + // Item toolbar buttons 'emojis' => (($this->is_toplevel() && $this->is_commentable() && $observer && feature_enabled($conv->get_profile_owner(),'emojis')) ? '1' : ''), - 'like' => $like, - 'dislike' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike : ''), - 'reply_to' => (((! $this->is_toplevel()) && feature_enabled($conv->get_profile_owner(),'reply_to')) ? $reply_to : ''), + 'reply_to' => ((feature_enabled($conv->get_profile_owner(),'reply_to')) ? $reply_to : ''), 'top_hint' => t("Go to previous comment"), 'share' => $share, 'embed' => $embed, 'rawmid' => $item['mid'], + 'parent_mid' => $item['parent_mid'], 'plink' => get_plink($item), - 'edpost' => $edpost, // ((feature_enabled($conv->get_profile_owner(),'edit_posts')) ? $edpost : ''), + 'edpost' => $edpost, 'star' => ((feature_enabled($conv->get_profile_owner(),'star_posts') && ($item['item_type'] == ITEM_TYPE_POST)) ? $star : ''), 'tagger' => ((feature_enabled($conv->get_profile_owner(),'commtag')) ? $tagger : ''), 'filer' => ((feature_enabled($conv->get_profile_owner(),'filing') && ($item['item_type'] == ITEM_TYPE_POST)) ? $filer : ''), @@ -500,44 +456,24 @@ class ThreadItem { 'addtocal' => (($has_event) ? t('Add to Calendar') : ''), 'drop' => $drop, 'dropdown_extras' => $dropdown_extras, -// end toolbar buttons + // end toolbar buttons 'unseen_comments' => $unseen_comments, 'comment_count' => $total_children, 'comment_count_txt' => $comment_count_txt, 'list_unseen_txt' => $list_unseen_txt, 'markseen' => t('Mark all comments seen'), 'responses' => $responses, - 'my_responses' => $my_responses, - /* - 'like_count' => $like_count, - 'like_list' => $like_list, - 'like_list_part' => $like_list_part, - 'like_button_label' => $like_button_label, - 'like_modal_title' => t('Likes','noun'), - - 'repeat_count' => $repeat_count, - 'repeat_list' => $repeat_list, - 'repeat_list_part' => $repeat_list_part, - 'repeat_button_label' => $repeat_button_label, - 'repeat_modal_title' => t('Repeats','noun'), - - - 'dislike_modal_title' => t('Dislikes','noun'), - 'dislike_count' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_count : ''), - 'dislike_list' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_list : ''), - 'dislike_list_part' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_list_part : ''), - 'dislike_button_label' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike_button_label : ''), -*/ + // 'my_responses' => $my_responses, 'modal_dismiss' => t('Close'), - // 'showlike' => $showlike, - // 'showdislike' => $showdislike, 'comment' => ($item['item_delayed'] ? '' : $this->get_comment_box()), + 'comment_hidden' => feature_enabled($conv->get_profile_owner(),'reply_to'), + 'no_comment' => (($item['item_thread_top'] && $item['item_nocomment'])? t('Comments disabled') : ''), 'previewing' => ($conv->is_preview() ? true : false ), 'preview_lbl' => t('This is an unsaved preview'), 'wait' => t('Please wait'), 'thread_level' => $thread_level, 'settings' => $settings, - 'thr_parent_uuid' => (($item['parent_mid'] != $item['thr_parent']) ? $mid_uuid_map[$item['thr_parent']] : ''), + 'thr_parent_uuid' => (($item['parent_mid'] !== $item['thr_parent'] && isset($conv->mid_uuid_map[$item['thr_parent']])) ? $conv->mid_uuid_map[$item['thr_parent']] : ''), 'contact_id' => (($contact) ? $contact['abook_id'] : ''), 'moderate' => ($item['item_blocked'] == ITEM_MODERATED), 'moderate_approve' => t('Approve'), @@ -545,7 +481,25 @@ class ThreadItem { 'rtl' => in_array($item['lang'], rtl_languages()), 'reactions_allowed' => $reactions_allowed, 'reaction_str' => [t('Add yours'), t('Remove yours')], - 'is_contained' => $this->is_toplevel() && str_contains($item['tgt_type'], 'Collection') + 'is_contained' => $this->is_toplevel() && str_contains($item['tgt_type'], 'Collection'), + 'observer_activity' => [ + 'like' => intval($item['observer_like_count'] ?? 0), + 'dislike' => intval($item['observer_dislike_count'] ?? 0), + 'announce' => intval($item['observer_announce_count'] ?? 0), + 'comment' => intval($item['observer_comment_count'] ?? 0), + 'accept' => intval($item['observer_accept_count'] ?? 0), + 'reject' => intval($item['observer_reject_count'] ?? 0), + 'tentativeaccept' => intval($item['observer_tentativeaccept_count'] ?? 0) + ], + 'threaded' => $this->threaded, + 'blog_mode' => $blog_mode, + 'collapse_comments' => t('show less'), + 'expand_comments' => $this->threaded ? t('show more') : t('show all'), + 'load_more' => $load_more, + 'load_more_title' => $load_more_title, + 'comments_total' => $conv->comments_total, + 'comments_total_percent' => $comments_total_percent, + 'expand' => $expand ); $arr = array('item' => $item, 'output' => $tmp_item); @@ -554,33 +508,19 @@ class ThreadItem { $result = $arr['output']; $result['children'] = array(); - $nb_children = count($children); - $visible_comments = Config::Get('system','expanded_comments'); - if($visible_comments === false) - $visible_comments = 3; + $visible_comments = 3; // Config::Get('system', 'expanded_comments', 3); -// needed for scroll to comment from notification but needs more work -// as we do not want to open all comments unless there is actually an #item_xx anchor -// and the url fragment is not sent to the server. -// if(in_array(\App::$module,['display','update_display'])) -// $visible_comments = 99999; - - if(($this->get_display_mode() === 'normal') && ($nb_children > 0)) { + if(($this->get_display_mode() === 'normal') && ($children_count > 0)) { foreach($children as $child) { - $result['children'][] = $child->get_template_data($conv_responses, $mid_uuid_map, $thread_level + 1,$conv_flags); + $result['children'][] = $child->get_template_data($thread_level + 1, $conv_flags); } + // Collapse - if(($nb_children > $visible_comments) || ($thread_level > 1)) { + if($thread_level === 1 && $children_count > $visible_comments) { $result['children'][0]['comment_firstcollapsed'] = true; $result['children'][0]['num_comments'] = $comment_count_txt['label']; - $result['children'][0]['hide_text'] = t('show all'); - if($thread_level > 1) { - $result['children'][$nb_children - 1]['comment_lastcollapsed'] = true; - } - else { - $result['children'][$nb_children - ($visible_comments + 1)]['comment_lastcollapsed'] = true; - } + $result['children'][$children_count - ($visible_comments + 1)]['comment_lastcollapsed'] = true; } } @@ -833,7 +773,7 @@ class ThreadItem { */ private function get_comment_box() { - if(!$this->is_toplevel() && !Config::Get('system','thread_allow')) { + if(!$this->is_toplevel()) { return ''; } @@ -869,14 +809,15 @@ class ThreadItem { '$submit' => t('Submit'), '$edbold' => t('Bold'), '$editalic' => t('Italic'), + '$edhighlighter' => t('Highlight selected text'), '$eduline' => t('Underline'), '$edquote' => t('Quote'), '$edcode' => t('Code'), - '$edimg' => t('Image'), + '$edimg' => t('Embed (existing) photo from your photo albums'), '$edatt' => t('Attach/Upload file'), '$edurl' => t('Insert Link'), '$edvideo' => t('Video'), - '$preview' => t('Preview'), // ((feature_enabled($conv->get_profile_owner(),'preview')) ? t('Preview') : ''), + '$preview' => t('Preview'), '$can_upload' => (perm_is_allowed($conv->get_profile_owner(),get_observer_hash(),'write_storage') && $conv->is_uploadable()), '$feature_encrypt' => ((feature_enabled($conv->get_profile_owner(),'content_encrypt')) ? true : false), '$encrypt' => t('Encrypt text'), @@ -897,12 +838,13 @@ class ThreadItem { } /** - * Check if we are a wall to wall item and set the relevant properties + * Check if we are a wall to wall or announce item and set the relevant properties */ protected function check_wall_to_wall() { $conv = $this->get_conversation(); $this->wall_to_wall = false; $this->owner_url = ''; + $this->owner_addr = ''; $this->owner_photo = ''; $this->owner_name = ''; @@ -911,12 +853,14 @@ class ThreadItem { if($this->is_toplevel() && ($this->get_data_value('author_xchan') != $this->get_data_value('owner_xchan'))) { $this->owner_url = chanlink_hash($this->data['owner']['xchan_hash']); + $this->owner_addr = $this->data['owner']['xchan_addr']; $this->owner_photo = $this->data['owner']['xchan_photo_s']; $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_addr = $this->data['source']['xchan_addr']; $this->owner_photo = $this->data['source']['xchan_photo_s']; $this->owner_name = $this->data['source']['xchan_name']; $this->wall_to_wall = true; @@ -931,6 +875,10 @@ class ThreadItem { return $this->owner_url; } + private function get_owner_addr() { + return $this->owner_addr; + } + private function get_owner_photo() { return $this->owner_photo; } diff --git a/Zotlabs/Lib/ThreadStream.php b/Zotlabs/Lib/ThreadStream.php index fb3b6dd9b..1d968fa1a 100644 --- a/Zotlabs/Lib/ThreadStream.php +++ b/Zotlabs/Lib/ThreadStream.php @@ -24,6 +24,10 @@ class ThreadStream { private $prepared_item = ''; public $reload = ''; private $cipher = 'AES-128-CCM'; + public $mid_uuid_map = []; + public $comments_total = 0; + public $comments_loaded = 0; + // $prepared_item is for use by alternate conversation structures such as photos // wherein we've already prepared a top level item which doesn't look anything like @@ -211,16 +215,15 @@ class ThreadStream { * _ The data requested on success * _ false on failure */ - public function get_template_data($conv_responses, $mid_uuid_map) { + public function get_template_data() { $result = array(); foreach($this->threads as $item) { - if(($item->get_data_value('id') == $item->get_data_value('parent')) && $this->prepared_item) { $item_data = $this->prepared_item; } else { - $item_data = $item->get_template_data($conv_responses, $mid_uuid_map); + $item_data = $item->get_template_data(); } if(!$item_data) { logger('Failed to get item template data ('. $item->get_id() .').', LOGGER_DEBUG, LOG_ERR); diff --git a/Zotlabs/Lib/Traits/HelpHelperTrait.php b/Zotlabs/Lib/Traits/HelpHelperTrait.php index 63b0eb22e..69b3cc21b 100644 --- a/Zotlabs/Lib/Traits/HelpHelperTrait.php +++ b/Zotlabs/Lib/Traits/HelpHelperTrait.php @@ -89,7 +89,7 @@ trait HelpHelperTrait { ); return bbcode( - t("This page is not yet available in {$prefered_language_name}. See [observer.baseurl]/help/developer/developer_guide#Translations for information about how to help.") + t("This page is not yet available in {$prefered_language_name}. See [observer.baseurl]/help/developer/developers_guide#Translations for information about how to help.") ); } } |